Hardening Against needrestart LPE and the /proc/environ Injection Pattern

Hardening Against needrestart LPE and the /proc/environ Injection Pattern

Problem

needrestart is a widely deployed utility on Debian and Ubuntu systems that checks whether running services need to be restarted after library or kernel updates. It is typically invoked automatically by apt and dpkg after package installations and runs as root. This elevated context is what makes the CVE-2024-48990 family dangerous.

The vulnerability class works as follows: needrestart reads /proc/PID/environ for running processes to determine which interpreter (Python, Perl, Ruby) each process is using and whether that interpreter has been updated. An unprivileged user who controls the environment of a long-running process can place attacker-controlled values in that process’s environment — specifically PYTHONPATH, PERLLIB, RUBYLIB, or the interpreter path itself — before a package installation triggers needrestart’s scan.

CVE-2024-48990 (PYTHONPATH injection): needrestart reads the PYTHONPATH environment variable from /proc/PID/environ and uses it when spawning a Python interpreter to check version information. An attacker who controls a running process’s environment can set PYTHONPATH to a directory containing a malicious os.py, which needrestart’s root-spawned Python then imports.

CVE-2024-48991 (interpreter path substitution): needrestart uses the interpreter path found in /proc/PID/cmdline or /proc/PID/environ to spawn a subprocess. A TOCTOU race condition allows an attacker to replace the interpreter at that path between needrestart’s check and its execution, substituting a malicious binary that runs as root.

CVE-2024-48992 (RUBYLIB injection): same class as CVE-2024-48990 but via the Ruby library path.

These vulnerabilities affect needrestart versions prior to 3.8. Ubuntu 22.04 LTS ships needrestart; Debian 12 ships needrestart. Any multi-user system where unprivileged users can run long-lived processes is affected.

The broader pattern. The needrestart CVEs are an instance of a broader class: root-running tools that read /proc/PID/environ to make decisions. Any tool in this category is susceptible to the same injection pattern. Auditing which root-running tools on your system read /proc/*/environ is the defensive generalisation of this specific fix.

Target systems: Debian and Ubuntu servers with needrestart installed (check: dpkg -l needrestart); any multi-user Linux system; systems where apt/dpkg post-install hooks run automatically.


Threat Model

Adversary 1 — Local user on a shared server. A developer or CI runner account on a shared build server has code execution as an unprivileged user. They set PYTHONPATH=/tmp/evil in the environment of a persistent process they control (e.g., a long-running Python web server they own). When an admin runs apt-get upgrade, needrestart scans processes, reads the malicious PYTHONPATH, and imports /tmp/evil/os.py as root.

Adversary 2 — Container with host process namespace. A container running with --pid=host can read /proc entries for host processes. If the container is also able to write to a shared filesystem path, it can influence environment variables of host processes visible via /proc, then wait for an admin package installation to trigger needrestart on the host.

Adversary 3 — Cronjob race condition. A cronjob runs as a user who has write access to a directory in PATH or PYTHONPATH for a root-owned process. The user places a malicious interpreter substitute in that directory and waits for needrestart to be triggered by an automated package update.


Configuration / Implementation

Step 1 — Check if you are affected

# Check if needrestart is installed
dpkg -l needrestart 2>/dev/null | grep "^ii"
# If output shows "ii needrestart", it is installed

# Check the installed version
needrestart --version 2>/dev/null || dpkg -l needrestart | awk '/^ii/{print $3}'
# Versions < 3.8 are affected by CVE-2024-48990 through CVE-2024-48992

# Check if needrestart runs automatically after apt
grep -r "needrestart" /etc/apt/apt.conf.d/ /usr/lib/dpkg/ 2>/dev/null | head -5
# If output appears, needrestart is hooked into apt post-install

# Check which Ubuntu/Debian version you're on
lsb_release -a 2>/dev/null
# Ubuntu 22.04 ships needrestart 3.5; Ubuntu 24.04 ships 3.6

Step 2 — Patch needrestart

# The fix is in needrestart >= 3.8
apt-get update && apt-get install --only-upgrade needrestart

# Verify patched version
needrestart --version
# Expected: needrestart 3.8 or later

# If 3.8 is not yet available in your distro's repository,
# check if a security update has been backported:
apt-cache policy needrestart
# Look for a version with security patch (e.g., 3.5ubuntu2.2 on Ubuntu 22.04)

Step 3 — Immediate workaround: disable interpreter scanning

If you cannot immediately update to a patched version, disable the interpreter scanning feature that reads /proc/PID/environ:

# /etc/needrestart/needrestart.conf
# Disable interpreter scanning entirely — eliminates the CVE attack surface
# while preserving service restart detection

# Comment out or set to false:
# $nrconf{interpscan} = 1;
$nrconf{interpscan} = 0;

# Alternatively, switch needrestart to non-interactive mode
# (prevents it from acting on scan results without manual review)
$nrconf{restart} = 'l';  # 'l' = list only; 'a' = automatic; 'i' = interactive
# Apply the configuration change
# Verify the setting takes effect by checking needrestart behaviour
needrestart -v -r l 2>&1 | grep -i "interp\|scan"
# Should show no interpreter scan activity

Step 4 — Run needrestart in non-root context where possible

For systems where interpreter scanning is needed, consider running needrestart with restricted capabilities:

# Check what capabilities needrestart actually needs
strace -e trace=openat needrestart 2>&1 | grep "/proc/" | head -20
# This shows what /proc paths it accesses — use to audit the scope

# needrestart does need root to restart services via systemd
# but interpreter scanning does not require root
# The patch in 3.8 sanitises environment variable reading

# For systems that cannot upgrade, use a wrapper to sanitise the environment
cat > /usr/local/sbin/needrestart-safe << 'EOF'
#!/bin/bash
# Sanitise environment before invoking needrestart
# Prevent PYTHONPATH, PERLLIB, RUBYLIB injection
exec env -i \
  PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  HOME=/root \
  LANG=C \
  /usr/sbin/needrestart "$@"
EOF
chmod 755 /usr/local/sbin/needrestart-safe

Step 5 — Audit other root tools that read /proc/PID/environ

The needrestart pattern is not unique. Audit which tools on your system read process environments as root:

#!/bin/bash
# scripts/audit-proc-environ-readers.sh
# Find processes and tools that read /proc/*/environ
# These are candidates for the same CVE class

