Back to Blog

When your security scanner gets infected

When your security scanner gets infected


Last week was a rough one for the npm and PyPI ecosystems. Between March 30-31, two separate threat actor groups ran simultaneous supply chain campaigns targeting some of the most widely used packages in JavaScript and Python development. The window was roughly 48 hours. The packages involved had hundreds of millions of combined weekly downloads. And one of the tools many teams rely on to catch exactly this kind of attack was itself a target.

The axios attack

UNC1069, a North Korea-nexus financially motivated threat actor that Google Threat Intelligence Group has been tracking since 2018, hijacked an axios maintainer account and pushed malicious versions 1.14.1 and 0.30.4 to npm on March 31. The account takeover was straightforward to spot in hindsight — the registered email on the compromised maintainer account changed from the legitimate address to ifstap@proton.me — but by the time anyone noticed, the packages were already live.

axios is the most popular JavaScript library for HTTP requests. Version 1.x alone gets over 100 million weekly downloads.

The attack didn't modify axios directly. Instead, both malicious versions introduced a new dependency called plain-crypto-js, designed to look like the legitimate and widely used crypto-js package. plain-crypto-js contained an obfuscated JavaScript dropper that Google's researchers have named SILKBELL. When npm installs a package, it automatically executes any script listed under postinstall in the package's package.json. SILKBELL abused this mechanism to run silently the moment a developer or CI system ran npm install.

SILKBELL's job was to figure out what OS it was running on and download the appropriate second-stage payload from sfrclak.com:8000. On macOS, it fetched a native Mach-O binary dropped to /Library/Caches/com.apple.act.mond. On Windows, it copied the legitimate powershell.exe to %PROGRAMDATA%\wt.exe to masquerade as Windows Terminal, then downloaded and executed a PowerShell script, establishing persistence via a registry Run key named MicrosoftUpdate. On Linux, it dropped a Python script to /tmp/ld.py. After delivering its payload, SILKBELL deleted itself and replaced the injected package.json with a clean copy it had stored as package.md to cover its tracks.

All three platform variants deployed what GTIG calls WAVESHAPER.V2 — a backdoor that beacons to the C2 server every 60 seconds over port 8000 using base64-encoded JSON, with a hard-coded User-Agent of mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0). Once established, it supports remote command execution, in-memory PE injection, file system enumeration, and arbitrary script execution. It's a full RAT, not just a credential stealer.

The malicious versions were live from 00:21 UTC until approximately 03:20 UTC — just under three hours — before npm pulled them

The PyPI campaign

Running concurrently, a separate group tracked as TeamPCP (also known as UNC6780) was hitting PyPI. Their targets included LiteLLM, the Telnyx Python SDK, and Trivy.

The Telnyx attack is technically interesting in its own right. Rather than embedding malicious code directly into the package — which makes it easier to detect via static analysis — the compromised versions (4.87.1 and 4.87.2) downloaded their payload disguised as a .wav audio file, which was then decoded and executed on the target machine. This is a simple but effective evasion technique. Static scanners looking for obviously malicious Python code in the package won't find any; the actual malicious content never lands in the package repository at all.

But the most significant target in the TeamPCP campaign wasn't LiteLLM or Telnyx.

It was Trivy.

The problem with scanners

Trivy is an open source vulnerability and misconfiguration scanner maintained by Aqua Security. A lot of teams run it specifically to catch supply chain compromises — it scans container images, filesystems, and package dependency trees looking for known vulnerabilities and malicious packages. It's a reasonable thing to do.

TeamPCP compromised it anyway.

This is the fundamental problem with scanner-based security approaches: scanners are software, and software can be compromised. If you install a scanner from a public registry and run it in your CI/CD pipeline, that scanner is itself a dependency — and it has the same attack surface as any other package you install. When your security tool is fetched from the same ecosystem it's supposed to be protecting, a determined attacker can simply poison the tool.

This isn't a knock on Trivy specifically. The same logic applies to any scanner you pull from npm or PyPI and run with elevated access to your codebase and environment. The more trusted and widely used the tool, the more attractive a target it becomes.

How Hextrap approaches this differently

Hextrap doesn't pull external tools and run them against your dependencies. It operates as a proxy that sits in front of your package manager. When your build system or a developer runs npm install, pip install, go get, or any supported package manager command, that request goes through Hextrap before anything is downloaded to the local environment.

Because the enforcement layer is the proxy itself — not a scanner you install from a public registry — there's nothing in this attack surface for a threat actor to poison. Hextrap's protection is entirely independent of any package in npm, PyPI, or any other registry staying clean. It doesn't matter if Trivy, pip-audit, or any other scanning tool gets compromised.

The firewall still works.

But the architectural argument almost undersells the simpler point: soak time would have stopped both of these campaigns before they got anywhere near a developer machine.

Soak time as a passive defense

Hextrap's soak time feature lets you configure a minimum age for any package version before it's allowed through the proxy. If a version of a package was published less than X hours ago, the install is blocked — automatically, with no analysis required and no knowledge that an attack is in progress.

The malicious axios versions were live for just under three hours. The compromised Telnyx versions had a similar exposure window before PyPI intervened. Any organization with a 24-hour soak time policy in place would never have downloaded any of these versions. By the time the waiting period expired, npm and PyPI had already pulled the packages.

This is precisely why soak time works against campaigns like this. UNC1069 and TeamPCP both optimized for velocity — publish malicious packages, compromise as many systems as possible in the first few hours, and disappear before the security community catches up. Soak time directly counters that strategy. It doesn't require threat intelligence feeds, it doesn't require knowing an attack is happening, and it doesn't require your security tooling to remain uncompromised. It just slows down the last step in the kill chain long enough for the detection ecosystem to do its job.

Combined with an allowlist — which would have blocked plain-crypto-js entirely, since it was never an approved dependency — the attack chain breaks before a single payload executes.

What this week should change

Two separate nation-state and cybercrime campaigns, hitting both major JavaScript and Python ecosystems simultaneously, within a 48-hour window. Google Threat Intelligence noted that hundreds of thousands of stolen secrets may be circulating as a result of these combined campaigns — credentials that could fuel ransomware, SaaS compromises, and further supply chain attacks in the near term.

If your current approach to supply chain security involves scanning dependencies with tools you pull from the same registries you're trying to protect, this week is a good time to reconsider that architecture. The better defense is one that doesn't share an attack surface with what it's defending against.

Protect Your Supply Chain

Add a security firewall to your package manager and CI/CD pipelines.

Get Started Free