The serpent’s tongue: Luring the Python out of its den
This report from Cisco Talos details a growing threat landscape surrounding Python packages, focusing on supply chain attacks leveraging malicious packages installed through package managers like PyPI. The report highlights how adversaries, notably TeamPCP, exploit Python's popularity and the trust placed in its package ecosystem to compromise developer devices and infrastructure. Attackers can inject malicious code during package build or installation, leading to payload execution and potential data exfiltration. The analysis emphasizes the need for robust defenses, including dependency auditing, version pinning, and careful scrutiny of package installation processes, given the potential for rapid exploitation and long dwell times.
Python's popularity, driven by its readable syntax and extensive library ecosystem, has made it a prime target for threat actors seeking to compromise developer devices and infrastructure. Malicious packages and supply-chain attacks are increasingly common, exploiting the trust built into Python's packaging ecosystem to execute payloads at the moment of installation, without any direct interaction from the victim. This report from Cisco Talos examines the full lifecycle of a Python package, from hosting on repositories such as PyPI or custom web servers, through source and wheel distribution formats, to the final installation into virtual or system-wide Python environments. Each technique is assessed for persistence, supported build methods, and distribution compatibility.
We conclude with practical defensive measures, including dependency auditing tools, version pinning strategies, installation time controls, and general best practices for minimizing supply-chain risk. Due to the friendly nature of its syntax, extensive capabilities, and wide range of libraries, Python has a very vibrant community of modules that can be easily installed using various package indexes. Unfortunately, this convenience comes with an additional burden. Malicious packages and supply chain infection are also increasingly common, as threat actors attempt to utilize these modules to infect as many victim devices as possible, abusing the very trust that the community is built upon. GitHub’s 2025 security data highlights the accelerating threat to the software supply chain, noting a 69% year-over-year increase in published malware advisories. Notably for Python developers, 17% of all reviewed advisories in the GitHub Advisory Database are now related to the Pip ecosystem, reflecting a significant targeting of Python-based environments. The threat actor group TeamPCP has also utilized software supply chain attacks, including misuse of Python modules, to compromise Microsoft’s GitHub subsidiary and carry out 20 "waves" of supply chain attacks according to Wired.
Users often believe that for a malicious payload to be executed they need to directly interact with the infected piece of code (e.g., providing it with a sensitive input, executing its entry point, or importing it to a working project). In reality, Python packages can establish a foothold simply through installation. While analyzing these techniques in detail, we will take a deeper look at the background process of package installation for Python. This will help understand the threat landscape for Python packages, including legitimate components adversaries try to alter for their benefit.
Journey of a Python package The process of moving a Python package from a remote repository to a local machine involves three distinct layers. While these layers are interconnected, they provide a useful abstraction for understanding the installation process:
- Hosting layer: Defines the location where the package is published
- Distribution layer: Specifies the file formats supported by the package
- Installation layer: Dictates the method of deployment for the package
Hosting packages Python packages can be installed from various remote repositories.
PyPI (Python Package Index): PyPI is the official repository for Python packages. The native package manager, pip, uses PyPI by default. Package details are accessible via a JSON API at “https://pypi.org/pypi/<package-name>/json”. During installation, the PyPI frontend redirects users to “files.pythonhosted.org”, where the actual files are stored. Download URLs are derived from the distribution file name and its blake2b_256 hash. For example:
Version control systems (VCS): Projects hosted on platforms like GitHub or GitLab can be installed directly. This supports open source development through transparent issue tracking.
Custom web servers: Any web server with a suitable directory structure can serve as a repository. Packages must be hosted in folders using their normalized names, with all versions grouped together:
To use a custom repository, pip must be instructed to use a different index URL:
Alternatively, users can provide an extra index URL to search multiple repositories:
Configuration and environment variables: Index URLs can be specified in pip configuration files at three levels, global (system-wide), user (specific to a user), or site (specific to an environment). The PIP_CONFIG_FILE environment variable can also point to a custom configuration. Additionally, any pip command line argument can be converted into an environment variable using the PIP_<UPPER_LONG_NAME> format, such as PIP_FIND_LINKS.
Distributing Packages Independent of the hosting layer, Python packages are published in two primary formats:
- Source Distributions (sdist): These come in a packed .tar.gz format. They contain the full source code and build instructions, requiring the package to be built on the user's workstation before installation.
- Wheel Distributions: These come in a pre-built .whl format. They are ready to be deployed immediately, providing a faster and more robust installation process. Despite having .whl extension, these distributions use same format as the .zip files.
Source distributions rely on build instructions, typically written as either a “setup.py” or “pyproject.toml” file. While “pyproject.toml” is the modern, preferred format due to its transparency and support for various backends, “setup.py” is a standalone script that uses Setuptools. A critical security concern is that “setup.py” executes automatically during installation or download, allowing for the execution of arbitrary code.
Installing packages Distribution format and target environment are the main parameters of the installation layer. Distribution format determines if the build process will take place on the user computer. Depending on the target environment used, accessibility of the package will differ after installation.
Besides system-wide deployments, use of virtual environments are very common. Virtualization of Python environment isolates packages and their versions per deployment. It prevents version conflicts between different software, which are relying on the same set of dependencies with varying versions. Isolation occurs mainly on the package and binary level. Once activated, a virtual environment is treated as a separate Python site, sharing the same file system and network interfaces, but with its own binary and package library. Therefore, we cannot consider this type of virtualization containerized.
Use of virtual environments provides convenient management for package dependencies. Direct dependencies of a Python package is handled in various locations.
Within source distributions, direct dependencies can be found on:
- “setup.py”, the setup function contains an install_requiresparameter, where dependencies can be listed
- “pyproject.toml”, under “projects” section, dependenciesparameter lists required packages
- “requirements.txt” file contains a list of dependencies
- “Pipfile”, under packagessection
After package is built, dependencies are listed in the “METADATA” file under “.dist-info” folder within wheel distribution file.
Tools like poetry, uv, and hatch are replacing native solutions by providing end-to-end management for environments, packages, and projects. These tools extend “pyproject.toml” capabilities by adding tool-specific sections to handle complex build-time tasks.
Entry points for malicious payloads The delivery medium for a malicious payload often defines its victim base. While phishing campaigns might target broad demographics based on geography or language, package manager attacks specifically target individuals with software development skills. In a modern enterprise, these individuals often hold administrative rights across sensitive assets, including endpoints, source code repositories, Continuous Integration and Continuous Delivery (CI/CD) pipelines, and cloud infrastructure. This makes them high-interest targets for adversarial campaigns. Furthermore, the rise of AI-based tooling has expanded this target group, increasing the potential impact of a single breach.
In this landscape, Python offers a feature-rich and popular environment favored by both developers and adversaries. Despite malicious packages being identified and removed from public repositories within hours, this is still a valid opportunity window for adversaries. Payload execution can occur within minutes of installing the malicious package, and exfiltration can be achieved within an hour, depending on adversarial goals. Later, operationalization of stolen assets by different actors can occur within just a few days. Recent trends indicated dwell time of nine days once compromised. This might vary based-on detection and response capabilities of an organization. Therefore, having a structured understanding of malicious Python packages can help us estimate the impact of the infection and avoid being compromised all together.
With this in mind, let's delve deeper into Python packages and highlight native features abused by the adversaries within a structured format. We group adversarial techniques into two main categories (build hook abuses and package content abuses) and list some additional characteristics, that convey the potential impact that can be inflicted upon the victim system:
- OS: Operating System support for this technique
- Category: Category of the technique, based on the Python feature it abuses
- Persistence: Indicates if the payload can persist between executions
- Build: Shows the supported build methods for the technique
- Distributions: Indicates if technique requires package to be build on victim endpoint
Build hook abuses Command class utilization on setup files “setup.py” helps with building the source distribution into a package on clients. It can execute arbitrary code during build process and mainly uses setuptools library’s setup function, to run pre/post-installation actions. setup function uses command classes through distutils library to build the package. The initial payload is executed once during package installation. In the wild examples of install command class abuse were previously reported.
In Figure 2, the setup function uses a malicious mock-object called BeaconOnInstall to override installation behavior of the package.
After installing the Python package, the pyproject hooking script beacons third-party domains, as instructed with the BeaconOnInstall object.
Use of Path Configuration Files Path configuration files (.pth) are used to extend system path coverage for Python environments. They are expected to contain file paths in order to point out additional directories for runtime usage. Yet, they are capable of executing Python one-liners. Once a .pth file was added directly under package folders, such as “site-packages” or “dist-packages”, they are executed with every invocation of Python, therefore exhibiting a persistent behavior on the victim endpoint. This technique can be achieved regardless of the build method and distribution type. One of the high profile campaigns using this technique was initiated by the TeamPCP during supply chain compromise of the litellm package.
In order to leverage .pth files through “setup.py”, we can again use the help of distutils command classes. Our hidden payload located in a .md file, will be converted to a .pth file and will be added to the root of the package directory, once the build process is completed.
Alternatively, same can be achieved through “pyproject.toml” file. If the Hatchling backend is used, you can use the tool.hatch.build.targets.wheel.fo
