mirror of
https://github.com/ilyakooo0/nixpkgs.git
synced 2024-12-27 13:57:10 +03:00
nixos/wrappers: allow setuid and setgid wrappers to run in user namespaces
In user namespaces where an unprivileged user is mapped as root and root is unmapped, setuid bits have no effect. However setuid root executables like mount are still usable *in the namespace* as the user already has the required privileges. This commit detects the situation where the wrapper gained no privileges that the parent process did not already have and in this case does less sanity checking. In short there is no need to be picky since the parent already can execute the foo.real executable themselves. Details: man 7 user_namespaces: Set-user-ID and set-group-ID programs When a process inside a user namespace executes a set-user-ID (set-group-ID) program, the process's effective user (group) ID inside the namespace is changed to whatever value is mapped for the user (group) ID of the file. However, if either the user or the group ID of the file has no mapping inside the namespace, the set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the process's effective user (group) ID is left unchanged. (This mirrors the semantics of executing a set-user-ID or set-group-ID program that resides on a filesystem that was mounted with the MS_NOSUID flag, as described in mount(2).) The effect of the setuid bit is that the real user id is preserved and the effective and set user ids are changed to the owner of the wrapper. We detect that no privilege was gained by checking that euid == suid == ruid. In this case we stop checking that euid == owner of the wrapper file. As a reminder here are the values of euid, ruid, suid, stat.st_uid and stat.st_mode & S_ISUID in various cases when running a setuid 42 executable as user 1000: Normal case: ruid=1000 euid=42 suid=42 setuid=2048, st_uid=42 nosuid mount: ruid=1000 euid=1000 suid=1000 setuid=2048, st_uid=42 inside unshare -rm: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534 inside unshare -rm, on a suid mount: ruid=0 euid=0 suid=0 setuid=2048, st_uid=65534
This commit is contained in:
parent
f292b4964c
commit
0e4b8a05b2
@ -1,3 +1,4 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@ -16,7 +17,10 @@
|
||||
#include <syscall.h>
|
||||
#include <byteswap.h>
|
||||
|
||||
// aborts when false, printing the failed expression
|
||||
#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
|
||||
// aborts when returns non-zero, printing the failed expression and errno
|
||||
#define MUSTSUCCEED(expr) ((expr) ? print_errno_and_die(#expr) : (void) 0)
|
||||
|
||||
extern char **environ;
|
||||
|
||||
@ -41,6 +45,12 @@ static noreturn void assert_failure(const char *assertion) {
|
||||
abort();
|
||||
}
|
||||
|
||||
static noreturn void print_errno_and_die(const char *assertion) {
|
||||
fprintf(stderr, "Call `%s` in NixOS's wrapper.c failed: %s\n", assertion, strerror(errno));
|
||||
fflush(stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
int get_last_cap(unsigned *last_cap) {
|
||||
FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r");
|
||||
if (file == NULL) {
|
||||
@ -177,6 +187,17 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
|
||||
}
|
||||
|
||||
unsigned int ruid, euid, suid, rgid, egid, sgid;
|
||||
MUSTSUCCEED(getresuid(&ruid, &euid, &suid));
|
||||
MUSTSUCCEED(getresgid(&rgid, &egid, &sgid));
|
||||
|
||||
// If true, then we did not benefit from setuid privilege escalation,
|
||||
// where the original uid is still in ruid and different from euid == suid.
|
||||
int didnt_suid = (ruid == euid) && (euid == suid);
|
||||
// If true, then we did not benefit from setgid privilege escalation
|
||||
int didnt_sgid = (rgid == egid) && (egid == sgid);
|
||||
|
||||
|
||||
// Make sure that we are being executed from the right location,
|
||||
// i.e., `safe_wrapper_dir'. This is to prevent someone from creating
|
||||
// hard link `X' from some other location, along with a false
|
||||
@ -189,15 +210,22 @@ int main(int argc, char **argv) {
|
||||
ASSERT('/' == wrapper_dir[0]);
|
||||
ASSERT('/' == self_path[len]);
|
||||
|
||||
// Make *really* *really* sure that we were executed as
|
||||
// `self_path', and not, say, as some other setuid program. That
|
||||
// is, our effective uid/gid should match the uid/gid of
|
||||
// `self_path'.
|
||||
// If we got privileges with the fs set[ug]id bit, check that the privilege we
|
||||
// got matches the one one we expected, ie that our effective uid/gid
|
||||
// matches the uid/gid of `self_path`. This ensures that we were executed as
|
||||
// `self_path', and not, say, as some other setuid program.
|
||||
// We don't check that if we did not benefit from the set[ug]id bit, as
|
||||
// can be the case in nosuid mounts or user namespaces.
|
||||
struct stat st;
|
||||
ASSERT(lstat(self_path, &st) != -1);
|
||||
|
||||
ASSERT(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
|
||||
ASSERT(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
|
||||
// if the wrapper gained privilege with suid, check that we got the uid of the file owner
|
||||
ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid));
|
||||
// if the wrapper gained privilege with sgid, check that we got the gid of the file group
|
||||
ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid));
|
||||
// same, but with suid instead of euid
|
||||
ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid));
|
||||
ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid));
|
||||
|
||||
// And, of course, we shouldn't be writable.
|
||||
ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH)));
|
||||
|
@ -55,6 +55,10 @@ in
|
||||
out = machine.succeed(cmd_as_regular(cmd)).strip()
|
||||
assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
|
||||
|
||||
def test_as_regular_in_userns_mapped_as_root(cmd, expected):
|
||||
out = machine.succeed(f"su -l regular -c '${pkgs.util-linux}/bin/unshare -rm {cmd}'").strip()
|
||||
assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
|
||||
|
||||
test_as_regular('${busybox pkgs}/bin/busybox id -u', '${toString userUid}')
|
||||
test_as_regular('${busybox pkgs}/bin/busybox id -ru', '${toString userUid}')
|
||||
test_as_regular('${busybox pkgs}/bin/busybox id -g', '${toString usersGid}')
|
||||
@ -70,10 +74,27 @@ in
|
||||
test_as_regular('/run/wrappers/bin/sgid_root_busybox id -g', '0')
|
||||
test_as_regular('/run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}')
|
||||
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -u', '0')
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -ru', '0')
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -g', '0')
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -rg', '0')
|
||||
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -u', '0')
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -ru', '0')
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -g', '0')
|
||||
test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -rg', '0')
|
||||
|
||||
# We are only testing the permitted set, because it's easiest to look at with capsh.
|
||||
machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN'))
|
||||
machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN'))
|
||||
machine.succeed(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_CHOWN'))
|
||||
machine.fail(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_SYS_ADMIN'))
|
||||
|
||||
# test a few "attacks" against which the wrapper protects itself
|
||||
machine.succeed("cp /run/wrappers/bin/suid_root_busybox{,.real} /tmp/")
|
||||
machine.fail(cmd_as_regular("/tmp/suid_root_busybox id -u"))
|
||||
|
||||
machine.succeed("chmod u+s,a+w /run/wrappers/bin/suid_root_busybox")
|
||||
machine.fail(cmd_as_regular("/run/wrappers/bin/suid_root_busybox id -u"))
|
||||
'';
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user