Created
March 31, 2026 10:53
-
-
Save sorrycc/c5f19426e7d5d559f055d9d0403e1366 to your computer and use it in GitHub Desktop.
Axios supply chain attack detector (axios@1.14.1 / axios@0.30.4) - Bun script to check for RAT artifacts, malicious dependencies, C2 connections
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bun | |
| /** | |
| * Axios Supply Chain Attack Detector | |
| * Detects compromise from axios@1.14.1 / axios@0.30.4 (2026-03-31) | |
| * | |
| * Malicious versions inject plain-crypto-js@4.2.1 which deploys a | |
| * cross-platform RAT (Remote Access Trojan) via postinstall hook. | |
| * | |
| * Usage: bun run check-axios-attack.ts [--scan-dir /path/to/projects] | |
| * | |
| * Reference: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan | |
| */ | |
| import { $ } from "bun"; | |
| import { existsSync, readFileSync, readdirSync, statSync } from "fs"; | |
| import { join, resolve } from "path"; | |
| import { homedir, platform, tmpdir } from "os"; | |
| // ── Constants ────────────────────────────────────────────────────── | |
| const MALICIOUS_AXIOS_VERSIONS = ["1.14.1", "0.30.4"]; | |
| const MALICIOUS_PACKAGE = "plain-crypto-js"; | |
| const MALICIOUS_PACKAGE_VERSION = "4.2.1"; | |
| const C2_DOMAIN = "sfrclak.com"; | |
| const C2_IP = "142.11.206.73"; | |
| const C2_PORT = 8000; | |
| const RAT_PATHS: Record<string, string[]> = { | |
| darwin: ["/Library/Caches/com.apple.act.mond"], | |
| win32: [join(process.env.PROGRAMDATA || "C:\\ProgramData", "wt.exe")], | |
| linux: [join(tmpdir(), "ld.py")], | |
| }; | |
| const LOCKFILES = [ | |
| "package-lock.json", | |
| "bun.lockb", | |
| "bun.lock", | |
| "yarn.lock", | |
| "pnpm-lock.yaml", | |
| ]; | |
| // ── Types ────────────────────────────────────────────────────────── | |
| type Severity = "CRITICAL" | "WARNING" | "INFO" | "CLEAN"; | |
| interface Finding { | |
| severity: Severity; | |
| check: string; | |
| detail: string; | |
| path?: string; | |
| } | |
| // ── Helpers ──────────────────────────────────────────────────────── | |
| const os = platform(); | |
| const findings: Finding[] = []; | |
| function addFinding( | |
| severity: Severity, | |
| check: string, | |
| detail: string, | |
| path?: string | |
| ) { | |
| findings.push({ severity, check, detail, path }); | |
| } | |
| function colorize(severity: Severity): string { | |
| switch (severity) { | |
| case "CRITICAL": | |
| return "\x1b[91m[CRITICAL]\x1b[0m"; | |
| case "WARNING": | |
| return "\x1b[93m[WARNING]\x1b[0m"; | |
| case "INFO": | |
| return "\x1b[96m[INFO]\x1b[0m"; | |
| case "CLEAN": | |
| return "\x1b[92m[CLEAN]\x1b[0m"; | |
| } | |
| } | |
| function printBanner() { | |
| console.log(` | |
| \x1b[1m======================================== | |
| Axios Supply Chain Attack Detector | |
| axios@1.14.1 / axios@0.30.4 | |
| 2026-03-31 | |
| ========================================\x1b[0m | |
| Platform: ${os} | |
| Home: ${homedir()} | |
| Time: ${new Date().toISOString()} | |
| `); | |
| } | |
| // ── Check 1: Lockfile scan ───────────────────────────────────────── | |
| function findProjectDirs(root: string, maxDepth = 3): string[] { | |
| const dirs: string[] = []; | |
| function walk(dir: string, depth: number) { | |
| if (depth > maxDepth) return; | |
| try { | |
| const hasPackageJson = existsSync(join(dir, "package.json")); | |
| if (hasPackageJson) dirs.push(dir); | |
| for (const entry of readdirSync(dir)) { | |
| if (entry === "node_modules" || entry === ".git" || entry.startsWith(".")) continue; | |
| const full = join(dir, entry); | |
| try { | |
| if (statSync(full).isDirectory()) walk(full, depth + 1); | |
| } catch {} | |
| } | |
| } catch {} | |
| } | |
| walk(root, 0); | |
| return dirs; | |
| } | |
| function checkLockfiles(projectDir: string) { | |
| for (const lockfile of LOCKFILES) { | |
| const lockPath = join(projectDir, lockfile); | |
| if (!existsSync(lockPath)) continue; | |
| // bun.lockb is binary, skip text search | |
| if (lockfile === "bun.lockb") { | |
| // Check node_modules instead for binary lockfiles | |
| continue; | |
| } | |
| try { | |
| const content = readFileSync(lockPath, "utf-8"); | |
| if (content.includes(MALICIOUS_PACKAGE)) { | |
| addFinding( | |
| "CRITICAL", | |
| "Lockfile contains malicious dependency", | |
| `Found "${MALICIOUS_PACKAGE}" in ${lockfile}. The malicious dependency was resolved during install.`, | |
| lockPath | |
| ); | |
| } | |
| for (const ver of MALICIOUS_AXIOS_VERSIONS) { | |
| // Match common lockfile patterns for version pinning | |
| const patterns = [ | |
| `"axios": "${ver}"`, | |
| `axios@${ver}`, | |
| `"version": "${ver}"`, | |
| `axios-${ver}.tgz`, | |
| `axios/-/axios-${ver}.tgz`, | |
| ]; | |
| for (const pat of patterns) { | |
| if (content.includes(pat)) { | |
| addFinding( | |
| "CRITICAL", | |
| "Lockfile pins malicious axios version", | |
| `Found axios@${ver} reference ("${pat}") in ${lockfile}.`, | |
| lockPath | |
| ); | |
| break; | |
| } | |
| } | |
| } | |
| } catch {} | |
| } | |
| } | |
| // ── Check 2: node_modules scan ───────────────────────────────────── | |
| function checkNodeModules(projectDir: string) { | |
| const maliciousDir = join(projectDir, "node_modules", MALICIOUS_PACKAGE); | |
| if (existsSync(maliciousDir)) { | |
| addFinding( | |
| "CRITICAL", | |
| "Malicious package directory exists", | |
| `"node_modules/${MALICIOUS_PACKAGE}" exists. Even if contents look clean, the dropper may have already executed and replaced its own package.json to cover tracks.`, | |
| maliciousDir | |
| ); | |
| } | |
| // Check installed axios version | |
| const axiosPkgPath = join(projectDir, "node_modules", "axios", "package.json"); | |
| if (existsSync(axiosPkgPath)) { | |
| try { | |
| const pkg = JSON.parse(readFileSync(axiosPkgPath, "utf-8")); | |
| if (MALICIOUS_AXIOS_VERSIONS.includes(pkg.version)) { | |
| addFinding( | |
| "CRITICAL", | |
| "Malicious axios version installed", | |
| `axios@${pkg.version} is currently installed in node_modules.`, | |
| axiosPkgPath | |
| ); | |
| } | |
| } catch {} | |
| } | |
| // Check if package.json has unpinned axios (warning) | |
| const pkgPath = join(projectDir, "package.json"); | |
| if (existsSync(pkgPath)) { | |
| try { | |
| const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); | |
| const allDeps = { | |
| ...pkg.dependencies, | |
| ...pkg.devDependencies, | |
| }; | |
| if (allDeps.axios) { | |
| const ver: string = allDeps.axios; | |
| if (ver.startsWith("^") || ver.startsWith("~") || ver === "*" || ver === "latest") { | |
| addFinding( | |
| "WARNING", | |
| "Unpinned axios dependency", | |
| `axios version "${ver}" is not pinned. Future installs may resolve to a malicious version if another attack occurs.`, | |
| pkgPath | |
| ); | |
| } | |
| } | |
| } catch {} | |
| } | |
| } | |
| // ── Check 3: RAT file artifacts ──────────────────────────────────── | |
| function checkRATFiles() { | |
| const paths = RAT_PATHS[os] || []; | |
| for (const p of paths) { | |
| if (existsSync(p)) { | |
| addFinding( | |
| "CRITICAL", | |
| "RAT binary/script found on disk", | |
| `The Remote Access Trojan artifact exists at "${p}". Your system is compromised.`, | |
| p | |
| ); | |
| } else { | |
| addFinding("CLEAN", "RAT artifact not found", `"${p}" does not exist.`, p); | |
| } | |
| } | |
| } | |
| // ── Check 4: Active processes ────────────────────────────────────── | |
| async function checkProcesses() { | |
| const processPatterns: Record<string, string[]> = { | |
| darwin: ["com.apple.act.mond"], | |
| linux: ["ld.py"], | |
| win32: ["wt.exe"], | |
| }; | |
| const patterns = processPatterns[os] || []; | |
| if (patterns.length === 0) return; | |
| try { | |
| let output: string; | |
| if (os === "win32") { | |
| output = (await $`tasklist`.text()).trim(); | |
| } else { | |
| output = (await $`ps aux`.text()).trim(); | |
| } | |
| for (const pattern of patterns) { | |
| const lines = output | |
| .split("\n") | |
| .filter((l) => l.includes(pattern) && !l.includes("grep") && !l.includes("check-axios")); | |
| if (lines.length > 0) { | |
| addFinding( | |
| "CRITICAL", | |
| "Suspicious process running", | |
| `Process matching "${pattern}" found:\n${lines.join("\n")}`, | |
| ); | |
| } else { | |
| addFinding( | |
| "CLEAN", | |
| "No suspicious process", | |
| `No running process matches "${pattern}".` | |
| ); | |
| } | |
| } | |
| } catch {} | |
| } | |
| // ── Check 5: Network connections to C2 ───────────────────────────── | |
| async function checkNetwork() { | |
| try { | |
| let output: string; | |
| if (os === "win32") { | |
| output = (await $`netstat -an`.text()).trim(); | |
| } else { | |
| output = (await $`netstat -an`.nothrow().text()).trim(); | |
| } | |
| const c2Lines = output | |
| .split("\n") | |
| .filter((l) => l.includes(C2_IP) || l.includes(`:${C2_PORT}`)); | |
| if (c2Lines.length > 0) { | |
| addFinding( | |
| "CRITICAL", | |
| "Active C2 connection detected", | |
| `Network connection to C2 (${C2_IP}:${C2_PORT}) found:\n${c2Lines.join("\n")}`, | |
| ); | |
| } else { | |
| addFinding( | |
| "CLEAN", | |
| "No C2 network connection", | |
| `No active connection to ${C2_IP}:${C2_PORT} detected.` | |
| ); | |
| } | |
| } catch {} | |
| // DNS check via /etc/hosts or resolver cache | |
| try { | |
| if (os !== "win32") { | |
| const hosts = readFileSync("/etc/hosts", "utf-8"); | |
| if (hosts.includes(C2_DOMAIN)) { | |
| addFinding( | |
| "INFO", | |
| "C2 domain in /etc/hosts", | |
| `"${C2_DOMAIN}" found in /etc/hosts (may be a block entry, which is good).` | |
| ); | |
| } | |
| } | |
| } catch {} | |
| } | |
| // ── Main ─────────────────────────────────────────────────────────── | |
| async function main() { | |
| printBanner(); | |
| // Parse args | |
| const args = process.argv.slice(2); | |
| let scanDir = homedir(); | |
| const scanDirIdx = args.indexOf("--scan-dir"); | |
| if (scanDirIdx !== -1 && args[scanDirIdx + 1]) { | |
| scanDir = resolve(args[scanDirIdx + 1]); | |
| } | |
| // Phase 1: System-level checks | |
| console.log("\x1b[1m[Phase 1] System-level checks\x1b[0m\n"); | |
| console.log(" Checking RAT artifacts..."); | |
| checkRATFiles(); | |
| console.log(" Checking running processes..."); | |
| await checkProcesses(); | |
| console.log(" Checking network connections..."); | |
| await checkNetwork(); | |
| // Phase 2: Project scanning | |
| console.log(`\n\x1b[1m[Phase 2] Scanning projects in: ${scanDir}\x1b[0m\n`); | |
| const projects = findProjectDirs(scanDir); | |
| console.log(` Found ${projects.length} project(s) with package.json\n`); | |
| for (const dir of projects) { | |
| checkLockfiles(dir); | |
| checkNodeModules(dir); | |
| } | |
| // ── Report ───────────────────────────────────────────────────── | |
| console.log("\n\x1b[1m========== SCAN RESULTS ==========\x1b[0m\n"); | |
| const criticals = findings.filter((f) => f.severity === "CRITICAL"); | |
| const warnings = findings.filter((f) => f.severity === "WARNING"); | |
| const infos = findings.filter((f) => f.severity === "INFO"); | |
| const cleans = findings.filter((f) => f.severity === "CLEAN"); | |
| for (const f of [...criticals, ...warnings, ...infos, ...cleans]) { | |
| console.log(`${colorize(f.severity)} ${f.check}`); | |
| console.log(` ${f.detail}`); | |
| if (f.path) console.log(` Path: ${f.path}`); | |
| console.log(); | |
| } | |
| // Summary | |
| console.log("\x1b[1m========== SUMMARY ==========\x1b[0m\n"); | |
| console.log(` CRITICAL : ${criticals.length}`); | |
| console.log(` WARNING : ${warnings.length}`); | |
| console.log(` INFO : ${infos.length}`); | |
| console.log(` CLEAN : ${cleans.length}`); | |
| console.log(); | |
| if (criticals.length > 0) { | |
| console.log( | |
| "\x1b[91m\x1b[1mYOUR SYSTEM MAY BE COMPROMISED.\x1b[0m\n" | |
| ); | |
| console.log("Recommended actions:"); | |
| console.log(" 1. Disconnect from the network immediately"); | |
| console.log(" 2. Do NOT attempt to just 'clean up' the malware files"); | |
| console.log(" 3. Rotate ALL credentials: npm tokens, SSH keys, AWS keys, CI/CD secrets, DB passwords"); | |
| console.log(" 4. Rebuild from a known-clean state"); | |
| console.log(" 5. Audit git history for unauthorized commits"); | |
| console.log(" 6. Block C2: add '127.0.0.1 sfrclak.com' to /etc/hosts"); | |
| console.log(); | |
| process.exit(1); | |
| } else if (warnings.length > 0) { | |
| console.log( | |
| "\x1b[93m\x1b[1mNo active compromise detected, but some dependencies need attention.\x1b[0m\n" | |
| ); | |
| console.log("Recommended actions:"); | |
| console.log(" 1. Pin axios to an exact safe version (e.g. 1.14.0 or 0.30.3)"); | |
| console.log(" 2. Use 'npm ci' instead of 'npm install' in CI/CD"); | |
| console.log(" 3. Consider adding release-age constraints to your package manager config"); | |
| console.log(); | |
| process.exit(0); | |
| } else { | |
| console.log("\x1b[92m\x1b[1mNo signs of compromise detected.\x1b[0m\n"); | |
| process.exit(0); | |
| } | |
| } | |
| main().catch((err) => { | |
| console.error("Fatal error:", err); | |
| process.exit(2); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
在我的活动连接中出现了带8000的IPv6地址时会提示Active C2 connection detected,但是这个可能跟C2无关,不知道是啥问题🙂
是因为
l.includes(C2_IP) || l.includes(:${C2_PORT}));吗?误报了?