Python Executable Hooks

Keys hanging from a lego key hook
Photo by Scott Webb / Unsplash
🗣️
This is part of a series of posts examining the methods malicious Python code gains execution.

Python allows for so-called customization modules to hook the system executable. These can be applied at either the user or system level and the code in these modules will run with every invocation of Python. This is possible thanks to the site module from the standard library. This module is automatically imported during initialization of the Python interpreter!

The module's documentation does show how the interpreter options -S and -s (or PYTHONNOUSERSITE environment variable) can be specified to disable this technique for the system and user levels, respectively. That is some comfort but how many developers do you know that use those options?

--cta--

This is the basic sequence for how to set the hook for the user-specific site-packages directory:

## Get the user `site-packages` directory and check if it is enabled
❯ python -c 'import site; print(site.getusersitepackages(), site.ENABLE_USER_SITE)'
/Users/maxrake/.local/lib/python3.12/site-packages True

## Ensure the directory structure exists
❯ mkdir -p /Users/maxrake/.local/lib/python3.12/site-packages

## Create the `usercustomize` module
❯ touch /Users/maxrake/.local/lib/python3.12/site-packages/usercustomize.py

## Put whatever you want there
❯ cat /Users/maxrake/.local/lib/python3.12/site-packages/usercustomize.py
print("[!] Malware could have run here")

## See that it runs whenever the Python interpreter is executed
❯ python
[!] Malware could have run here
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

## ...unless `PYTHONNOUSERSITE` is defined
❯ PYTHONNOUSERSITE=True python
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

## ...or the `-s` option is provided
❯ python -s
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

## ...or the `-S` option is provided (which prevents some other expected behavior)
❯ python -S
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
>>> quit()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'quit' is not defined
>>> import site
>>> site.main()
[!] Malware could have run here
>>> quit()

Adding malware to the usercustomize module of the user-specific site-packages directory

What happens if site.ENABLE_USER_SITE is not enabled, like when in this virtual environment?

(.venv) ❯ python -c 'import site; print(site.getusersitepackages(), site.ENABLE_USER_SITE)'
/Users/maxrake/.local/lib/python3.12/site-packages False

Hakuna matata (it means no worries). We can follow the same sequence but for the global site-packages directory:

## Get the global `site-packages` directories
(.venv) ❯ python -c 'import site; print(site.getsitepackages())'
['/Users/maxrake/dev/phylum/.venv/lib/python3.12/site-packages']

## Create the `sitecustomize` module(s)
(.venv) ❯ touch /Users/maxrake/dev/phylum/.venv/lib/python3.12/site-packages/sitecustomize.py

## Put whatever you want there
(.venv) ❯ cat /Users/maxrake/dev/phylum/.venv/lib/python3.12/site-packages/sitecustomize.py
print("[!] Malware could have run here")

## See that it runs whenever the Python interpreter is executed
(.venv) ❯ python
[!] Malware could have run here
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

## ...even when `PYTHONNOUSERSITE` is defined
(.venv) ❯ PYTHONNOUSERSITE=True python
[!] Malware could have run here
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

## ...or the `-s` option is provided
(.venv) ❯ python -s
[!] Malware could have run here
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()

## ...but NOT when the `-S` option is provided (which prevents some other expected behavior)
(.venv) ❯ python -S
Python 3.12.2 (main, Feb 14 2024, 10:56:22) [Clang 15.0.0 (clang-1500.1.0.2.5)] on darwin
>>> quit()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'quit' is not defined
>>> import site
>>> site.main()
[!] Malware could have run here
>>> quit()

Adding malware to the siteustomize module of a global site-packages directory

It might seem like the obvious choice is to hook the global site-packages directory (or directories) and not even worry about the user-specific site-packages directory. However, there could be permissions issues that limit the ability to set and/or execute the hook at the system level.

Most of the techniques covered so far in this series describe how to get malicious Python code to execute one time. This method is different in that it can serve as a way to gain execution multiple times: infect once, run everywhere. The threat actor's use case here is as a rudimentary backdoor and persistence mechanism. The malicious code would install the hook(s) once, using any of the other techniques already covered. The hook would then contain different malicious code that could check for new secrets or ephemeral environment variables to exfiltrate every time the python executable is run.

Charles Coggins

Charles Coggins

Senior Software Engineer, responsible for integrations and author of the "phylum" Python package. Documentation and quality champion, runner, baseball and scout dad, pod-faster, and lover of outdoors.