A PyPI typosquatting campaign post-mortem

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

bots1

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:

Screen Shot 2023-02-27 at 21.44.45

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

Screen Shot 2023-02-27 at 21.44.15

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

award

As a brief aside, we note that the attacker was unsuccessful in his attempt to register the transposition cctx for this reason:

cctx

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

first_wave

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

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.

pytorch

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

final_aasault

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

conclusion

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.

Phylum Research Team

Phylum Research Team

Hackers, Data Scientists, and Engineers responsible for the identification and takedown of software supply chain attackers.