28.03.2024 15:13

Hobby hunter notes: PyPI under attack

When I wrap up at CERT.at, where I mostly work on our notification system (if you’re a network operator in Austria and got a misassigned notification about some security issues – I might have been involved in that), I sometimes change my hat and explore other “cyber”-security areas, especially looking for malicious packages in PyPI, a standard Python package repository. The short summary is: there are a lot of them – but also, don’t panic.

It’s happening now

Let’s start with a rough analysis of a recent campaign that could be named “funcaptcha.” According to records I was able to access, it started the day before yesterday (26th March) with a package called “schubismomv3”, but a post on Twitter [0] suggests there might have already been more by the time this gained my attention.

As per my findings, the first version of the package was published around 18:00 on March 26th (all timestamps are UTC+1), starting without any active malicious content, but included hate speech and used the name of a well-known security researcher [0].

A sample from first version of “schubismomv3” package
A sample from first version of “schubismomv3” package

Over multiple iterations with an apparently “trial and error” approach (Have you heard about testing your software locally? Or a test environment? No? Sorry, I might be biased. I’m primarily a developer.) we ended up with version 1.10.0 published around 20:30. It used classic methods:

  • the setup.py script was configured with a custom installation command overriding the default
  • during the installation on Windows OS, a sub process was started with an encrypted script inside
  • the malicious script used the “Fernet” encryption library to avoid automated de-obfuscation.
Final version of “schubismomv3” – overriding the installation command
Final version of “schubismomv3” – overriding the installation command

The obfuscated code performed a  number of tasks typical for information stealers, such as exfiltrating cookies and passwords from web browsers, but also looked for browser extensions and applications related to cryptocurrencies, documents with names indicating that they contained secret information (Do you name your top-secret documents something like “seecret”?) and so on. All this information is then compressed and sent out to “funcaptcha[.]ru/delivery” (thus the name for the campaign). Afterwards, an interesting thing would happen: if the script detected an installation of Atomic Wallet [1], a cryptocurrency wallet app, it downloaded its own version, trying to replace the original. Finally, the next Python script was downloaded and put in the Windows start up directory.

A sample from de-obfuscated code attempting to replace the original app
A sample from de-obfuscated code attempting to replace the original app

I'll leave the deep analysis of these artifacts to others. Let's take a look at why this is a campaign, and not just a single malicious package, instead. As soon as “schubismomv3” was reported and removed (shortly after 21:00, according to the information I was able to gather), PyPI was flooded with similar packages, all displaying the same malicious activity.

Initially, the threat actor used not exactly marketing friendly names, such as “insanepackagev1434” or “insanepackage217234234242423442983”. But later on they began to attempt to “typo-squat” popular packages, by creating and uploading packages which closely – but not exactly – mirrored the names of popular ones.

A few examples were “reqzests”, “requetsa”, “py-cordd”, “py-coqrd”, “coloramza”, “corlorama”, “capmonstercloudclouidclient”, “piolow”, “bop-utils”, and many hundreds (!) more.

The campaign continued until the early hours of March 28th, when PyPI administrators took the decision to temporarily suspend registration of new users and projects [14]. By then, more than 500 packages had been created.

They are not alone…

Looking at the few months I spent looking at PyPI, I can confidently say that the “funcaptcha” campaign might have been an exception in terms of the number of malicious packages involed – but it was not the only one.

I observed a number of potentially malicious packages, with another recent case, “yocolor”, initially looking like a small thing on PyPI, but turning out to be a significantly bigger campaign targeting repositories on Github [2] [3].

A further number of suspicious packages didn't do anything harmful, but weren't what you wanted to get either – some were pentesting packages (they usually get removed very quickly), some were advertisements, some may be part of research efforts.

Example command extracted from a package that is not harmful on its own, but you probably didn't want to share all of it
Example command extracted from a package that is not harmful on its own, but you probably didn't want to share all of it

...but it’s also no reason to panic

Everything I have written about is disturbing, but comparatively simple to catch. The methods are so popular (and obvious) that it's somewhat confusing to me. The threat actors must be aware that the chances of successfully compromising an actual, real world systems are slim to negligible. The reasonably expectable return on investment is far outweighed by the effort the attackers have put into these campaigns.

The "funcaptcha" campaign is a good example - yes, their code contains functionality to exfiltrate data, as well as some more advanced techniques. But the initial infection vector – assuming that there aren't any further, undiscovered ones – exposes packages to quick detection and swift removal.

Overriding the default install command is one of the first things that is being checked when a package is examined, and an external connection during installation is a pretty suspicious activity (although often a legitimate behavior).

The attackers seemed to know all of this and didn't attempt to hide it – which is odd, unless the campaign was just a smokescreen. The first step was slightly modified in later packages, downloading the first malicious script from their domain instead of embedding it, and recording the name of the package.

