Skip to content

Instantly share code, notes, and snippets.

@sorrycc
Created March 31, 2026 10:53
Show Gist options
  • Select an option

  • Save sorrycc/c5f19426e7d5d559f055d9d0403e1366 to your computer and use it in GitHub Desktop.

Select an option

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
#!/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);
});
@lepiai
Copy link
Copy Markdown

lepiai commented Apr 1, 2026

在我的活动连接中出现了带8000的IPv6地址时会提示Active C2 connection detected,但是这个可能跟C2无关,不知道是啥问题🙂
是因为l.includes(C2_IP) || l.includes(:${C2_PORT}));吗?误报了?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment