DEV Community

Alessio
Alessio

Posted on Originally published at py-forge.com

How to Audit Python Environments for Malicious .pth Files

Python startup mechanisms offer convenience for package setup, but they also present subtle opportunities for unwanted persistence. When Python boots, it inspects directory trees for path configuration files (.pth), as well as user and site customizer scripts. If an untrusted dependency or a compromised package drops files into these locations, code can execute every time the interpreter launches.

Auditing an environment requires inspecting these startup hooks safely. If you attempt to inspect a compromised environment by running Python scripts that import modules from it, you risk executing the very payloads you are attempting to analyze.

The PthGuard tool address this problem. It is a standalone, dependency-free scanner designed to perform static read and checksum verification against Python environments without importing or executing target files.


The Problem: Stealth Startup Hooks in Python

Python path configuration files typically contain directory paths added to sys.path. However, the Python interpreter supports a feature where lines beginning with import are evaluated directly during interpreter startup.

Attackers and compromised packages can exploit this behavior by embedding inline Python commands inside .pth files. For instance, a single .pth file can decode strings, spawn subprocesses, or establish network connections before your application code runs. Similar issues affect sitecustomize.py and usercustomize.py, which execute automatically whenever Python initialises if they reside within the module search path.

Additionally, standard package managers track installed files and checksums inside a .dist-info/RECORD file. Tampered packages might alter existing modules or add rogue unrecorded scripts directly to the package directory. Detecting these modifications requires verifying on-disk files against the signed or recorded standard hashes without running code from the packages themselves.


Install & Usage

The scanner relies exclusively on standard library modules (argparse, base64, csv, hashlib, json, os, pathlib, re, sys, and tempfile). It does not require any third-party libraries or installation frameworks.

Step 1: Save the File

Save the scanner code locally as pthguard_python_backdoor_scanner.py.

Step 2: Run the Scanner Under Safe Interpreter Flags

When running any security audit against an existing installation, you should isolate your running Python interpreter. Use the -I (isolated mode) and -S (skip site initialization) flags:

python -I -S pthguard_python_backdoor_scanner.py /path/to/target/site-packages
Enter fullscreen mode Exit fullscreen mode

The -I flag tells Python to ignore environment variables such as PYTHONPATH and prevent the user site-packages directory from being loaded into memory. The -S flag disables the automatic import of the site module, preventing the interpreter from parsing rogue .pth files in your own scanning environment.

You can inspect specific directories, isolated virtual environments, or individual candidate files:

python -I -S pthguard_python_backdoor_scanner.py ~/.local/lib/python3.11/site-packages
Enter fullscreen mode Exit fullscreen mode

How It Works

The scanner evaluates targets through purely static checks and checksum calculations, avoiding dynamic loading.

1. Pattern Matching on .pth Files

The scanner reads path files line by line using standard text parsing. It rejects execution and scans each line against common execution indicators:

PTH_DANGEROUS_PATTERNS = [
    (re.compile(r"^\s*import\s+[a-zA-Z0-9_]+", re.MULTILINE), "Direct code execution via 'import' statement"),
    (re.compile(r"exec\s*\(", re.IGNORECASE), "Executable 'exec()' call found"),
    (re.compile(r"eval\s*\(", re.IGNORECASE), "Dynamic 'eval()' call found"),
    (re.compile(r"base64\.(?:b64decode|decode)", re.IGNORECASE), "Base64 payload decoding detected"),
    (re.compile(r"subprocess\.", re.IGNORECASE), "Subprocess / command execution attempt"),
    (re.compile(r"os\.system\s*\(", re.IGNORECASE), "Direct OS shell execution"),
    (re.compile(r"__import__\s*\(", re.IGNORECASE), "Obfuscated __import__() call"),
    (re.compile(r"urllib\.request|urllib2|requests|socket", re.IGNORECASE), "Network / egress socket invocation"),
]
Enter fullscreen mode Exit fullscreen mode

Any .pth file containing executable expressions or raw import directives triggers a critical alert with the exact line number and character sample.

2. Checking Customizer Scripts

The scanner checks for sitecustomize.py and usercustomize.py files in the target path. These files are matched against expressions associated with credential access (such as references to environment variables or key files), base64 decoding routines, socket creation, and process execution.

3. RECORD Integrity and Tamper Checks

Python wheels follow the PEP 376 format, which stores relative file paths, cryptographic hashes, and byte counts inside dist-info/RECORD. The scanner reads these records via the standard csv reader and computes SHA-256 hashes matching the standard format:

def calculate_file_hash(self, filepath: pathlib.Path) -> Tuple[str, int]:
    """Calculates sha256 in url-safe base64 unpadded (PEP 376 RECORD standard) and byte size."""
    hasher = hashlib.sha256()
    size = 0
    try:
        with open(filepath, "rb") as f:
            while chunk := f.read(65536):
                hasher.update(chunk)
                size += len(chunk)
        b64_hash = base64.urlsafe_b64encode(hasher.digest()).rstrip(b"=").decode("ascii")
        return f"sha256={b64_hash}", size
    except (OSError, PermissionError) as e:
        return f"ERR:{e}", 0
Enter fullscreen mode Exit fullscreen mode

The script performs three checks:

  • Path Traversal Protection: Ensures that the relative paths inside RECORD do not resolve outside the target site-packages directory.
  • Hash and Size Validation: Confirms that on-disk files match the unpadded base64 SHA-256 hash listed in RECORD. Mismatches indicate altered code.
  • Unrecorded File Detection: Scans the .dist-info directory for unexpected files that are not referenced in the manifest.

4. Persistence File Locations

The scanner evaluates known operating system scratchpads and persistence paths (such as /tmp/.pth, /var/tmp/.pth, and autostart directory hooks) to determine if staging artifacts exist outside normal module trees.


Limitations

Understanding what static tools cannot do is essential for a thorough audit:

  1. Regular Expression Evasion: The scanner uses pattern matches to detect suspicious operations. Custom variable indirection, character concatenation, or atypical encodings that do not match the compiled expressions will not be flagged by the pattern rules alone.
  2. Untrusted RECORD Manifests: Hash validation verifies that files on disk match the RECORD file. If a completely malicious package was constructed from the start with matching hashes in its own RECORD, the hashes will validate cleanly. Hash checking only flags modifications made after initial package installation.
  3. No Dynamic Execution Analysis: The tool does not execute files in an instrumented sandbox. Code paths, dynamically constructed calls, and network communication that occur during regular program flow are outside the scope of static source inspection.
  4. Scope of Unrecorded Files: The tool checks for untracked files located directly inside the .dist-info directories. It does not perform a recursive inventory of every neighboring package subfolder to discover untracked helper scripts unless they are specifically mentioned in a RECORD manifest.

Frequently Asked Questions

Why run the scanner with python -I -S?

The -I and -S options isolate the interpreter. If the Python environment you are using to launch the scanner shares paths with an infected directory, running without these flags could cause Python to load malicious .pth or sitecustomize.py files before the scanner even starts.

Does the script automatically delete infected files?

No. The script explicitly avoids mutating or deleting files on disk. Instead, it aggregates findings into structured reports and produces quarantine instructions, leaving administrative review and remediation decisions entirely to the user.

Can I run the scanner offline?

Yes. The scanner operates completely offline. It relies exclusively on local file access, internal regular expressions, local checksum operations, and a bundled offline list of known bad releases. It does not query remote package indexes or network services.


Full version and the rest of the toolkit: PthGuard Python backdoor scanner on PyForge. Product page: https://py-forge.com/products/pthguard-python-backdoor-scanner

Top comments (0)