A PyPI typosquatting campaign post-mortem
tl;dr - An unsophisticated actor efficiently published about a thousand typosquatted packages of forty popular Python packages containing malicious code in a campaign that lasted two days, but actually only took about an hour to execute.
Phylum recently reported a massive typosquatting campaign against a number of popular Python packages in PyPI. Our analysis exposed the campaign while it was ongoing and unraveled the actor’s obfuscation to reveal malicious code that eventually delivered cryptocurrency wallet stealing malware. In the end, the actor successfully registered about 900 typosquatted packages over just a couple of days. The sheer volume of this campaign led us to take a retrospective look at all of our data to see what we might learn about the actor’s tactics. What we discovered was sobering.
First, the scope of the campaign was vast and varied. The 40 targeted packages began in the cryptocurrency domain, but quickly entailed web development, scientific computing, and general developer tools. Many of these packages are some of the most popular on PyPI, either measured by GitHub stars (scikit-learn
has 53K ⭐ - a quarter of the targets have over 10K ⭐) or by the number of dependent repositories (urllib3
has over 37K dependent repos — over half of the targets have over 1K dependent repos).
Second, the campaign was efficient. The actor successfully registered 899 out of 1042 possible typosquats of these popular packages (not including eight initial test packages and two apparent one-offs) for an overall efficiency rate of 86.3% - about six hits for every seven attempts.
Third, when the campaign was active, it was fast. Though the campaign was drawn out over a couple of days with many long gaps of inactivity, the total accumulated wall time for all the successfully registered typosquats during the automated part of the campaign was just over an hour — one typosquatted package every 4.5 seconds on average.
But ultimately, aside from the scope, the efficiency, and the velocity of this campaign, what is most remarkable is the vast disproportion between the actor’s effectiveness and his sophistication. This actor laid a dense minefield of typosquats around the most popular packages in PyPI in wave after wave of automated attack against Python developers in a short period of time while displaying only a rudimentary amount of programming skill.
Initial testing
The attacker began his testing with a set of eight packages that he published over about two hours.
2023-02-09 06:10:31 6524 homeworktest
2023-02-09 06:32:32 6526 homeworktestt
2023-02-09 06:33:22 6536 homeworktesttt
2023-02-09 06:53:28 6525 homeworkte
2023-02-09 07:10:55 6852 homeworktee
2023-02-09 07:16:17 6839 homeworkteee
2023-02-09 07:19:03 6841 homeworkteeee
2023-02-09 08:04:09 6838 homeworkwork
The time gaps suggest that the attacker was working out the kinks of his deployment automation, but the 300 byte differences between the packages caught our eye. Apparently, the attacker forgot to copy the entirety of his obfuscated malicious code in setup.py
.
目木水口水口刀木鸟口木目女鸟人山.write(''.join(map(getattr(__builtins__, oct.__
And that incomplete line is the last line. This malicious code would never have run. He eventually caught his mistake, and all subsequent packages in the campaign contained the functional malicious code in setup.py
so that as soon as the victim installed the package, the malware was delivered.
Typosquat testing
The attacker began about 15 minutes later to test his typosquats against the ccxt
package, a reasonable choice given the short name. The package description is “A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges", and this would be a target rich environment for his clipboard hijacking malware targeting cryptowallets. (Note: We suppress the package sizes for the sake of brevity; all of the packages in the campaign were about 6.8K bytes and roughly proportional to the length of the name of the package.)
2023-02-09 08:18:59 cccxt
2023-02-09 08:19:03 ccxxt
2023-02-09 08:19:05 ccxtt
2023-02-09 08:20:59 cxt
2023-02-09 08:21:04 ccx
2023-02-09 08:36:06 cxct
A moment’s reflection on this list lays bare his intent, and we intend to use this information to measure the attacker’s efficiency:
Our data gives the numerator. It only remains to figure out the denominator.
Insertion
2023-02-09 08:18:59 cccxt
2023-02-09 08:19:03 ccxxt
2023-02-09 08:19:05 ccxtt
First, he tried to register packages by inserting a single repeated character. It is easy to count the number of typosquats this affords: given a word of length n, there are at most n different ways to insert a repeated character.
However, the consecutive repeated characters cc
in the target package name reduce the number of unique typosquats, because repeating either the first or second c
yields the same typosquat cccxt
. So, if there are r instances of a consecutive repeated character in a package name, there are n-r many typosquats possible.
Deletion
2023-02-09 08:20:59 cxt
2023-02-09 08:21:04 ccx
Next, he tried the opposite approach — deleting a single character. There are at most n many ways to delete a character in a word with n characters, but again in the same manner as insertion, the presence of a repeated character reduces this to n-r. Note that the attacker was unsuccessful with cct
, because there is a package registered in PyPI with that name.
Transposition
2023-02-09 08:36:06 cxct
Finally, the attacker transposed a pair of consecutive characters. Given a word with n characters, there are n-1 pairs of consecutive characters, and hence at most n-1 transpositions.
However, the repeated characters reduce this number again. Swapping consecutive repeated characters in the original package name yields itself, which is of course already registered. So, for a package name of length n with r repetitions, there are n-r-1 possible transposition typosquats.
These three techniques together yield the following formula: for a package name of length n having r many repeated consecutive characters, there are
many typosquats using only these three techniques. In the case of ccxt, n=4 and r=1 and so we have 3(4-1)-1 = 9-1 = 8 typosquats available. The attacker registered six, and so his efficiency is 6/8 = 75%.
Note that all of this math is for our own computational purposes in order to measure the attacker’s efficiency; we are dubious that he had any of these combinatorics in mind.
Of course, these three strategies are not the only means of creating typosquats. They are, however, trivially easy to generate. In fact, anyone who has made it to about chapter 3 in their favorite Learning Python for Beginning Programmers book could achieve the level of sophistication that this attacker has demonstrated. (We leave this as a coding exercise for the reader — given a word, generate all of the unique typosquats with these three strategies.)
Good citizenship
As a brief aside, we note that the attacker was unsuccessful in his attempt to register the transposition cctx
for this reason:
The cctx
maintainers foresaw the potential damage of this typosquat and took the initiative to register a placeholder package. The torch
maintainers also deserve credit for registering the package pytorch
(not properly a typosquat, but we have more to say about this later). Similarly, Yandex responded earlier this month to malware and potential dependency confusion attacks by registering numerous packages of interest to them (over 2200 by last count in Phylum’s data).
There is a rabbit hole here that we will not descend but only mention in passing.
On the one hand, when package maintainers register the likely typosquats of their projects with placeholder packages, it does reduce the effectiveness of these typosquatting attacks which provides an obvious benefit to their users. Taking on this responsibility themselves seems preferable to pushing it off to the repository maintainers, or worse, some other third party.
On the other hand, empty placeholders could be construed as invalid packages in light of PEP 541. Also, even for these three simple typosquatting techniques (and there are others that the attacker could have chosen), the combinatorics quickly get unwieldy. A long package name is going to have a lot of potential typosquats. And finally, resolving conflicts between similarly named packages would be difficult, maybe impossible in some cases.
The first wave of automation
About a minute after registering the last typosquat of ccxt
, the first automated wave began. His first 9 targets (including ccxt
) were mostly cryptocurrency packages. Other targets included blockchain, finance, and web development packages which we list below in the order the attacker attempted registration.
Name | Description | Successful registrations | Possible registrations | Efficiency | Wall time elapsed |
---|---|---|---|---|---|
ccxt | A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges | 6 | 8 | 75.0% | 17m7s (full automation had not yet begun) |
freqtrade | Freqtrade - Crypto Trading Bot | 26 | 26 | 100.0% | 1m44s |
cryptofeed | Cryptocurrency Exchange Websocket Data Feed Handler | 26 | 26 | 100.0% | 2m1s |
cryptocompare | Wrapper for CryptoCompare.com | 38 | 38 | 100.0% | 2m38s |
bitcoinlib | Bitcoin and Other cryptocurrency Library | 26 | 29 | 89.7% | 2m3s |
vyper | Vyper: the Pythonic Programming Language for the EVM | 13 | 14 | 92.9% | 1m15s |
solana | Solana Python API | 16 | 17 | 94.1% | 1m6s |
yfinance | Download market data from Yahoo! Finance API | 19 | 23 | 82.6% | 1m55s |
websockets | An implementation of the WebSocket Protocol (RFC 6455 & 7692) | 25 | 29 | 86.2% | 3m24s |
TOTAL | 195 | 210 | 92.8% | 16m6s |
Out of 210 potential typosquats, the attacker successfully registered 195. That is, for every 14 shots he took, 13 hit — and it was all over in 40 minutes from start to finish.
2023-02-09 08:36:06 cxct
...
2023-02-09 09:16:50 webosckets
Subsequent waves
The next wave was less ambitious, and it began with a single attempt to register erquests
, a typosquat of the popular HTTP library requests
. It is not clear from this single attempt if he tried all the others and failed or if this was merely a single trial. Regardless, we chalked this up as a one-off.
By this time, the Phylum Research Team was already writing up our initial findings, and the attacker targeted four more packages in this next wave, branching out from web development to scientific computing.
Name | Description | Successful registrations | Possible registirations | Efficiency | Wall time elapsed |
---|---|---|---|---|---|
colorama | Cross-platform colored terminal text | 14 | 23 | 75.9% | 0m58s |
beautifulsoup4 | Screen-scraping library | 27 | 41 | 65.9% | 1m36s |
selenium | A browser automation framework and ecosystem | 17 | 23 | 73.9% | 1m29s |
matplotlib | Python plotting package | 22 | 29 | 75.9% | 1m35s |
TOTAL | 80 | 116 | 69.0% | 5m38s |
This wave underperformed the first wave and lasted about an hour and a half from start to finish. A closer look shows that the attacker was inactive during the hour between selenium
and matplotlib
. If, however, we take into consideration only the wall time that elapsed when he was active, the entire attack lasted less than six minutes.
2023-02-10 02:38:22 coloama
...
2023-02-10 02:39:21 coloorama
2023-02-10 02:42:26 beautiflsoup4
...
2023-02-10 02:44:03 beuatifulsoup4
2023-02-10 02:51:41 seleinum
...
2023-02-10 02:53:10 selnium
2023-02-10 03:57:42 atplotlib
...
2023-02-10 03:59:18 matploltib
Three and half hours later, a third wave with slightly better efficiency.
Name | Description | Successful registrations | Possible registrations | Efficiency | Wall time elapsed |
---|---|---|---|---|---|
scrapy | A high-level Web Crawling and Web Scraping framework | 10 | 17 | 58.8% | 0m42s |
pyinstaller | PyInstaller bundles a Python application and all its dependencies into a single package | 23 | 29 | 79.3% | 2m9s |
tensorflow | TensorFlow is an open source machine learning framework for everyone | 24 | 29 | 82.8% | 1m43s |
TOTAL | 57 | 75 | 76.0% | 4m34s |
The velocity of his success rate registering these packages is also significant. For example, in this wave he successfully registered 57 packages in about four and a half minutes of wall time, or one success every 4.8 seconds on average.
At this point, the attacker updated his tactics.
Upgrades
Crestfallen that his last waves did not enjoy the same success rate as his initial attack, the attacker made a slight change to his typosquatting strategy. Consider scikit-learn
, his first package name that contained a hyphen:
2023-02-10 10:13:01 sciki-learn
2023-02-10 10:13:04 scikkit-learn
2023-02-10 10:13:08 scikitt-learn
2023-02-10 10:13:13 scikit-leearn
2023-02-10 10:13:16 scikit-learnn
2023-02-10 10:13:19 scikit-laern
2023-02-10 10:13:23 scikit-lean
2023-02-10 10:13:26 scikit-learrn
2023-02-10 10:13:31 sciikit-learn
2023-02-10 10:13:34 scikit-lern
2023-02-10 10:13:37 scikit-larn
2023-02-10 10:13:40 sckiit-learn
2023-02-10 10:13:46 csikit-learn
2023-02-10 10:13:49 scikt-learn
2023-02-10 10:13:52 sikit-learn
2023-02-10 10:13:55 sciikt-learn
2023-02-10 10:13:58 scikit-leanr
2023-02-10 10:14:01 cikit-learn
2023-02-10 10:14:04 scikiit-learn
2023-02-10 10:14:07 scikit-earn
2023-02-10 10:14:11 sickit-learn
2023-02-10 10:14:16 scikit-leaarn
2023-02-10 10:14:21 scikit-lear
2023-02-10 10:14:25 sscikit-learn
2023-02-10 10:14:28 sciit-learn
2023-02-10 10:14:30 scikit-elarn
2023-02-10 10:14:34 scikit-llearn
2023-02-10 10:14:37 scikit-leran
2023-02-10 10:14:39 scikti-learn
2023-02-10 10:14:42 sccikit-learn
Observe that there are no double hyphens (such a package name does not conform to the normalization guidelines of PEP 503), nor are there any missing hyphens. Furthermore, out of these 30 typosquats, exactly 14 of the names start with scikit-
and exactly 16 end with -learn
. Since 14+16=30, we conclude that the attacker chose to ignore hyphens for insertion, deletion, and transposition in generating typosquats. (Supplemental exercise to the above — update your code to accommodate this change.)
This decision slightly changes our denominator calculations. If we rephrase our original statement by saying that n is the number of alphanumeric characters in a package name with r repeated characters, then there are now 3(n-r)-2 many possible typosquats.
With these tweaks to our computations, we continue to measure his efficiency. This wave began around 10:13 UTC on 10 Feb.
Name | Description | Successful registrations | Possible registrations | Efficiency | Wall time elapsed |
---|---|---|---|---|---|
scikit-learn | A set of python modules for machine learning and data mining | 30 | 31 | 96.8% | 1m40s |
pandas | Powerful data structures for data analysis, time series, and statistics | 8 | 17 | 47.1% | 0m39s |
pytorch | None - (The package named for PyTorch is “torch”) | 14 | 20 | 70.0% | 0m48s |
pygame | Python Game Development | 11 | 17 | 64.7% | 0m51s |
python-binance | Binance REST API python implementation | 37 | 37 | 100.0% | 2m44s |
aiohttp | Async http client/server framework (asyncio) | 11 | 17 | 64.7% | 0m45s |
TOTAL | 111 | 139 | 80.0% | 7m27s |
Better performance than the previous waves, but still below his first wave. We note in passing that the torch
maintainers registered the pytorch
package out of caution.
Rather than a typosquat, pytorch
is more properly called a combosquat, i.e., combining words to make a package name look like a plausible duplicate of the legitimate package torch
, but we will have to treat these in a future blog post.
Shortly after the attacker registered the last typosquat of aiohttp
, we published the Phylum Research Team’s initial findings back to the beginning of the campaign. We speculate that once the attacker became aware, he decided that it was time to go big, or go home.
The final assault
At around 01:20UTC on 11 Feb, he made his boldest push targeting 17 packages. Abandoning his initial cryptocurrency angle, he went hard after a wide variety of Python developers by targeting many popular packages in a variety of specialties. In less than an hour an half, he more than doubled the number of packages that he had attempted thus far.
Name | Description | Successful registrations | Possible registrations | Efficiency | Wall time elapsed |
---|---|---|---|---|---|
websocket-client | WebSocket client for Python with low level API options | 40 | 43 | 93.0% | 2m38s |
click | Composable command line interface toolkit | 8 | 14 | 57.1% | 0m33s |
pillow | Python Imaging Library (Fork) | 8 | 14 | 57.1% | 0m36s |
openpyxl | A Python library to read/write Excel 2010 xlsx/xlsm files | 17 | 23 | 73.9% | 0m52s |
xlsxwriter | A Python module for creating Excel XLSX files | 26 | 29 | 89.7% | 1m23s |
urllib3 | HTTP library with thread-safe connection pooling, file post, and more | 10 | 17 | 58.8% | 0m52s |
simplejson | Simple, fast, extensible JSON encoder/decoder for Python | 28 | 29 | 96.9% | 1m38s |
requests-toolbelt | A utility belt for advanced users of python-requests | 43 | 43 | 100.0% | 2m13s |
discord-webhook | execute discord webhooks | 37 | 37 | 100.0% | 2m42s |
discord-py | None | 24 | 25 | 96.0% | 2m38s |
psutil | Cross-platform lib for process and system monitoring in Python | 10 | 17 | 58.88% | 1m17s |
pysocks | A Python SOCKS client module | 19 | 20 | 95.0% | 1m11s |
progressbar2 | A Python Progressbar library to provide visual (yet text based) progress to long running operations | 31 | 32 | 96.9% | 1m36s |
prompt-toolkit | Library for building powerful interactive command lines in Python | 32 | 34 | 94.1% | 2m20s |
pycodestyle | Python style guide checker | 32 | 32 | 100.0% | 3m19s |
gitpython | GitPython is a python library used to interact with Git repositories | 25 | 26 | 96.2% | 1m14s |
beautifulsoup | Screen-scraping library | 37 | 38 | 97.4% | 3m59s |
TOTAL | 427 | 473 | 90.2% | 31m1s |
We note that the discord-py
package does not exist, but discord.py
does. It is possible that the normalization rules (again from PEP 503) may have led the attacker to this variant.
About seven hours later, he targeted tkcalendar
and went 29 for 29 registering typosquats. But, after that the game was up. We had alerted the PyPI maintainers who were rapidly taking down his packages. His campaign was over.
2023-02-11 09:26:14 tkcalndar
... 27 other typosquats ...
2023-02-11 09:28:51 tkcallendar
One day later, we did see the same obfuscated malicious code pop-up in a single one-off registration named python-coinbase
. Maybe this was an homage to his early forays in the cryptocurrency domain, but with a single data point, it is difficult to discern if this is the same attacker giving a half-hearted attempt to restart the campaign or if this is just a copycat.
Conclusions
Excluding the eight initial tests and the two one-offs, we can summarize the campaign in the following table:
Wave | Successful registrations | Possible registrations | Efficiency | Wall time elapsed (automated part only) |
---|---|---|---|---|
1 | 195 | 210 | 92.8% | 16m6s |
2 | 80 | 116 | 69.0% | 5m38s |
3 | 57 | 75 | 76.0% | 4m34s |
4 | 111 | 139 | 80.0% | 7m27s |
5 | 427 | 473 | 90.2% | 31m1s |
6 | 29 | 29 | 100.0% | 2m36s |
TOTAL | 899 | 1042 | 86.3% | 1h7m22s |
Phylum’s automation platform together with the PyPI security team made this a short-lived campaign, but attacks like this will continue and increase. (While we have been putting the finishing touches on this writeup, the Phylum Research Team published their findings of a combosquatting campaign in PyPI — over 1100 packages and counting.)
The risk/reward proposition for attackers is well worth the relatively minuscule time and effort, if they can land a whale with a fat cryptowallet. And the loss of a few bitcoin pales in comparison to the potential damage of the loss of a developer’s SSH keys in a large enterprise such as a corporation or government.
The Phylum Research Team remains dedicated to defending the developer from these kinds of attacks. Subscribe to our blog and our Discord channel to keep up with the latest.