Created
April 2, 2026 16:33
-
-
Save sreejithbnaick/2f9457f46965d41167ce68cccd9f393d to your computer and use it in GitHub Desktop.
Check axios compromise on your system or local workspace folders
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 bash | |
| # ============================================================================= | |
| # check-axios-compromise.sh | |
| # Detects systems/projects affected by the axios npm supply chain attack | |
| # (axios@1.14.1 / axios@0.30.4 — published 2026-03-31, malicious versions | |
| # injecting plain-crypto-js@4.2.1 which drops a cross-platform RAT) | |
| # | |
| # Reference: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan | |
| # | |
| # Usage: | |
| # ./check-axios-compromise.sh # system-wide check only | |
| # ./check-axios-compromise.sh --workspace /path/to # system + all projects under path | |
| # ./check-axios-compromise.sh --help | |
| # ============================================================================= | |
| set -euo pipefail | |
| # --------------------------------------------------------------------------- | |
| # Colour helpers (disabled automatically when not a tty) | |
| # --------------------------------------------------------------------------- | |
| if [ -t 1 ]; then | |
| RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m' | |
| CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' | |
| else | |
| RED=''; YELLOW=''; GREEN=''; CYAN=''; BOLD=''; RESET='' | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Globals / constants | |
| # --------------------------------------------------------------------------- | |
| SCRIPT_VERSION="1.0.0" | |
| REPORT_FILE="/tmp/axios-compromise-report-$(date +%Y%m%d-%H%M%S).txt" | |
| MALICIOUS_AXIOS_VERSIONS=("1.14.1" "0.30.4") | |
| MALICIOUS_AXIOS_SHASUMS=( | |
| "2553649f2322049666871cea80a5d0d6adc700ca" # axios@1.14.1 | |
| "d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71" # axios@0.30.4 | |
| ) | |
| MALICIOUS_PLAIN_CRYPTO_SHASUM="07d889e2dadce6f3910dcbc253317d28ca61c766" | |
| C2_DOMAIN="sfrclak.com" | |
| C2_IP="142.11.206.73" | |
| C2_PORT="8000" | |
| C2_CAMPAIGN_ID="6202033" | |
| WORKSPACE_PATH="" | |
| FOUND_ISSUES=0 | |
| WARNINGS=0 | |
| CHECKS_RUN=0 | |
| # --------------------------------------------------------------------------- | |
| # Utility functions | |
| # --------------------------------------------------------------------------- | |
| log() { echo -e "${BOLD}[*]${RESET} $*" | tee -a "$REPORT_FILE"; } | |
| ok() { echo -e "${GREEN}[✔]${RESET} $*" | tee -a "$REPORT_FILE"; } | |
| warn() { echo -e "${YELLOW}[!]${RESET} $*" | tee -a "$REPORT_FILE"; ((WARNINGS++)) || true; } | |
| alert() { echo -e "${RED}[✘ COMPROMISED]${RESET} $*" | tee -a "$REPORT_FILE"; ((FOUND_ISSUES++)) || true; } | |
| info() { echo -e "${CYAN} →${RESET} $*" | tee -a "$REPORT_FILE"; } | |
| banner() { echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════════${RESET}" | tee -a "$REPORT_FILE" | |
| echo -e "${BOLD}${CYAN} $*${RESET}" | tee -a "$REPORT_FILE" | |
| echo -e "${BOLD}${CYAN}══════════════════════════════════════════════${RESET}" | tee -a "$REPORT_FILE"; } | |
| check_run() { ((CHECKS_RUN++)) || true; } | |
| usage() { | |
| cat <<EOF | |
| ${BOLD}check-axios-compromise.sh v${SCRIPT_VERSION}${RESET} | |
| Detects compromise from the axios npm supply chain attack | |
| (axios@1.14.1 / axios@0.30.4 injecting plain-crypto-js RAT dropper). | |
| ${BOLD}Usage:${RESET} | |
| $0 System-wide check only | |
| $0 --workspace <path> System check + scan all Node projects under <path> | |
| $0 --workspace <path> --depth N Max folder depth for project discovery (default: 5) | |
| $0 --help Show this help | |
| ${BOLD}Examples:${RESET} | |
| $0 | |
| $0 --workspace ~/projects | |
| $0 --workspace /srv/apps --depth 3 | |
| ${BOLD}Exit codes:${RESET} | |
| 0 — No indicators of compromise found | |
| 1 — One or more indicators of compromise detected | |
| 2 — Warnings / inconclusive findings | |
| EOF | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Argument parsing | |
| # --------------------------------------------------------------------------- | |
| FIND_DEPTH=5 | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --workspace|-w) WORKSPACE_PATH="${2:-}"; shift 2 ;; | |
| --depth|-d) FIND_DEPTH="${2:-5}"; shift 2 ;; | |
| --help|-h) usage; exit 0 ;; | |
| *) echo "Unknown argument: $1"; usage; exit 1 ;; | |
| esac | |
| done | |
| # --------------------------------------------------------------------------- | |
| # Print header | |
| # --------------------------------------------------------------------------- | |
| { | |
| echo "=====================================================" | |
| echo " axios npm Compromise Detection Script v${SCRIPT_VERSION}" | |
| echo " Reference: stepsecurity.io/blog/axios-compromised-on-npm..." | |
| echo " Run at: $(date)" | |
| echo " Host: $(hostname)" | |
| echo " OS: $(uname -s) $(uname -r)" | |
| echo "=====================================================" | |
| } | tee -a "$REPORT_FILE" | |
| echo "" | |
| # --------------------------------------------------------------------------- | |
| # Helper: check a single node_modules dir for plain-crypto-js | |
| # --------------------------------------------------------------------------- | |
| check_plain_crypto_in_dir() { | |
| local nm_dir="$1" # path to node_modules | |
| local label="$2" # human-readable label for display | |
| local pcp_dir="${nm_dir}/plain-crypto-js" | |
| check_run | |
| if [ -d "$pcp_dir" ]; then | |
| alert "plain-crypto-js found in ${label}" | |
| info "Path: ${pcp_dir}" | |
| info "This package is NOT a dependency of any legitimate axios version." | |
| info "Its presence means the RAT dropper has already executed." | |
| # The dropper replaces package.json with a clean stub reporting v4.2.0. | |
| # Check version field — if it says 4.2.0 the swap already happened. | |
| local pj="${pcp_dir}/package.json" | |
| if [ -f "$pj" ]; then | |
| local ver | |
| ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$pj" 2>/dev/null \ | |
| | grep -o '"[0-9][^"]*"' | tr -d '"' || true) | |
| if [ "$ver" = "4.2.0" ]; then | |
| alert "package.json inside plain-crypto-js reports v4.2.0 — this is the anti-forensics stub." | |
| info "The real malicious version was 4.2.1. The dropper already swapped package.json." | |
| fi | |
| # setup.js should be deleted by the dropper; if still present, dropper didn't finish | |
| if [ -f "${pcp_dir}/setup.js" ]; then | |
| alert "setup.js still present in ${pcp_dir} — dropper may not have completed!" | |
| info "This IS the malicious postinstall script. Do not run it." | |
| fi | |
| fi | |
| return 0 | |
| fi | |
| # Check npm list (may report 4.2.0 due to version spoofing, so grep both) | |
| if command -v npm &>/dev/null 2>&1; then | |
| local npm_out | |
| npm_out=$(cd "${nm_dir}/.." 2>/dev/null && npm list plain-crypto-js 2>/dev/null || true) | |
| if echo "$npm_out" | grep -qE "plain-crypto-js@(4\.2\.0|4\.2\.1)"; then | |
| alert "npm list reports plain-crypto-js in ${label} (version spoofing may show 4.2.0)" | |
| info "Output: $npm_out" | |
| fi | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Helper: check package-lock.json for compromised versions | |
| # --------------------------------------------------------------------------- | |
| check_lockfile() { | |
| local lockfile="$1" | |
| local label="$2" | |
| check_run | |
| if [ ! -f "$lockfile" ]; then return 0; fi | |
| local hit=0 | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| # Ensure "axios" and the version appear near each other (within 10 lines) | |
| # to avoid false positives from unrelated packages with the same version. | |
| if grep -A 10 '"axios"' "$lockfile" 2>/dev/null | grep -q "\"${v}\""; then | |
| alert "Compromised axios@${v} found in ${label}" | |
| info "File: ${lockfile}" | |
| hit=1 | |
| fi | |
| done | |
| if grep -q '"plain-crypto-js"' "$lockfile" 2>/dev/null; then | |
| alert "plain-crypto-js found in lockfile ${label}" | |
| info "File: ${lockfile}" | |
| hit=1 | |
| fi | |
| [ "$hit" -eq 0 ] && ok "Lockfile clean: ${label}" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Helper: check package.json for compromised axios version range | |
| # --------------------------------------------------------------------------- | |
| check_package_json() { | |
| local pj="$1" | |
| local label="$2" | |
| check_run | |
| if [ ! -f "$pj" ]; then return 0; fi | |
| local hit=0 | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| # Match "axios" and the version on the SAME line to avoid false positives | |
| # when another package coincidentally shares the same version string. | |
| if grep -qE "\"axios\"[[:space:]]*:[[:space:]]*\"[~^]?${v}\"" "$pj" 2>/dev/null; then | |
| alert "Compromised axios@${v} pinned in package.json — ${label}" | |
| info "File: ${pj}" | |
| hit=1 | |
| fi | |
| done | |
| [ "$hit" -eq 0 ] && ok "package.json axios version safe: ${label}" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Helper: verify installed axios shasum | |
| # --------------------------------------------------------------------------- | |
| check_axios_shasum_in_dir() { | |
| local nm_dir="$1" | |
| local label="$2" | |
| local axios_pj="${nm_dir}/axios/package.json" | |
| #log "2.4.1 Starting axios shasum check for ${axios_pj}" | |
| check_run | |
| [ -f "$axios_pj" ] || return 0 | |
| #log "2.4.2 Found axios package.json at ${axios_pj}" | |
| local installed_ver | |
| installed_ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$axios_pj" 2>/dev/null \ | |
| | grep -o '"[0-9][^"]*"' | tr -d '"' || true) | |
| # Only warn on malicious version numbers | |
| #log "2.4.3 Checking if installed axios version matches malicious release..." | |
| for i in "${!MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| if [ "$installed_ver" = "${MALICIOUS_AXIOS_VERSIONS[$i]}" ]; then | |
| alert "Installed axios version matches malicious release: ${installed_ver} — ${label}" | |
| info "Expected shasum: ${MALICIOUS_AXIOS_SHASUMS[$i]}" | |
| # If shasum command available, compute the tarball shasum | |
| local axios_dir="${nm_dir}/axios" | |
| if command -v sha1sum &>/dev/null || command -v shasum &>/dev/null; then | |
| local actual_shasum="" | |
| if command -v sha1sum &>/dev/null; then | |
| actual_shasum=$(find "$axios_dir" -type f | sort | xargs sha1sum 2>/dev/null | sha1sum | awk '{print $1}') | |
| else | |
| actual_shasum=$(find "$axios_dir" -type f | sort | xargs shasum -a 1 2>/dev/null | shasum -a 1 | awk '{print $1}') | |
| fi | |
| info "Computed directory shasum (may differ from npm tarball shasum): ${actual_shasum}" | |
| fi | |
| fi | |
| done | |
| } | |
| # ============================================================================= | |
| # SECTION 1 — SYSTEM-WIDE CHECKS | |
| # ============================================================================= | |
| banner "SECTION 1: System-Level Checks" | |
| # ------------------------------------------------------------------- | |
| # 1.1 — Global npm axios version | |
| # ------------------------------------------------------------------- | |
| log "1.1 Checking global npm axios version..." | |
| check_run | |
| if command -v npm &>/dev/null; then | |
| global_axios=$(npm list -g axios 2>/dev/null | grep axios || true) | |
| if [ -n "$global_axios" ]; then | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| if echo "$global_axios" | grep -q "$v"; then | |
| alert "GLOBAL npm axios@${v} installed! — $global_axios" | |
| fi | |
| done | |
| if ! echo "$global_axios" | grep -qE "1\.14\.1|0\.30\.4"; then | |
| ok "Global npm axios appears safe: $global_axios" | |
| fi | |
| else | |
| ok "axios not installed globally via npm" | |
| fi | |
| else | |
| warn "npm not found in PATH — skipping npm global check" | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.2 — Global npm plain-crypto-js | |
| # ------------------------------------------------------------------- | |
| log "1.2 Checking global npm for plain-crypto-js..." | |
| check_run | |
| if command -v npm &>/dev/null; then | |
| global_pcp=$(npm list -g plain-crypto-js 2>/dev/null || true) | |
| if echo "$global_pcp" | grep -q "plain-crypto-js"; then | |
| alert "plain-crypto-js found in GLOBAL npm packages!" | |
| info "$global_pcp" | |
| else | |
| ok "plain-crypto-js not installed globally" | |
| fi | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.3 — Global node_modules plain-crypto-js directory | |
| # ------------------------------------------------------------------- | |
| log "1.3 Scanning global node_modules for plain-crypto-js directory..." | |
| check_run | |
| if command -v npm &>/dev/null; then | |
| global_nm=$(npm root -g 2>/dev/null || true) | |
| if [ -d "$global_nm" ]; then | |
| check_plain_crypto_in_dir "$global_nm" "global node_modules" | |
| if [ ! -d "${global_nm}/plain-crypto-js" ]; then | |
| ok "plain-crypto-js directory absent from global node_modules" | |
| fi | |
| fi | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.4 — RAT artifact: macOS /Library/Caches/com.apple.act.mond | |
| # ------------------------------------------------------------------- | |
| log "1.4 Checking for macOS RAT artifact..." | |
| check_run | |
| if [ "$(uname -s)" = "Darwin" ]; then | |
| if [ -f "/Library/Caches/com.apple.act.mond" ]; then | |
| alert "macOS RAT binary found: /Library/Caches/com.apple.act.mond" | |
| info "This file was placed by the plain-crypto-js dropper." | |
| info "Remove it and treat this machine as COMPROMISED." | |
| ls -la "/Library/Caches/com.apple.act.mond" 2>/dev/null | tee -a "$REPORT_FILE" || true | |
| else | |
| ok "macOS RAT artifact not found: /Library/Caches/com.apple.act.mond" | |
| fi | |
| else | |
| info "Not macOS — skipping macOS artifact check" | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.5 — RAT artifact: Linux /tmp/ld.py | |
| # ------------------------------------------------------------------- | |
| log "1.5 Checking for Linux RAT artifact (/tmp/ld.py)..." | |
| check_run | |
| if [ -f "/tmp/ld.py" ]; then | |
| alert "Suspicious file found: /tmp/ld.py" | |
| info "This is the Python RAT script dropped by plain-crypto-js on Linux." | |
| info "File details:" | |
| ls -la /tmp/ld.py 2>/dev/null | tee -a "$REPORT_FILE" || true | |
| info "First 5 lines (do NOT execute):" | |
| head -5 /tmp/ld.py 2>/dev/null | tee -a "$REPORT_FILE" || true | |
| else | |
| ok "/tmp/ld.py not found" | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.6 — Temp script artifact: /tmp/6202033 (macOS AppleScript temp) | |
| # ------------------------------------------------------------------- | |
| log "1.6 Checking for dropper temp file (/tmp/${C2_CAMPAIGN_ID})..." | |
| check_run | |
| if [ -f "/tmp/${C2_CAMPAIGN_ID}" ]; then | |
| alert "Dropper temp file found: /tmp/${C2_CAMPAIGN_ID}" | |
| info "This file was written by the plain-crypto-js dropper (AppleScript wrapper)." | |
| ls -la "/tmp/${C2_CAMPAIGN_ID}" 2>/dev/null | tee -a "$REPORT_FILE" || true | |
| else | |
| ok "/tmp/${C2_CAMPAIGN_ID} not found" | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.7 — Suspicious running processes (nohup, ld.py, osascript dropper) | |
| # ------------------------------------------------------------------- | |
| log "1.7 Checking for suspicious running processes..." | |
| check_run | |
| PROC_FOUND=0 | |
| if command -v ps &>/dev/null; then | |
| # nohup python3 /tmp/ld.py | |
| if ps aux 2>/dev/null | grep -v grep | grep -q "ld\.py"; then | |
| alert "Suspicious process: python3 running ld.py (RAT payload)" | |
| ps aux 2>/dev/null | grep -v grep | grep "ld\.py" | tee -a "$REPORT_FILE" || true | |
| PROC_FOUND=1 | |
| fi | |
| # setup.js still running | |
| if ps aux 2>/dev/null | grep -v grep | grep -q "setup\.js"; then | |
| alert "Suspicious process: node running setup.js (dropper may still be active)" | |
| ps aux 2>/dev/null | grep -v grep | grep "setup\.js" | tee -a "$REPORT_FILE" || true | |
| PROC_FOUND=1 | |
| fi | |
| # macOS osascript running campaign ID script | |
| if ps aux 2>/dev/null | grep -v grep | grep -q "osascript.*${C2_CAMPAIGN_ID}"; then | |
| alert "Suspicious osascript process referencing campaign ID ${C2_CAMPAIGN_ID}" | |
| ps aux 2>/dev/null | grep -v grep | grep "osascript" | tee -a "$REPORT_FILE" || true | |
| PROC_FOUND=1 | |
| fi | |
| fi | |
| [ "$PROC_FOUND" -eq 0 ] && ok "No suspicious dropper/RAT processes found" | |
| # ------------------------------------------------------------------- | |
| # 1.8 — Network: active connections to C2 | |
| # ------------------------------------------------------------------- | |
| log "1.8 Checking active network connections to C2 (${C2_IP}:${C2_PORT} / ${C2_DOMAIN})..." | |
| check_run | |
| NET_FOUND=0 | |
| for tool in ss netstat lsof; do | |
| if command -v "$tool" &>/dev/null; then | |
| case "$tool" in | |
| ss) | |
| conn=$(ss -tnp 2>/dev/null | grep -E "${C2_IP}|${C2_DOMAIN}" || true) | |
| [ -n "$conn" ] && { alert "Active C2 connection via ss: $conn"; NET_FOUND=1; } | |
| ;; | |
| netstat) | |
| conn=$(netstat -tn 2>/dev/null | grep -E "${C2_IP}" || true) | |
| [ -n "$conn" ] && { alert "Active C2 connection via netstat: $conn"; NET_FOUND=1; } | |
| ;; | |
| lsof) | |
| conn=$(lsof -i "@${C2_IP}:${C2_PORT}" 2>/dev/null || true) | |
| [ -n "$conn" ] && { alert "Active C2 connection via lsof: $conn"; NET_FOUND=1; } | |
| ;; | |
| esac | |
| break | |
| fi | |
| done | |
| [ "$NET_FOUND" -eq 0 ] && ok "No active connections to C2 IP ${C2_IP}" | |
| # ------------------------------------------------------------------- | |
| # 1.9 — DNS / hosts: C2 domain resolution | |
| # ------------------------------------------------------------------- | |
| log "1.9 Checking if C2 domain ${C2_DOMAIN} resolves (should NOT unless already blocked)..." | |
| check_run | |
| if command -v host &>/dev/null 2>&1; then | |
| c2_resolve=$(host "$C2_DOMAIN" 2>/dev/null | grep -v "NXDOMAIN\|not found\|has no" || true) | |
| if [ -n "$c2_resolve" ]; then | |
| warn "C2 domain ${C2_DOMAIN} still resolves — consider blocking it in /etc/hosts or firewall." | |
| info "$c2_resolve" | |
| else | |
| ok "C2 domain ${C2_DOMAIN} does not resolve (may be taken down or already blocked)" | |
| fi | |
| elif command -v nslookup &>/dev/null 2>&1; then | |
| c2_resolve=$(nslookup "$C2_DOMAIN" 2>/dev/null | grep -i "address" | grep -v "127.0.0.1\|::1" || true) | |
| if [ -n "$c2_resolve" ]; then | |
| warn "C2 domain ${C2_DOMAIN} still resolves. Consider blocking." | |
| info "$c2_resolve" | |
| else | |
| ok "C2 domain ${C2_DOMAIN} does not resolve" | |
| fi | |
| else | |
| warn "Neither 'host' nor 'nslookup' available — cannot test C2 DNS resolution" | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.10 — /etc/hosts: check if C2 already blocked | |
| # ------------------------------------------------------------------- | |
| log "1.10 Checking /etc/hosts for C2 block entry..." | |
| check_run | |
| if grep -q "${C2_DOMAIN}" /etc/hosts 2>/dev/null; then | |
| ok "C2 domain ${C2_DOMAIN} is already in /etc/hosts (blocked or redirected)" | |
| grep "${C2_DOMAIN}" /etc/hosts | tee -a "$REPORT_FILE" || true | |
| else | |
| warn "C2 domain ${C2_DOMAIN} is NOT blocked in /etc/hosts" | |
| info "Consider adding: echo '0.0.0.0 ${C2_DOMAIN}' >> /etc/hosts" | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.11 — npm cache: check for cached malicious tarballs | |
| # ------------------------------------------------------------------- | |
| log "1.11 Checking npm cache for malicious package tarballs..." | |
| check_run | |
| if command -v npm &>/dev/null; then | |
| npm_cache_dir=$(npm config get cache 2>/dev/null || true) | |
| if [ -d "$npm_cache_dir" ]; then | |
| CACHE_HIT=0 | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| cached=$(find "$npm_cache_dir" -type f -name "*.tgz" 2>/dev/null \ | |
| | xargs grep -l "axios" 2>/dev/null \ | |
| | grep "$v" 2>/dev/null || true) | |
| # simpler path check: | |
| if find "$npm_cache_dir" -path "*/axios/${v}*" -type f 2>/dev/null | grep -q .; then | |
| warn "npm cache may contain axios@${v} tarball in: ${npm_cache_dir}" | |
| info "Run: npm cache clean --force (after resolving any active projects)" | |
| CACHE_HIT=1 | |
| fi | |
| done | |
| if find "$npm_cache_dir" -path "*/plain-crypto-js*" -type f 2>/dev/null | grep -q .; then | |
| warn "npm cache may contain plain-crypto-js tarball in: ${npm_cache_dir}" | |
| info "Run: npm cache clean --force" | |
| CACHE_HIT=1 | |
| fi | |
| [ "$CACHE_HIT" -eq 0 ] && ok "npm cache appears clean of malicious packages" | |
| else | |
| warn "npm cache directory not found at: ${npm_cache_dir}" | |
| fi | |
| fi | |
| # ------------------------------------------------------------------- | |
| # 1.12 — Shell history: npm install of compromised versions | |
| # ------------------------------------------------------------------- | |
| log "1.12 Scanning shell history for installs of compromised versions..." | |
| check_run | |
| HISTORY_FOUND=0 | |
| for hist_file in ~/.bash_history ~/.zsh_history ~/.sh_history ~/.history; do | |
| if [ -f "$hist_file" ]; then | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| if grep -q "axios@${v}\|axios.*${v}" "$hist_file" 2>/dev/null; then | |
| warn "Shell history references axios@${v} in ${hist_file}" | |
| grep -n "axios.*${v}\|${v}.*axios" "$hist_file" 2>/dev/null | head -5 | tee -a "$REPORT_FILE" || true | |
| HISTORY_FOUND=1 | |
| fi | |
| done | |
| if grep -q "plain-crypto-js" "$hist_file" 2>/dev/null; then | |
| warn "Shell history references plain-crypto-js in ${hist_file}" | |
| HISTORY_FOUND=1 | |
| fi | |
| fi | |
| done | |
| [ "$HISTORY_FOUND" -eq 0 ] && ok "No compromised version references in shell history" | |
| # ------------------------------------------------------------------- | |
| # 1.13 — ~/.npmrc: check for compromised tokens or overrides | |
| # ------------------------------------------------------------------- | |
| log "1.13 Checking ~/.npmrc for suspicious overrides..." | |
| check_run | |
| if [ -f ~/.npmrc ]; then | |
| if grep -qiE "sfrclak|ifstap|nrwise" ~/.npmrc 2>/dev/null; then | |
| alert "~/.npmrc contains C2/attacker references!" | |
| grep -iE "sfrclak|ifstap|nrwise" ~/.npmrc | tee -a "$REPORT_FILE" || true | |
| else | |
| ok "~/.npmrc looks clean" | |
| fi | |
| else | |
| ok "~/.npmrc not found (no overrides configured)" | |
| fi | |
| # ============================================================================= | |
| # SECTION 2 — PROJECT-LEVEL CHECKS (system-wide home directory scan) | |
| # ============================================================================= | |
| banner "SECTION 2: Home Directory Project Scan" | |
| log "Scanning home directory for Node.js projects..." | |
| # Collect all node_modules under home (respecting depth) | |
| HOME_RESULTS=$(find ~ \ | |
| -maxdepth "$FIND_DEPTH" \ | |
| -type d -name "node_modules" \ | |
| ! -path "*/node_modules/*/node_modules" \ | |
| 2>/dev/null || true) | |
| if [ -z "$HOME_RESULTS" ]; then | |
| ok "No node_modules directories found under home directory" | |
| else | |
| #log "Scanning ${HOME_RESULTS}" | |
| echo "" | |
| echo "$HOME_RESULTS" | while IFS= read -r nm; do | |
| project_dir=$(dirname "$nm") | |
| label="${project_dir}" | |
| log "2.1 Checking ${project_dir} for compromised axios..." | |
| # 2.1 Check for plain-crypto-js | |
| #log "2.1 Checking ${project_dir} for plain-crypto-js..." | |
| check_plain_crypto_in_dir "$nm" "$label" | |
| # 2.2 Check lockfile | |
| lock="${project_dir}/package-lock.json" | |
| yarn_lock="${project_dir}/yarn.lock" | |
| pnpm_lock="${project_dir}/pnpm-lock.yaml" | |
| if [ -f "$lock" ]; then | |
| #log "2.2 Checking ${project_dir} for package-lock.json..." | |
| check_lockfile "$lock" "$label" | |
| fi | |
| # yarn.lock | |
| if [ -f "$yarn_lock" ]; then | |
| #log "2.2 Checking ${project_dir} for yarn.lock..." | |
| check_run | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| if grep -q "axios@" "$yarn_lock" 2>/dev/null && \ | |
| grep -A5 "axios@" "$yarn_lock" 2>/dev/null | grep -q "\"${v}\""; then | |
| alert "Compromised axios@${v} found in yarn.lock — ${label}" | |
| info "File: ${yarn_lock}" | |
| fi | |
| done | |
| if grep -q "plain-crypto-js" "$yarn_lock" 2>/dev/null; then | |
| alert "plain-crypto-js in yarn.lock — ${label}" | |
| fi | |
| fi | |
| # pnpm-lock.yaml | |
| if [ -f "$pnpm_lock" ]; then | |
| #log "2.2 Checking ${project_dir} for pnpm-lock.yaml..." | |
| check_run | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| # Match axios and version together (pnpm uses /axios/version or axios@version format) | |
| if grep -qE "axios[@/]${v}" "$pnpm_lock" 2>/dev/null; then | |
| alert "Possible compromised axios@${v} in pnpm-lock.yaml — ${label}" | |
| info "File: ${pnpm_lock}" | |
| fi | |
| done | |
| if grep -q "plain-crypto-js" "$pnpm_lock" 2>/dev/null; then | |
| alert "plain-crypto-js in pnpm-lock.yaml — ${label}" | |
| fi | |
| fi | |
| # 2.3 Check package.json dependency declarations | |
| #log "2.3 Checking ${project_dir} for package.json..." | |
| check_package_json "${project_dir}/package.json" "$label" | |
| # 2.4 Verify axios shasum | |
| #log "2.4 Checking ${project_dir} for axios shasum..." | |
| check_axios_shasum_in_dir "$nm" "$label" | |
| done | |
| ok "Home directory scanning done" | |
| fi | |
| # ============================================================================= | |
| # SECTION 3 — WORKSPACE SCAN | |
| # ============================================================================= | |
| echo $WORKSPACE_PATH | |
| if [ -n "$WORKSPACE_PATH" ]; then | |
| banner "SECTION 3: Workspace Scan — ${WORKSPACE_PATH}" | |
| if [ ! -d "$WORKSPACE_PATH" ]; then | |
| warn "Workspace path does not exist: ${WORKSPACE_PATH}" | |
| else | |
| log "Discovering Node.js projects in: ${WORKSPACE_PATH} (depth ${FIND_DEPTH})..." | |
| # Find package.json files that aren't inside node_modules | |
| PROJECT_ROOTS=$(find "$WORKSPACE_PATH" \ | |
| -maxdepth "$FIND_DEPTH" \ | |
| -name "package.json" \ | |
| ! -path "*/node_modules/*" \ | |
| 2>/dev/null | xargs -I{} dirname {} | sort -u || true) | |
| if [ -z "$PROJECT_ROOTS" ]; then | |
| ok "No Node.js projects found under ${WORKSPACE_PATH}" | |
| else | |
| PROJECT_COUNT=$(echo "$PROJECT_ROOTS" | wc -l | tr -d ' ') | |
| log "Found ${PROJECT_COUNT} project(s). Scanning..." | |
| echo "$PROJECT_ROOTS" | while IFS= read -r proj; do | |
| nm_dir="${proj}/node_modules" | |
| label="$(basename "$proj") [${proj}]" | |
| echo "" | tee -a "$REPORT_FILE" | |
| log "── Project: ${label}" | |
| # 3.1 Check package.json | |
| check_package_json "${proj}/package.json" "$label" | |
| # 3.2 Check lockfile | |
| check_lockfile "${proj}/package-lock.json" "$label" | |
| # 3.3 yarn.lock | |
| if [ -f "${proj}/yarn.lock" ]; then | |
| check_run | |
| YARN_HIT=0 | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| # Check version appears within context of an axios entry | |
| if grep -A 5 "axios@" "${proj}/yarn.lock" 2>/dev/null | grep -q "\"${v}\""; then | |
| alert "Compromised axios@${v} in yarn.lock — ${label}" | |
| YARN_HIT=1 | |
| fi | |
| done | |
| if grep -q "plain-crypto-js" "${proj}/yarn.lock" 2>/dev/null; then | |
| alert "plain-crypto-js in yarn.lock — ${label}" | |
| YARN_HIT=1 | |
| fi | |
| [ "$YARN_HIT" -eq 0 ] && ok "yarn.lock clean — ${label}" | |
| fi | |
| # 3.4 pnpm-lock | |
| if [ -f "${proj}/pnpm-lock.yaml" ]; then | |
| check_run | |
| PNPM_HIT=0 | |
| for v in "${MALICIOUS_AXIOS_VERSIONS[@]}"; do | |
| # Match axios and version together (pnpm uses /axios/version or axios@version format) | |
| if grep -qE "axios[@/]${v}" "${proj}/pnpm-lock.yaml" 2>/dev/null; then | |
| alert "Possible compromised axios@${v} in pnpm-lock.yaml — ${label}" | |
| PNPM_HIT=1 | |
| fi | |
| done | |
| if grep -q "plain-crypto-js" "${proj}/pnpm-lock.yaml" 2>/dev/null; then | |
| alert "plain-crypto-js in pnpm-lock.yaml — ${label}" | |
| PNPM_HIT=1 | |
| fi | |
| [ "$PNPM_HIT" -eq 0 ] && ok "pnpm-lock.yaml clean — ${label}" | |
| fi | |
| # 3.5 node_modules checks (only if installed) | |
| if [ -d "$nm_dir" ]; then | |
| check_plain_crypto_in_dir "$nm_dir" "$label" | |
| check_axios_shasum_in_dir "$nm_dir" "$label" | |
| # 3.6 Check axios package.json inside node_modules for missing husky prepare | |
| # (the attacker removed "prepare": "husky" from the malicious release) | |
| axios_pj="${nm_dir}/axios/package.json" | |
| if [ -f "$axios_pj" ]; then | |
| check_run | |
| ax_ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$axios_pj" 2>/dev/null \ | |
| | grep -o '"[0-9][^"]*"' | tr -d '"' || true) | |
| has_prepare=$(grep '"prepare"' "$axios_pj" 2>/dev/null || true) | |
| has_plain_crypto=$(grep '"plain-crypto-js"' "$axios_pj" 2>/dev/null || true) | |
| has_githead=$(grep '"gitHead"' "$axios_pj" 2>/dev/null || true) | |
| if [ -n "$has_plain_crypto" ]; then | |
| alert "axios package.json contains plain-crypto-js dependency! — ${label}" | |
| info "Installed version: ${ax_ver}" | |
| fi | |
| # For v1.x: missing prepare script + no gitHead = malicious publish signal | |
| if echo "$ax_ver" | grep -q "^1\."; then | |
| if [ -z "$has_prepare" ] && [ -z "$has_githead" ]; then | |
| warn "axios@${ax_ver} is missing 'prepare' script and 'gitHead' — possible malicious publish in ${label}" | |
| info "Legitimate 1.x releases are published via GitHub Actions OIDC and include gitHead" | |
| fi | |
| fi | |
| fi | |
| # 3.7 Check for setup.js still present (dropper didn't finish cleanup) | |
| pcp_dir="${nm_dir}/plain-crypto-js" | |
| if [ -f "${pcp_dir}/setup.js" ]; then | |
| alert "setup.js still present in ${pcp_dir} — dropper active/incomplete!" | |
| fi | |
| else | |
| info "node_modules not installed for ${label} — only manifest/lockfile checked" | |
| fi | |
| done | |
| fi | |
| fi | |
| else | |
| echo "" | tee -a "$REPORT_FILE" | |
| info "No --workspace path given. Pass --workspace <path> to also scan local projects." | |
| fi | |
| # ============================================================================= | |
| # SECTION 4 — REMEDIATION GUIDANCE | |
| # ============================================================================= | |
| banner "SECTION 4: Remediation Reference" | |
| cat <<'REMEDIATION' | tee -a "$REPORT_FILE" | |
| If any issues were found above, take these steps: | |
| IMMEDIATE (any compromised project): | |
| ───────────────────────────────────── | |
| 1. Downgrade axios in affected projects: | |
| npm install axios@1.14.0 # for 1.x users | |
| npm install axios@0.30.3 # for 0.x users | |
| 2. Remove plain-crypto-js and reinstall with scripts disabled: | |
| rm -rf node_modules/plain-crypto-js | |
| npm install --ignore-scripts | |
| 3. Add overrides to prevent re-resolution of malicious versions: | |
| In package.json: | |
| "overrides": { "axios": "1.14.0" } | |
| "resolutions": { "axios": "1.14.0" } | |
| COMPROMISED MACHINE (RAT artifacts found / plain-crypto-js executed): | |
| ──────────────────────────────────────────────────────────────────── | |
| 1. Isolate machine from the network immediately. | |
| 2. Rotate ALL credentials (npm tokens, SSH keys, AWS, GCP, Azure, | |
| GitHub PATs, Docker credentials, .env secrets, browser passwords). | |
| 3. Remove RAT artifacts: | |
| macOS: sudo rm -f /Library/Caches/com.apple.act.mond | |
| Linux: rm -f /tmp/ld.py | |
| 4. Block C2: | |
| echo "0.0.0.0 sfrclak.com" | sudo tee -a /etc/hosts | |
| # Linux firewall: | |
| sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP | |
| 5. Reformat and rebuild from a known-good image. | |
| CI/CD (ephemeral runners): | |
| ────────────────────────── | |
| • Rotate all secrets injected into any workflow that ran during the | |
| compromise window (2026-03-31 00:21 UTC – 03:15 UTC). | |
| • Add --ignore-scripts to CI install commands: | |
| npm ci --ignore-scripts | |
| PREVENTION (going forward): | |
| ─────────────────────────── | |
| • Pin exact axios versions in package.json and commit lockfiles. | |
| • Configure npm release age gate in .npmrc: | |
| min-release-age=7d | |
| • Use npm ci --ignore-scripts in CI/CD. | |
| • Enable Dependabot cooldown (3 days minimum). | |
| IOCs to block: | |
| ────────────── | |
| Domain : sfrclak.com | |
| IP : 142.11.206.73 | |
| Port : 8000 | |
| REMEDIATION | |
| # ============================================================================= | |
| # SUMMARY | |
| # ============================================================================= | |
| banner "SUMMARY" | |
| echo "" | tee -a "$REPORT_FILE" | |
| echo -e " Checks run : ${BOLD}${CHECKS_RUN}${RESET}" | tee -a "$REPORT_FILE" | |
| echo -e " Warnings : ${BOLD}${YELLOW}${WARNINGS}${RESET}" | tee -a "$REPORT_FILE" | |
| if [ "$FOUND_ISSUES" -gt 0 ]; then | |
| echo -e " COMPROMISE INDICATORS FOUND : ${BOLD}${RED}${FOUND_ISSUES}${RESET}" | tee -a "$REPORT_FILE" | |
| echo "" | tee -a "$REPORT_FILE" | |
| echo -e "${RED}${BOLD} !! SYSTEM MAY BE COMPROMISED — See remediation steps above !!${RESET}" | tee -a "$REPORT_FILE" | |
| elif [ "$WARNINGS" -gt 0 ]; then | |
| echo -e " Compromise indicators : ${BOLD}${GREEN}None detected${RESET}" | tee -a "$REPORT_FILE" | |
| echo "" | tee -a "$REPORT_FILE" | |
| echo -e "${YELLOW}${BOLD} Warnings found — review items marked [!] above.${RESET}" | tee -a "$REPORT_FILE" | |
| else | |
| echo -e " Compromise indicators : ${BOLD}${GREEN}None detected${RESET}" | tee -a "$REPORT_FILE" | |
| echo "" | tee -a "$REPORT_FILE" | |
| echo -e "${GREEN}${BOLD} No indicators of compromise found. Stay vigilant.${RESET}" | tee -a "$REPORT_FILE" | |
| fi | |
| echo "" | tee -a "$REPORT_FILE" | |
| echo -e " Full report saved to: ${BOLD}${REPORT_FILE}${RESET}" | tee -a "$REPORT_FILE" | |
| echo "" | tee -a "$REPORT_FILE" | |
| # Exit code: 1 = compromised, 2 = warnings only, 0 = clean | |
| if [ "$FOUND_ISSUES" -gt 0 ]; then | |
| exit 1 | |
| elif [ "$WARNINGS" -gt 0 ]; then | |
| exit 2 | |
| else | |
| exit 0 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment