BLOG
Windows Infostealer Hits npm and Ruby
Shared IOCs confirm StubMaker campaign originally discovered in Ruby was pushed concurrently to npm
By cb482791-4ef1-4762-96ad-b0ca4bdd538e ·
Today, we came across independent research from OpenHack documenting a 37-package typosquatting campaign on npm, targeting chalk, commander, lodash, typescript, react, and axios. It described a 22 MB Rust loader carrying an embedded Go infostealer that decrypts entirely in memory with no second-stage download. That immediately caught our attention, because earlier this week we documented the RubyGems typosquatting campaign StubMaker, which has the same shape. That includes the specific file size.
StubMaker used a fake native-extension install hook to deliver a Windows loader, which decrypted an embedded Go infostealer entirely in memory. That campaign included 16 malicious package names across three RubyGems accounts, typosquatting bundler, i18n, rake, and activesupport, and ending in theft of browser credentials, cryptocurrency wallets, and Telegram data, exfiltrated via Gofile to a webhook at dresslee.com. One name, brumdler, was published under two separate owners. The operator got it taken down once and then successfully republished it under a second account.
See all the threat reports here.
We pulled the payloads for the npm packages and cross-checked against the Ruby samples. Both sets of packages share infrastructure that confirms this is the same threat actor.
Both installers beacon to the same C2 endpoint:
193.70.34.101:20099/voteBoth pull a Windows loader from the same GitHub release:
github.com/bebraz1/qPzM50V1AKG0rVlH/releases/download/null/main.exeThe loader and the embedded Go infostealer it decrypts are byte-for-byte identical between the two campaigns (matching SHA-256 hashes, not just similar behavior)
Identical hashes mean identical files. This was one threat actor running two typosquatting fronts against two package ecosystems, sharing a single payload and a single C2 backend. This post covers what’s different about the npm side, and what the timeline between the two campaigns actually tells us about how this operation moves.
TL;DR
Threat Type: Supply-chain downloader and Windows infostealer
Affected Ecosystems: RubyGems and npm
Campaign Scope: 16 package RubyGems packages and 37 npm packages on npm
Impact: Theft of credentials, authenticated browser sessions, payment-card data, cryptocurrency wallets and seed phrases, Telegram data, and host information
Key Finding: Both versions deliver the same Rust loader and the same Go infostealer (confirmed by matching SHA-256 hashes, not just similar behavior) through two independent typosquatting campaigns using different install hooks
Exfiltration: Stolen data is uploaded to Gofile; the resulting link is reported to an attacker webhook, confirmed on RubyGems as
dresslee.comover unencrypted HTTP
The 37 npm packages
Like the Ruby packages, the npm cohort are typosquats of very popular packages. Some are quite clumsy, but a few (chalk-core among them) read as more plausible sub-package names than the rest. The 37 packages went live in an eight-minute burst starting 02:48 UTC on August 16, and npm had pulled every one of them down by 04:12 UTC. Registry metadata recovered after takedown shows the 37 packages split across at five npm accounts, each registered with a distinct, generic-looking Gmail address:
verifikasikkiunila555averifikasi.kki.unila@gmail.comwhatisthisapplive20238261whatisthisapplive2023@gmail.comfullbasketpropertywebsiteb53afullbasketpropertywebsite@gmail.comnodemailert451e6nodemailert4@gmail.comantoniorodriguezmonte84ddf8antoniorodriguezmonte84@gmail.com
That’s a burner-account pattern consistent with what we saw across the RubyGems set.
Package
Typosquat
npm user
First Published (UTC)
Last Modified (UTC)
none (initial linked sample)
whatisthisapplive20238261
02:28
04:09
axios
nodemailert451e6
02:56
04:07
axios
nodemailert451e6
02:54
04:07
chalk
whatisthisapplive20238261
02:48
04:09
chalk
antoniorodriguezmonte84ddf8
02:48
04:12
chalk
verifikasikkiunila555a
02:48
04:09
chalk
verifikasikkiunila555a
02:48
04:09
commander
verifikasikkiunila555a
02:49
04:10
commander
antoniorodriguezmonte84ddf8
02:49
04:12
commander
fullbasketpropertywebsiteb53a
02:49
04:11
commander
verifikasikkiunila555a
02:49
04:10
commander
antoniorodriguezmonte84ddf8
02:49
04:12
commander
whatisthisapplive20238261
02:49
04:09
commander
nodemailert451e6
02:49
04:07
commander
nodemailert451e6
02:50
04:07
commander
antoniorodriguezmonte84ddf8
02:49
04:12
lodash
nodemailert451e6
02:51
04:07
lodash
verifikasikkiunila555a
02:50
04:10
lodash
whatisthisapplive20238261
02:51
04:09
lodash
fullbasketpropertywebsiteb53a
02:50
04:11
lodash
whatisthisapplive20238261
02:50
04:09
lodash
antoniorodriguezmonte84ddf8
02:51
04:12
lodash
whatisthisapplive20238261
02:51
04:09
typescript
verifikasikkiunila555a
02:52
04:10
typescript
verifikasikkiunila555a
02:53
04:10
typescript
fullbasketpropertywebsiteb53a
02:52
04:11
typescript
verifikasikkiunila555a
02:53
04:10
typescript
whatisthisapplive20238261
02:52
04:09
typescript
fullbasketpropertywebsiteb53a
02:52
04:11
typescript
verifikasikkiunila555a
02:52
04:10
typescript
fullbasketpropertywebsiteb53a
02:52
04:11
typescript
fullbasketpropertywebsiteb53a
02:53
04:11
typescript
nodemailert451e6
02:52
04:07
typescript
nodemailert451e6
02:52
04:07
typescript
whatisthisapplive20238261
02:52
04:09
typescript
fullbasketpropertywebsiteb53a
02:52
04:11
react
fullbasketpropertywebsiteb53a
02:53
04:11
How the npm packages differs
Here’s what actually changed between the two parts of the campaign:
The build-fakery is gone because npm doesn’t need it
StubMaker’s Ruby installer had to work inside extconf.rb, a file RubyGems expects to configure and build a native C extension. It fabricated a working-looking Makefile and make_stub/make_stub.bat scripts (why we called this campaign "StubMaker") to make a fake build look successful. npm has no equivalent expectation to fake. Instead it uses postinstall.js, a standard, documented lifecycle hook npm runs automatically, no pretense required. Moving to npm let the operator drop an entire subsystem of the Ruby version, because npm already hands you unrestricted code execution on install by design.
The obfuscation was re-implemented, not reused
The Ruby installer decoded its loader URL from Base64 whereas the npm installer uses repeated-key XOR with a hardcoded key, stf2026. Different technique, comparable simplicity. This reads as a deliberate rewrite for the new platform rather than a copy-paste of the Ruby approach, which argues for an operator (or toolkit) that treats the obfuscation layer as a per-ecosystem component to swap rather than a fixed piece of shared code.
npm ran wider and faster, from parallel accounts rather than one
RubyGems saw 16 malicious package names land across three accounts over roughly two days, including a clear catch-and-adapt cycle: the first two packages (brumdler, brundlef) were killed, and the operator came back under a new account with 15 more (one of them a successful republish of brumdler itself under the new owner).
npm saw 37 packages land in an eight-minute window across five accounts running in parallel, with no visible adaptation cycle and no second wave yet identified. That’s a structurally different approach, not just a bigger one: RubyGems’ pattern was sequential and single-point-of-failure. One account gets caught, the operator adapts and returns. npm’s pattern spread the same burst across multiple burner accounts simultaneously, so losing any one account wouldn’t have taken down the whole batch. However, in spite of the different approach, the npm packages were rapidly discovered and removed as a cohort.
Indicators of Compromise (IOCs)
The network indicators and payload hashes below are shared with the original StubMaker RubyGems campaign. They’re confirmed identical, not just similar.
Network Indicators
193.70.34.101
193.70.34.101:20099
http://193.70.34.101:20099/vote
github.com/bebraz1/qPzM50V1AKG0rVlH
https://github.com/bebraz1/qPzM50V1AKG0rVlH/releases/download/null/main.exe
https://api.ipify.org
https://upload.gofile.io/uploadfile
https://gofile.io/d/
dresslee.com
dresslee.com:20027
http://dresslee.com:20027/xf39jMJ9P1 Here's an example of how the threat actor "obfuscated" the IP address:

File Indicators
postinstall.js (npm install hook, the equivalent of extconf.rb on RubyGems)
main.exe
abe_payload.dll (RubyGems-confirmed only)
SHA-256 Hashes
# Downloaded Rust loader: main.exe
6f088ade49456db2422c3edfbb9998f4a3e9cce7c4c00a7279fb45d672a82b7d
# Recovered Go infostealer
1afff50ca4064310d3492c652e1c3168216dcb42063e0b26c223038db46b8731
# Recovered embedded ABE helper DLL (RubyGems-confirmed only)
67719fa6fcaa97936bf678565d6777db5c194e14efeba16864be8dac966e24bcWe don’t have a hash for postinstall.js itself. OpenHack’s write-up describes its behavior (XOR-decoding a config block with key stf2026) but doesn’t publish a hash for the file, unlike our own extconf.rb and Ruby installer hashes in the original StubMaker post.
What this means for defenders
If your organization runs both a Ruby and a Node stack, this operation had two independent paths into you, sharing nothing but infrastructure: the same beacon IP, the same GitHub-hosted loader, the same Go infostealer. A team watching Gemfile.lock and a team watching package-lock.json could each investigate “a typosquat campaign” in isolation and never realize it’s the same actor hitting them from two directions. Blocking 193.70.34.101:20099 and the dresslee.com webhook at the network layer covers both fronts regardless of which ecosystem delivered the initial install hook. That’s a more durable control than package-name blocklisting alone, which has to be rebuilt every time the operator picks new typosquat targets.