On the morning of May 10, 2023, Phylum’s automated risk detection platform flagged a series of publications surrounding the popular Flask package on PyPI. After reaching out to the author, we discovered that they were actually white hat publications intended for educational and demonstration purposes. However, this discovery serves as a crucial reminder that manual code review alone of seemingly innocuous packages is not sufficient to ensure security. Attackers can inject malware throughout the entire supply chain, including package dependencies.
A Few Test Publications
This began with the publication of the package
flaaks2. Over a span of approximately 15 minutes and the release of three different versions, we can observe the author's experimentation with utilizing Python's
cmdclass attribute in the setup file to execute two other functions in separate scripts.
Below is the
setup.py file from
flaaks version 0.2. While we won't delve into extensive details about this version since the author shifted tactics in version 0.3, it's important to note that the ultimate objective is to execute the
configure_package() function from
background_task and the
run() function from
Let's examine the scripts that the author is attempting to execute. It's worth noting that although both scripts belong to version 0.2, their content remains consistent throughout all three versions. Only the setup file and the top-level init file undergo changes as the author conducts their experiments. Below is the content of the
The script above facilitates remote command execution on a server by establishing a concealed terminal process and employing HTTP requests for server communication. It operates by monitoring incoming commands, executing them within the terminal process, and sending back the output to the server. Furthermore, it includes functionalities such as file downloading and uploading, directory manipulation, and the execution of scripts hosted on GitHub, although the provided URL presently directs to a placeholder. To handle multiple commands concurrently, the script utilizes threading and incorporates error handling mechanisms for common issues like connection timeouts and drops. Overall, its characteristics suggest that it serves as a basic Remote Access Trojan (RAT).
Now let's take a look at the
The script above executes another Python script as a background process on a Unix-like system. The function creates a subprocess using the
subprocess module, which runs the specified Python script using the
nohup command. By utilizing
nohup, the script continues running in the background even after the user logs out or closes the terminal. To ensure stealthiness, the script's output is redirected to
/dev/null, thereby preventing the victim from observing any output or becoming suspicious of any ongoing processes.
In version 0.3, the author removed the
cmdclass from the setup file and instead added the following to the top-level
__init__.py file, as shown below.
The script above registers a function named
atexit to execute automatically when the Python interpreter is closed. This function, in turn, calls two other functions,
run(), which are imported from the previously mentioned files. Since this code resides in the top-level
__init__.py file, the
_post_install() function is registered and executed only when the Python interpreter exits from a script that imports this package. However, it is worth noting that no other packages in PyPI are importing
flaaks2, indicating that this may have been a test or proof-of-concept.
Depending on a Malicious Package
Interestingly, in the timeline provided below, it can be observed that after the publication of the three versions of
flaaks2, two packages named
flaks-setup were released almost simultaneously. Over the next three hours, a total of 12 versions of
flaks and 16 versions of
flaks-setup were published. Let's now examine those packages.
For brevity, we will skip analyzing the version-to-version differences for each package and focus on the most recent versions. Provided below is the
setup.py file for
flaks version 1.2:
The first thing we notice above is that both
flaks_setup are required dependencies for the
flaks package. Therefore, installing
flaks will automatically attempt to install these dependencies. Additionally, the author of this package chose to utilize the
cmdclass, which triggers the execution of the
run() function within the
PostInstallCommand class after the installation of the
flaks package. In this case, the
run() function imports
flaks_setup and then executes its
post_install() function. This represents the entirety of the
flaks package, so now let's shift our focus to
To begin, let's examine the
setup.py file since the installation of
flaks will also trigger the installation of
flaks_setup. Here is the contents of the setup file:
There doesn't appear to be anything nefarious in this section. However, it's important to remember that once the installation is triggered by
flaks, the package is subsequently imported in the
PostInstallCommand so let's examine the
__init__.py file to see what happens there.
Well this looks familiar! It bears a striking resemblance to the
background_task.py file we encountered earlier in
flaaks2. However, there are a few distinctions between them. Here, we find two functions:
execute() function serves as a refactored version of
configure_package(), establishing a backdoor for remote control of a machine. It sets up a loop that listens for incoming commands from a remote server (
IP 126.96.36.199, located within the Digital Ocean address range in Germany). Communication with the server occurs through HTTP requests, where commands are received and executed, with the output returned. Available commands include changing the current directory, retrieving and uploading files to the server, and executing shell commands. If the command is
'exit', the loop is terminated, severing the connection with the server.
As a reminder, the
PostInstallCommand from the
flaks installation imports and executes the
post_install() command from this package. The
post_install() function forks the current process, creates a new session for the child process, and invokes the
execute() function. This ensures that the script continues to run even after the user logs out.
The initial attack vector here is typosquatting and once a victim accidentally performs a
pip install flaks (instead of
pip install flask) it triggers the following sequence of events:
install_requireskeyword within the
setup.pyinitiates the installation of the
- Following the installation of
setup.pyimports the newly installed
flaks_setuppackage and invokes the
post_install()command from it.
execute()within a child process, which patiently awaits commands from the remote server, establishing the rudimentary RAT.
Upon discovering these packages, we promptly reported them to PyPI, resulting in their swift removal. Furthermore, we contacted the package author, who diligently responded and acknowledged the publications and shared the following information with us:
I would like to clarify that I am working in the IT Security field and have been developing these packages for educational and demonstration purposes. My goal is to highlight the importance of carefully controlling which packages employees are permitted to install on their work laptops. By showcasing how seemingly innocuous package installations can potentially lead to a full compromise of a machine, I hope to raise awareness and promote better security practices.
Although the attack we discussed above turned out to be a false alarm, it emphasizes the challenges and significance of software supply-chain security. The complex nature of software dependencies and versioning provides attackers with numerous opportunities to slip in malicious code, whether as a direct or transitive dependency buried deep within the vast network. We've previously written about the intricate web of dependencies in a package, and if you've never considered the interconnectedness of open-source ecosystems, I highly recommend reading Hidden Dependencies Lurking in the Software Dependency Network.
The initial typosquatting attack vector discussed here highlights the obvious trigger for this particular attack chain - a simple typing mistake or fat finger error. However, a common question arises around strangely named malware publications: "Why would someone install a package named
onyxproxy? I would never accidentally type that!" In a fantastic blog post called Bad Beat Poetry authored by our own Charles Coggins, this very question is addressed.
|Package Name||Publication Time|