Python Library Hijacking
Full command arsenal in the cheatsheetWhen a Python script does import module, the interpreter looks for that module by walking the sys.path list in order. The first directory that contains a module.py wins. If an attacker can place a file named after the module in a directory searched before the standard library, the script will import the attacker's module instead. That's a library hijack.
Why it's escalation. The abuse is valuable when the vulnerable script runs with privileges (via a sudo, a root cron, a SUID). The hijacked module is imported and its top-level code runs in that privileged context, even before the script uses anything from the module.
The two ways to win the search.
- Writable script directory.
sys.path[0]is the script's own directory. If we can write there, ashutil.pyof ours is imported before the standard library one. PYTHONPATH. This environment variable is prepended tosys.path. If asudolets you keep it (SETENVtag in sudoers), pointing it at a writable directory is enough:
# the privileged script does: from shutil import make_archive
cat > /tmp/shutil.py <<'EOF'
import os
os.system('chmod u+s /bin/bash')
EOF
sudo PYTHONPATH=/tmp /opt/scripts/admin_tasks.sh # /tmp/shutil.py is imported as root
bash -p # shell with euid 0Nuance. On import, Python runs the module's entire body first and only then resolves names (make_archive). Even if our shutil.py doesn't define that function and the script ends in error, the malicious code (the chmod) already ran. The later error is irrelevant to the goal.
Why secure_path doesn't save you. In the sudo case, secure_path rewrites the command PATH, not Python's module path; that's why PYTHONPATH slips through despite Defaults env_reset, as long as the rule carries SETENV.
How to detect it. On the sudo side, the signature is a sudo PYTHONPATH=/tmp ... or sudo PYTHONPATH=<path> <script> in /var/log/auth.log: passing PYTHONPATH through sudo is anomalous by itself. On the host, auditd/Sysmon for Linux see the privileged interpreter opening a .py from a user-writable path (/tmp/shutil.py, the script directory) instead of the standard library, followed by a chmod +s /bin/bash or an sh child of the script. At rest, a module named after a standard library (shutil.py, os.py, random.py) in a user-writable directory or next to a privileged script is the indicator.
Defence. Don't grant SETENV unless truly needed; in privileged scripts, set sys.path defensively or use absolute paths and interpreters with -E/-I that ignore the environment. Related to SUID abuse, PATH hijacking and to sanitizing the environment before elevating privileges.
Machines where it's used
- AdmirerHackTheBox
- PsychoDockerLabs
- Symfonos 3VulnHub