During our Apport research we exploited Ubuntu’s crash handler, and following that, we decided to once again audit the coredump creation code. But this time, we chose to focus on a more general different target, rather than a specific crash handler. In this post, we will explore how the Linux kernel itself behaves when a process crash happens.
We will show bugs we found in the Linux kernel that allow unprivileged users to create root-owned core files, and how we were able to use them to get an LPE through the sudo program on machines that have been configured by administrators to allow running a single innocent command.
On Linux, a coredump will be generated for a process upon receiving several signals. The signals that result in a core dump are listed here (taken from “man signal”):
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
SIGABRT P1990 Core Abort signal from abort(3)
SIGBUS P2001 Core Bus error (bad memory access)
SIGFPE P1990 Core Floating-point exception
SIGILL P1990 Core Illegal Instruction
SIGIOT - Core IOT trap. A synonym for SIGABRT
SIGQUIT P1990 Core Quit from keyboard
SIGSEGV P1990 Core Invalid memory reference
SIGSYS P2001 Core Bad system call (SVr4);
see also seccomp(2)
SIGTRAP P2001 Core Trace/breakpoint trap
SIGUNUSED - Core Synonymous with SIGSYS
SIGXCPU P2001 Core CPU time limit exceeded (4.2BSD);
see setrlimit(2)
SIGXFSZ P2001 Core File size limit exceeded (4.2BSD);
see setrlimit(2)
When a process receives one of these signals, it will terminate and a coredump will be created. The coredump can be used to explore the memory of the process at the time of a crash.
Every process in Linux has an attribute called “dumpable”, this attribute is used to determine whether to generate a core file for a crashing process.
There are several possible dumpable values:
Usually, a process initial dumpable value is set to 1, but in specific cases (described here) a dumpable value may change. One of these cases is when the binary that is being executed is a suid program, suid programs have permissions of another user thus they can access files and resources that the original user can’t. We don’t want to allow suid programs to create coredump, to prevent leakage of such valuable information.
When a suid program is executed the dumpable attribute is reset to the dumpable value defined in /proc/sys/fs/suid_dumpable
.
But where does this decision occur? We noticed that the dumpable value is determined when a new process is executed.
// Code taken from /fs/exec.c (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); // suid_dumpable from /proc
else
set_dumpable(current->mm, SUID_DUMP_USER); // SUID_DUMP_USER == 1
If the process has (ruid != euid) or (rgid != egid) then the process dumpable value will be taken from /proc/sys/fs/suid_dumpable
.
It’s now clear that a suid program that has a real uid that differs from the effective uid would have its dumpable value changed. But it made us wonder “What would be the dumpable value of a child of a suid process?”.
The answer to this question is that the child process would have a dumpable value of SUID_DUMP_USER (1) only if the suid process dropped its privileges so ruid == euid and guid == egid.
If we find a suid binary that creates a child which is not suid, and we find a way to crash that child - a core dump will be generated on behalf of the user of the crashed process. To get extra privileges we want this user to be root.
To exploit this behavior we had to find a suid binary that meets the following requirements:
setuid(0)
and setgid(0)
so our coredump will be created with root privileges.execve
syscall.After investigating a few binaries we found that we can use sudo to exploit this issue. Although this vulnerability can’t be exploited in the default configuration of sudo that comes in popular distros such as Ubuntu or Debian, we will show how we are able to exploit sudo configurations that allow a user to run a single command as root. These setups are very common, for example an administator may permit users to restart a daemon.
To demonstrate the impact of this issue, our setup will allow a user only to run /usr/bin/true
binary as root. This binary has almost no logic in it, and we are able to use the same method to exploit any other innocent “safe” binary.
Sudo is a suid binary that allows users to execute commands as root or any other user. Before executing the command, sudo will drop its privileges to the privileges of the user who was specified to run the command through sudo (root by default).
Sudo can be configured through /etc/sudoers
file, in this file administrators can specify which command a specific user can run as root.
As we said before we will allow only execution of /usr/bin/true
, we added the following line to sudoers file (‘user’ is unprivileged user):
user ALL= /usr/bin/true
Now, ‘user’ can only run ‘true’ via sudo, when executed through sudo, true’s ruid and euid are 0. The dumpable value of true with sudo is 1, that’s means that the only thing left for us to do is to make ‘true’ crash and generate a coredump.
As we saw before, a process capable of creating a coredump will create one if it receives one of the signals listed above.
Most of the signals are generated by a misbehavior of the executing program, such as referencing invalid memory (SIGSEGV
) or executing illegal instructions (SIGILL
). We can’t rely on the logic of the executed program because we want to be able to generate a core dump for any program executed through sudo.
We found two methods for causing children of a suid process to terminate and create a core file.
One of the signals listed above is SIGXCPU
. This signal is unique compared to the rest - a process receives this signal if it exceeds the CPU time limit configured through setrlimit
.
Resource limits (rlimit) can be set for a process by the program. One of them is RLIMIT_CPU
, which specifies the seconds of CPU time a process is allowed to use. When a process reaches this limit, it will receive the SIGXCPU
signal, resulting in a core dump.
Child processes inherit rlimit values from their parents, therefore we need to set RLIMIT_CPU
before executing the suid binary (sudo in our case). To crash a child process of a suid binary, we need the limit not to reach the suid binary before the child process is executed.
Until commit 2bbdbdae05167c688b6d3499a7dab74208b80a22 in the Linux kernel, it was not possible to provide 0 as RLIMIT_CPU
. Instead, the kernel would treat it as 1, but since then it’s possible to provide 0 which will terminate the process almost immediately even for very fast child processes.
Another rlimit is RLIMIT_CORE
which specifies the maximum size of a core file, child of suid also inherits this limit from their parent.
The first method introduced has its own downsides, it can only receive integer seconds in the limit, and on older kernels it cannot crash immediately. We wanted to find another method to create coredump in the child which is more flexible and to be able to control more precisely when we terminate the child process.
We found SIGQUIT
, which is another signal that causes a core dump. This signal can be sent to a program directly or by pressing “CTRL + \” in the terminal the program runs in.
You might wonder how we are able to send SIGQUIT
from our unprivileged user to a process with real uid of root.
The answer to that question resides in the code of the tty driver, 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 (the kernel sends the signal, therefore we bypass the fact that we cannot send signals from our user directly to the child of suid).
This behavior works as expected, when you run a program via sudo you want to be able to send several signals to the child process (this driver also sends SIGINT
on CTRL+C press).
We can fork a new process and execute the suid binary. Meanwhile, at the parent process we will “insert” “CTRL + \” (SIGQUIT
) character programmatically to the terminal.
Now that we have several ways to crash the child of sudo, we are able to generate core dumps with root permissions in arbitrary directories.
As we’ve done in the previous post, our LPE relies on using logrotate to execute arbitrary code. We can achieve this by writing our core file to /etc/logrotate.d/
directory.
Logrotate has a permissive parsing configuration, which ignores all binary content in our core file, and only processes valid string data.
To be able to use logrotate for our needs, we had to find a way to include a logrotate configuration string in the memory of the child process (that will appear in the core dump). We know that environment variables are inherited by children from their parent processes.
After some trial and error, we noticed that not all environment variables from the parent exist in the child of sudo. We looked at the source code of sudo, and it turned out that sudo filters environment variables and only passes a limited number of variables to the child process.
One variable that is included in the child without any modifications is XAUTHORITY. We indeed were able to include our logrotate configuration in this variable!
Our final exploit strategy is as follows (tested on Ubuntu 21.04 and Debian 11):
/etc/logrotate
, the core file will be generated there.RLIMIT_CORE
to unlimited.RLIMIT_CPU
to 0 / send SIGQUIT
to the parent which will terminate children with coredump.Our program will sometimes crash before sudo has time to execute the privileged child. We can solve this by running our exploit several times in a loop. A core file will be generated in /etc/logrotate.d/
after several attempts.
We included a reverse shell payload in the coredump, which connects to our server, and we should see a root shell on the next execution of logrotate.
Another approach was to find another suid binary that executes predefined child processes. We downloaded several distros and tested them to see if a binary that meets our requirements from above exists.
We found su
, which is a suid binary.
For authentication su
uses PAM, which is a collection of libraries that handle the authentication task for applications.
Each program can specify an authentication “stack” which specifies what modules to use from a predefined set of modules.
su
uses several modules to authenticate, one of them is pam_unix.so
.
In this module, the module will run an external binary, /usr/sbin/unix_chkpwd
, in one of the code paths to assist with authentication. In an older version (which is included in up-to-date Ubuntu and CentOS) we can reach the code path if the system has SELinux supported.
We experimented with crashing su
’s children as an unauthenticated user without the same restrictions as before - and we are able to crash the program on default configuration without any changes by an administrator.
We have decided to focus on CentOS which has SELinux enforced by default. CentOS 8 is not exploitable, since it uses systemd-coredump
which only saves core dumps in a specific directory. We have decided to test the issue on CentOS 7 (still receives security updates), which uses abrt
as a core dump handler.
CentOS 7 runs an older kernel that doesn’t support providing zero to RLIMIT_CPU, therefore we used the second method of sending CTRL + \ to stdin to crash the helper program.
The helper program runs very quickly and we don’t know how much time it takes from the execution of su
until the helper executes. Therefore, we just ran our exploit several times in a loop with incremental sleep intervals prior to sending the SIGQUIT signal, until one of them succeed.
Using this method, we were able to generate core dump files in /etc/logrotate.d/
.
But before executing the child, pam_unix
wipes all the previous environment variables.
As for now, we haven’t found a way yet to include a logrotate configuration in the memory of the child before the coredump. Therefore, execution of commands is not possible without these strings in the memory.
In this research we dug deeper into how core dump handling happens inside the Linux kernel. We explored several different mechanisms which are all individually valid, but combined together can create dangerous unwanted behavior.
There are a few workarounds that can meanwhile be implemented to protect against this exploit.
Change /proc/sys/kernel/core_pattern
to an absolute, safe directory (this way, logrotate cannot be triggered).
Apparently, at some point in time, pam_limits.so
was removed from the PAM configuration of sudo (in Debian and Ubuntu). Adding this module back to sudo’s PAM configuration and setting RLIMIT_CORE to 0 makes sudo immune to this vulnerability.
Even without abusing logrotate this issue can still allow unauthorized user to create core dumps as root, even in directories where the user doesn’t have write permissions. This can lead to DoS by filling all of the storage quota of another user. Abusing like that should be simpler for an attacker as it doesn’t require injecting clear text commands in the process’ memory and doesn’t rely on logrotate being used.