lol

Merge pull request #231673 from symphorien/suid_wrappers_userns

authored by

Ryan Lahfa and committed by
GitHub
ec409e6f ce5e4a6e

+55 -6
+34 -6
nixos/modules/security/wrappers/wrapper.c
··· 1 + #define _GNU_SOURCE 1 2 #include <stdlib.h> 2 3 #include <stdio.h> 3 4 #include <string.h> ··· 16 17 #include <syscall.h> 17 18 #include <byteswap.h> 18 19 20 + // aborts when false, printing the failed expression 19 21 #define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr)) 22 + // aborts when returns non-zero, printing the failed expression and errno 23 + #define MUSTSUCCEED(expr) ((expr) ? print_errno_and_die(#expr) : (void) 0) 20 24 21 25 extern char **environ; 22 26 ··· 37 41 38 42 static noreturn void assert_failure(const char *assertion) { 39 43 fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion); 44 + fflush(stderr); 45 + abort(); 46 + } 47 + 48 + static noreturn void print_errno_and_die(const char *assertion) { 49 + fprintf(stderr, "Call `%s` in NixOS's wrapper.c failed: %s\n", assertion, strerror(errno)); 40 50 fflush(stderr); 41 51 abort(); 42 52 } ··· 177 187 fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size)); 178 188 } 179 189 190 + unsigned int ruid, euid, suid, rgid, egid, sgid; 191 + MUSTSUCCEED(getresuid(&ruid, &euid, &suid)); 192 + MUSTSUCCEED(getresgid(&rgid, &egid, &sgid)); 193 + 194 + // If true, then we did not benefit from setuid privilege escalation, 195 + // where the original uid is still in ruid and different from euid == suid. 196 + int didnt_suid = (ruid == euid) && (euid == suid); 197 + // If true, then we did not benefit from setgid privilege escalation 198 + int didnt_sgid = (rgid == egid) && (egid == sgid); 199 + 200 + 180 201 // Make sure that we are being executed from the right location, 181 202 // i.e., `safe_wrapper_dir'. This is to prevent someone from creating 182 203 // hard link `X' from some other location, along with a false ··· 189 210 ASSERT('/' == wrapper_dir[0]); 190 211 ASSERT('/' == self_path[len]); 191 212 192 - // Make *really* *really* sure that we were executed as 193 - // `self_path', and not, say, as some other setuid program. That 194 - // is, our effective uid/gid should match the uid/gid of 195 - // `self_path'. 213 + // If we got privileges with the fs set[ug]id bit, check that the privilege we 214 + // got matches the one one we expected, ie that our effective uid/gid 215 + // matches the uid/gid of `self_path`. This ensures that we were executed as 216 + // `self_path', and not, say, as some other setuid program. 217 + // We don't check that if we did not benefit from the set[ug]id bit, as 218 + // can be the case in nosuid mounts or user namespaces. 196 219 struct stat st; 197 220 ASSERT(lstat(self_path, &st) != -1); 198 221 199 - ASSERT(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid())); 200 - ASSERT(!(st.st_mode & S_ISGID) || (st.st_gid == getegid())); 222 + // if the wrapper gained privilege with suid, check that we got the uid of the file owner 223 + ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid)); 224 + // if the wrapper gained privilege with sgid, check that we got the gid of the file group 225 + ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid)); 226 + // same, but with suid instead of euid 227 + ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid)); 228 + ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid)); 201 229 202 230 // And, of course, we shouldn't be writable. 203 231 ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH)));
+21
nixos/tests/wrappers.nix
··· 55 55 out = machine.succeed(cmd_as_regular(cmd)).strip() 56 56 assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out) 57 57 58 + def test_as_regular_in_userns_mapped_as_root(cmd, expected): 59 + out = machine.succeed(f"su -l regular -c '${pkgs.util-linux}/bin/unshare -rm {cmd}'").strip() 60 + assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out) 61 + 58 62 test_as_regular('${busybox pkgs}/bin/busybox id -u', '${toString userUid}') 59 63 test_as_regular('${busybox pkgs}/bin/busybox id -ru', '${toString userUid}') 60 64 test_as_regular('${busybox pkgs}/bin/busybox id -g', '${toString usersGid}') ··· 70 74 test_as_regular('/run/wrappers/bin/sgid_root_busybox id -g', '0') 71 75 test_as_regular('/run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}') 72 76 77 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -u', '0') 78 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -ru', '0') 79 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -g', '0') 80 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -rg', '0') 81 + 82 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -u', '0') 83 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -ru', '0') 84 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -g', '0') 85 + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -rg', '0') 86 + 73 87 # We are only testing the permitted set, because it's easiest to look at with capsh. 74 88 machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN')) 75 89 machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN')) 76 90 machine.succeed(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_CHOWN')) 77 91 machine.fail(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_SYS_ADMIN')) 92 + 93 + # test a few "attacks" against which the wrapper protects itself 94 + machine.succeed("cp /run/wrappers/bin/suid_root_busybox{,.real} /tmp/") 95 + machine.fail(cmd_as_regular("/tmp/suid_root_busybox id -u")) 96 + 97 + machine.succeed("chmod u+s,a+w /run/wrappers/bin/suid_root_busybox") 98 + machine.fail(cmd_as_regular("/run/wrappers/bin/suid_root_busybox id -u")) 78 99 ''; 79 100 })