Example description of packages released in later stages of the “funcaptcha” campaign – it was what you would see in PyPI. I hope you wouldn't try to install such a package.
Example description of packages released in later stages of the “funcaptcha” campaign – it was what you would see in PyPI. I hope you wouldn't try to install such a package.

PyPI Security Team

The big role in securing your development environment against such attacks is played by PyPI. After a few attempts, the index decided against proactively hunting for malware years ago, instead investing in improving the handling of abuse reports.

And they do it well. The team responds very quickly, sometimes taking down malicious packages in a matter of minutes. Last year, PyPI reached an important milestone by hiring its first official Safety & Security Engineer [4].

They are active and transparent about their work, conducting a security audit of the registry [5], explaining the abuse reporting process [6], and most recently improving the reporting channel, as well as launching a private beta of the reporting API [7].

This means that the PyPI, while under constant attack from threat actors, is leveraging the power of many researchers hunting for malicious packages. And it seems to work well, at least against threat actors using known methods.

What does all of this mean for me?

My personal opinion is that most of the cases we see in the security media and researchers' blog posts about malicious packages aren't the real threats we should be spending sleepless nights thinking about – we can leave that restlessness to advanced threats like backdoors in popular libraries, well hidden malicious actions which are only triggered under very specific conditions, and so on.

The typical threats relying on obvious methods are more like the flu: we cannot ignore them, but we should get used to them and, most importantly, take basic precautions.

These always depend on what you're trying to secure – don't forget to think about your threat modeling, even if it's basic!

There are a few tips, useful not only for Python environments:

  • Do. Not. Download. Random. Stuff. Really, that’s the most important thing. Malicious code is often hidden in low-quality packages, repositories in Github, and so on. Please pay attention to what you run on your computer.
  • Use reputable dependencies. But be careful: the information in package registries, such as connected repositories or maintenance names, is often just a declaration. Instead, use external reputation services. There are a few free ones you can check (for example [8] [9]), as well as services that offer only verified dependencies for download.
  • Keep your dependencies healthy. Scan them regularly for known vulnerabilities (including container images), and install security updates (not necessarily fully automated – that would open the door to other threats). You can use free or paid services, and your source hosting service probably already has something ready for painless integration. For example, you can check out osv.dev [10].
  • Install only what you need. You can think of your project's dependencies as an ingredient list: if the food item or beverage you're about to buy has a long list of ingredients you don't understand, you should probably think twice before eating it. Dependencies that you don't need, dependencies that have been used but are no longer used – all of those unnecessarily increase the risk of an incident.
  • Think about reducing the data that your development environment has access to. Thread actors use malicious packages and repositories to target data on developer machines. Solutions such as development containers [11] can reduce the potential scope of a breach.
  • Monitor test environments the same way as you would production. Advanced threats may not be easy to detect locally, but there is a chance that they will reveal their intentions in your test environments before they reach production. Monitoring outgoing connections can be helpful in catching them. Also: you probably also want to secure your test environments as production if they are accessed externally [12], and not leave your production data there, especially of former customers [13].

Stay safe

I started by explaining a case from the PyPI world, but that was just an example. Developing software means relying on external dependencies, and it's great that we share common parts, especially when implementing complex solutions (Don't implement your own cryptography. Just don't.). Like everything, it brings its own risks, and we just have to be aware of them. And take precautions. And do not download random stuff.


You can look for signs of “funcaptcha” by:

  • funcaptcha[.]ru
  • 0c1ddd33e630f4ac684880f0e673dfa84919272494c11da0f1ec05fb4f919ce8 – first of modified apps the script tried to inject
  • abe19b0964daf24cd82c6db59212fd7a61c4c8335dd4a32b8e55c7c05c17220d – second modified app


[0] https://x.com/_JohnHammond/status/1772704618574705057?s=20

[1] https://atomicwallet.io/

[2] https://www.bleepingcomputer.com/news/security/hackers-poison-source-code-from-largest-discord-bot-platform/

[3] https://medium.com/@demonia/discovering-malwares-in-public-github-repositories-3e080f030ecc

[4] https://blog.pypi.org/posts/2023-08-04-pypi-hires-safety-engineer/

[5] https://blog.pypi.org/posts/2023-11-14-1-pypi-completes-first-security-audit/

[6] https://blog.pypi.org/posts/2023-09-18-inbound-malware-reporting/

[7] https://blog.pypi.org/posts/2024-03-06-malware-reporting-evolved/

[8] https://deps.dev/

[9] https://securityscorecards.dev/

[10]  https://osv.dev/

[11] https://containers.dev/

[12] https://www.theverge.com/2024/1/26/24051708/microsoft-hack-russian-security-attack-senior-leadership-emails

[13] https://niebezpiecznik.pl/post/dcg-centrum-medyczne-pokazuje-jak-nie-informowac-o-kradziezy-danych-pacjentow/  (Polish – data of a medical clinic stolen from the test environment of a vendor they have not worked with for a few years)

[14] https://status.python.org/incidents/dc9zsqzrs0bv 

Written by: Kamil Mankowski