echo "=== Tools that read /proc/PID/environ ==="

# Use auditd to capture opens of /proc/*/environ
auditctl -a always,exit -F arch=b64 -S openat -F path=/proc \
    -F key=proc_environ_read 2>/dev/null || \
    echo "Note: auditd not active — using strace approach"

# Alternatively, use inotifywait on /proc (limited — /proc is special)
# Best approach: check which SUID/root binaries call environ-related syscalls

# Find SUID binaries that might access /proc
find /usr /bin /sbin -perm /4000 2>/dev/null | while read -r binary; do
    if strings "$binary" 2>/dev/null | grep -q "environ\|PYTHON\|PERL\|RUBY"; then
        echo "  SUID binary with environ access: $binary"
    fi
done

# Check systemd services that access /proc
systemctl list-units --type=service --state=running 2>/dev/null | \
    awk '{print $1}' | grep "\.service$" | \
while read -r svc; do
    exec_path=$(systemctl show "$svc" -p ExecStart 2>/dev/null | \
        grep -oP '(?<=path=)[^;]+')
    if [[ -n "$exec_path" ]] && strings "$exec_path" 2>/dev/null | \
        grep -q "/proc.*environ"; then
        echo "  Service $svc binary ($exec_path) accesses /proc/*/environ"
    fi
done

echo ""
echo "=== Audit complete ==="
echo "For each tool found above:"
echo "  1. Check if it runs as root"
echo "  2. Check if it sanitises environment variables before using them"
echo "  3. If not, treat as a potential CVE-2024-48990 class vulnerability"

Step 6 — Detection: alert on suspicious interpreter spawning during apt

# /etc/audit/rules.d/needrestart-monitor.rules
# Detect potential exploitation attempts

# Alert on Python/Perl/Ruby processes spawned by root during package operations
-a always,exit -F arch=b64 -S execve -F euid=0 \
    -F key=root_interpreter_exec

# Alert on reads of /proc/*/environ by root processes
-a always,exit -F arch=b64 -S openat -F dir=/proc \
    -F uid=0 -F key=root_proc_environ_read
# Query recent audit events for exploitation indicators
ausearch -k root_interpreter_exec --start today 2>/dev/null | \
    grep -E "python|perl|ruby" | \
    grep -v "ARCH\|SYSCALL" | head -20

# Monitor for PYTHONPATH in root process environments
ausearch -k root_proc_environ_read --start today 2>/dev/null | \
    grep "environ" | head -10

Expected Behaviour

Scenario Unpatched needrestart Patched / mitigated
User sets PYTHONPATH=/tmp/evil in their process needrestart reads it; spawns root Python with malicious path Sanitised: needrestart 3.8 ignores untrusted environ values
interpscan = 0 in config N/A Interpreter scanning disabled; /proc/environ not read
apt-get upgrade runs needrestart Reads all running process environments as root Either skips interpreter scan or sanitises input
TOCTOU race on interpreter path Attacker can substitute interpreter between check and exec Race window closed in patched version
needrestart --version shows < 3.8 Vulnerable Update required

Trade-offs

Aspect Benefit Cost Mitigation
Disabling interpreter scan (interpscan=0) Eliminates CVE attack surface immediately Needrestart no longer detects Python/Perl services needing restart Acceptable on single-user servers; enable manual check needrestart -v after upgrades
Non-interactive mode (restart=l) No automatic restarts; eliminates exploitation trigger Requires manual service restart after package upgrades Build post-upgrade runbooks that include manual needrestart -v check
Upgrading to needrestart 3.8 Full fix; no functionality loss May not be available in all distro repos yet Check for backported security patch in distro security channel first
Environment sanitisation wrapper Works without upgrade Maintenance burden; may miss edge cases Use as temporary measure until package update is available

Failure Modes

Failure Symptom Detection Recovery
Patched needrestart not available in repo apt-get install --only-upgrade needrestart installs same version Version check shows < 3.8 after update Apply interpscan=0 workaround; check distro security advisory for backport timeline
interpscan=0 prevents detection of services needing restart Services not restarted after library update; stale binaries in memory Services run old library version; needrestart -v run manually shows pending restarts Run needrestart -v manually after major upgrades to check for pending restarts
Audit rules performance impact System I/O increases under heavy package operation auditd queue overflow warnings in logs Limit audit rules to specific event types; use rate limiting in audit.rules