Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Message-ID: <20241119162429.GA12472@localhost.localdomain>
Date: Tue, 19 Nov 2024 16:25:15 +0000
From: Qualys Security Advisory <qsa@...lys.com>
To: "oss-security@...ts.openwall.com" <oss-security@...ts.openwall.com>
Subject: Local Privilege Escalations in needrestart


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@...sr/(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@...nwall (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@...nwall; 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@...nwall.

2024-11-19: Coordinated Release Date (16:00 UTC).

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.