Qualys Security Advisory LPEs in needrestart (CVE-2024-48990, CVE-2024-48991, CVE-2024-48992, CVE-2024-10224, and CVE-2024-11003) ======================================================================== Contents ======================================================================== Summary Background CVE-2024-48990 (and CVE-2024-48992) CVE-2024-48991 CVE-2024-10224 (and CVE-2024-11003) Mitigation Acknowledgments Timeline I got bugs I got bugs in my room Bugs in my bed Bugs in my ears Their eggs in my head -- Pearl Jam, "Bugs" ======================================================================== Summary ======================================================================== needrestart (from https://github.com/liske/needrestart) is a Perl tool that is installed by default on Ubuntu Server since version 21.04. From https://discourse.ubuntu.com/t/needrestart-changes-in-ubuntu-24-04-service-restarts: ------------------------------------------------------------------------ What is needrestart, exactly? needrestart is a tool that probes your system to see if either the system itself or some of its services should be restarted. That last part is the one of interest in this document. Notably, a service is considered as needing to be restarted if one of its processes is using a shared library whose initial file isn't on the system anymore (for instance, if it has been overwritten by a new version as part of a package update). We ship this tool in our server images, and it is configured by default to run at the end of APT transactions, e.g. when doing apt install/upgrade/remove or during unattended-upgrades. ------------------------------------------------------------------------ We discovered three fundamental vulnerabilities in needrestart (three LPEs, Local Privilege Escalations, from any unprivileged user to full root), which are exploitable without user interaction on Ubuntu Server (through unattended-upgrades): - CVE-2024-48990: local attackers can execute arbitrary code as root by tricking needrestart into running the Python interpreter with an attacker-controlled PYTHONPATH environment variable. Last-minute update: an additional CVE, CVE-2024-48992, has been assigned to needrestart because local attackers can also execute arbitrary code as root by tricking needrestart into running the Ruby interpreter with an attacker-controlled RUBYLIB environment variable. - CVE-2024-48991: local attackers can execute arbitrary code as root by winning a race condition and tricking needrestart into running their own, fake Python interpreter (instead of the system's real Python interpreter). - CVE-2024-10224: local attackers can execute arbitrary shell commands as root by tricking needrestart into open()ing a filename of the form "commands|" (technically, this vulnerability is in Perl's ScanDeps module, but it is unclear whether this module was ever meant to operate on attacker-controlled files or not). Last-minute update: in the end, an additional CVE, CVE-2024-11003, has been assigned to needrestart for calling Perl's ScanDeps module with attacker-controlled files. To the best of our knowledge, these vulnerabilities have existed since the introduction of interpreter support in needrestart 0.8 (April 2014). From https://github.com/liske/needrestart#interpreters: ------------------------------------------------------------------------ needrestart 0.8 brings an interpreter scanning feature. Interpreters not only map binary (shared) objects but also use plaintext source files. The interpreter detection tries to check for outdated source files since they may contain security issues, too. This is only a heuristic and might fail to detect all relevant source files. The following interpreter scanners are shipped: - NeedRestart::Interp::Java - NeedRestart::Interp::Perl - NeedRestart::Interp::Python - NeedRestart::Interp::Ruby ------------------------------------------------------------------------ We will not publish our exploits for now; however, please note that these vulnerabilities are trivially exploitable, and other researchers might publish working exploits shortly after this coordinated release. ======================================================================== Background ======================================================================== And now the questions: Do I kill them? Become their friend? Do I eat them? -- Pearl Jam, "Bugs" While idly watching an "apt-get upgrade" of one of our Ubuntu Servers, we noticed a message that we had never noticed before: "Scanning processes..." We immediately wondered: What is printing this message? Is it scanning userland processes? As root? Even processes that do not belong to root? We quickly found out that this message is printed by needrestart, a tool that scans the userland for processes that need to be restarted after a package installation, upgrade, or removal. Naturally, needrestart scans all userland processes as root, including unprivileged user processes; i.e., possibly attacker-controlled processes. ======================================================================== CVE-2024-48990 (and CVE-2024-48992) ======================================================================== To determine whether a Python process (a process that is running the Python interpreter) needs to be restarted, needrestart extracts the PYTHONPATH environment variable from this process's /proc/pid/environ (at line 193), sets this environment variable if it exists (at line 196), and executes Python ("$ptable->{exec}" at line 203) with a "-" argument to read a short, hard-coded script from stdin (at line 204): ------------------------------------------------------------------------ 135 sub files { 136 my $self = shift; 137 my $pid = shift; 138 my $cache = shift; 139 my $ptable = nr_ptable_pid($pid); ... 193 my %e = nr_parse_env($pid); 194 local %ENV; 195 if(exists($e{PYTHONPATH})) { 196 $ENV{PYTHONPATH} = $e{PYTHONPATH}; 197 } ... 203 my ($pyread, $pywrite) = nr_fork_pipe2($self->{debug}, $ptable->{exec}, '-'); 204 print $pywrite "import sys\nprint(sys.path)\n"; 205 close($pywrite); ------------------------------------------------------------------------ Unfortunately, if a Python process belongs to a local attacker, then needrestart executes Python (at line 203) with an attacker-controlled PYTHONPATH environment variable, which allows the attacker to execute arbitrary code as root (even though needrestart's hard-coded Python script at line 204 is not attacker-controlled at all). This is CVE-2024-48990. For example, in our exploit we run a simple Python process (which sleep()s forever) with a "PYTHONPATH=/home/jane" environment variable, and plant a shared library "importlib/__init__.so" in our /home/jane. As soon as needrestart executes Python with our PYTHONPATH environment variable (at line 203), our shared library is executed (by Python's initialization code) and creates a SUID-root shell in /home/jane. Note: needrestart's support code for the Ruby interpreter seems equally vulnerable, but we have not investigated this any further, because (unlike Python) Ruby is not installed by default on Ubuntu Server. Last-minute update: we have now confirmed that needrestart's support code for the Ruby interpreter is indeed vulnerable and exploitable, through an attacker-controlled RUBYLIB environment variable and an "enc/encdb.so" shared library. This is CVE-2024-48992. ======================================================================== CVE-2024-48991 ======================================================================== To determine whether a process is indeed a Python process (a process that is running the Python interpreter, for example /usr/bin/python3), needrestart reads this process's /proc/pid/exe (at line 520), and then matches it against the regular expression at line 45: ------------------------------------------------------------------------ 520 my $exe = nr_readlink($pid); ... 606 $restart++ if(needrestart_interp_check($nrconf{verbosity} > 1, $pid, $exe, $nrconf{blacklist_interp}, $opt_t)); ------------------------------------------------------------------------ 166 sub needrestart_interp_check($$$$$) { 167 my $debug = shift; 168 my $pid = shift; 169 my $bin = shift; 170 my $blacklist = shift; 171 my $tolerance = shift; ... 176 if($interp->isa($pid, $bin)) { ------------------------------------------------------------------------ 40 sub isa { 41 my $self = shift; 42 my $pid = shift; 43 my $bin = shift; 44 45 return 1 if($bin =~ m@^/usr/(local/)?bin/python([23][.\d]*)?$@); 46 47 return 0; 48 } ------------------------------------------------------------------------ In fact, this code used to be vulnerable to CVE-2022-30688, a Local Privilege Escalation reported by Jakub Wilk: the regular expression at line 45 used to be unanchored (i.e., "/usr/(local/)?bin/python" instead of "^/usr/(local/)?bin/python([23][.\d]*)?$"), so local attackers could simply run their own, fake "/home/jane/usr/bin/python" (for example) and needrestart would later execute this fake Python interpreter as root (as if it were the system's real Python interpreter, at line 203). We tried to bypass the fixed, anchored regular expression at line 45, but we failed. However, we eventually realized that the filename that is checked at line 45 is not necessarily the same filename that is executed at line 203: the filename that is checked is read from /proc/pid/exe in the middle of needrestart's main loop (at line 520), but the filename that is executed ("$ptable->{exec}" at line 203) was first read from /proc/pid/exe long before needrestart entered its main loop. In other words, needrestart is vulnerable to a TOCTOU race condition (time-of-check, time-of-use). For example, our exploit /home/jane/race waits for needrestart to read our /proc/pid/exe for the first time (we use inotify to reliably win this race), and then quickly execve()s the system's real Python interpreter with a script that simply sleep()s for some time. As a result, needrestart does its checks on the real Python interpreter, but executes our own /home/jane/race instead, as root. Note: needrestart's support code for the Ruby interpreter seems equally vulnerable, but we have not investigated this any further. ======================================================================== CVE-2024-10224 (and CVE-2024-11003) ======================================================================== After we had discovered CVE-2024-48990 and CVE-2024-48991 in needrestart's support code for the Python interpreter (and Ruby), we began to wonder whether the support code for the Perl interpreter might also be vulnerable to a Local Privilege Escalation. Unlike needrestart's support code for Python and Ruby, the support code for Perl does not execute the Perl interpreter itself: instead, it calls the scan_deps() function from Perl's ScanDeps module, which analyzes a Perl script by recursively reading its source files. We therefore grepped the ScanDeps module for one of the oldest pitfalls of the Perl programming language: the two-argument form of open(), which allows attackers to execute arbitrary shell commands if they control the name of the file to be open()ed (for example, "commands|"). For more information, please refer to rain.forest.puppy's 1999 Phrack article ("That pesky pipe" section) and the SEI CERT Perl Coding Standard: https://phrack.org/issues/55/7.html#article https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=88890543 Incredibly, we found a match, at line 871 in ScanDeps.pm: ------------------------------------------------------------------------ 868 sub scan_file{ 869 my $file = shift; 870 my %found; 871 open my $fh, $file or die "Cannot open $file: $!"; ------------------------------------------------------------------------ In our exploit, we simply run a Perl script named "/home/jane/perl|" (which sleep()s forever), and as soon as needrestart calls scan_deps() to analyze our script, "/home/jane/perl|" is open()ed (at line 871), but because this filename ends with a "|" it is treated as a shell command, and our own "/home/jane/perl" is executed instead, as root. Last-minute update: while reviewing needrestart's patches for these vulnerabilities, we have discovered that Perl's ScanDeps module is also trivially exploitable through various calls to eval() ("string" eval()s, https://perldoc.perl.org/functions/eval). Consequently and impressively, in response to our advisory: - all of ScanDeps's vulnerable calls to open() and eval() have been patched, thus fixing CVE-2024-10224; - needrestart's dependence on ScanDeps has been completely removed (it uses a simple regex-based approach now), thus fixing CVE-2024-11003. ======================================================================== Mitigation ======================================================================== As already recommended by needrestart's advisory for CVE-2022-30688 (from https://www.openwall.com/lists/oss-security/2022/05/17/9): ------------------------------------------------------------------------ Disabling the interpreter heuristic in needrestart's config prevents this attack: # Disable interpreter scanners. $nrconf{interpscan} = 0; ------------------------------------------------------------------------ ======================================================================== Acknowledgments ======================================================================== We thank needrestart's maintainer (Thomas Liske), Module::ScanDeps's maintainers (Roderich Schupp in particular), the Ubuntu Security Team (Mark Esler in particular), and distros@openwall (Salvatore Bonaccorso from the Debian Security Team in particular) for their outstanding work; it has been a real pleasure to collaborate on this coordinated release. We also thank Adam Boileau (@metlstorm) and Rodrigo Branco (@bsdaemon) for their very kind words about our work; they mean the world to us: https://risky.biz/RB755/ https://phrack.org/issues/71/2.html#article ======================================================================== Timeline ======================================================================== 2024-10-04: We sent our advisory and exploits to the Ubuntu Security Team, and asked them if they could help us to coordinate this disclosure with the upstream projects and distros@openwall; they gladly accepted. 2024-10-08: The Ubuntu Security Team sent our advisory and exploits to needrestart's maintainer; we then started a very constructive exchange of patches and patch reviews. 2024-10-18: The Ubuntu Security Team opened GHSA-g597-359q-v529, a private GitHub repository to collaborate on this disclosure with Module::ScanDeps's maintainers. 2024-11-11: The Ubuntu Security Team sent our advisory and all of needrestart's and Module::ScanDeps's patches to distros@openwall. 2024-11-19: Coordinated Release Date (16:00 UTC).