Created
April 1, 2026 06:25
-
-
Save GNURub/9d32e263c23703d49ad3b4d5e4249ff1 to your computer and use it in GitHub Desktop.
Detector script for the WAVESHAPER.V2 / axios npm supply chain attack attributed to North Korea-nexus threat actor UNC1069 (Mandiant/GTIG, March 31 2026). Checks for IOC file hashes, malicious plain-crypto-js package, compromised axios versions (1.14.1 / 0.30.4), active C2 connections (sfrclak.com / 142.11.206.73), npm cache artifacts, suspiciou…
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_waveshaper.sh — Detector WAVESHAPER.V2 / axios supply chain (UNC1069) | |
| # Basado en: https://cloud.google.com/blog/topics/threat-intelligence/ | |
| # north-korea-threat-actor-targets-axios-npm-package/ | |
| # Objetivo: Ubuntu / Linux | |
| # Uso: sudo bash check_waveshaper.sh | |
| # ============================================================================= | |
| set -euo pipefail | |
| # ---------- colores ----------------------------------------------------------- | |
| RED='\033[0;31m'; YEL='\033[1;33m'; GRN='\033[0;32m' | |
| CYA='\033[0;36m'; BLD='\033[1m'; RST='\033[0m' | |
| INFECTED=0 | |
| WARNINGS=0 | |
| LOG_FILE="/tmp/waveshaper_scan_$(date +%Y%m%d_%H%M%S).log" | |
| log() { echo -e "$*" | tee -a "$LOG_FILE"; } | |
| ok() { log "${GRN} [OK]${RST} $*"; } | |
| warn() { log "${YEL} [WARN]${RST} $*"; ((WARNINGS++)) || true; } | |
| hit() { log "${RED} [!!!]${RST} $*"; ((INFECTED++)) || true; } | |
| info() { log "${CYA} [→]${RST} $*"; } | |
| sep() { log "${BLD}──────────────────────────────────────────────────────${RST}"; } | |
| # ============================================================================= | |
| # INDICADORES DE COMPROMISO (IOCs) — Google Threat Intelligence / Mandiant | |
| # ============================================================================= | |
| # Hashes SHA256 maliciosos | |
| declare -A MALICIOUS_HASHES=( | |
| ["fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf"]="WAVESHAPER.V2 Linux Python RAT" | |
| ["e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09"]="SILKBELL dropper (setup.js)" | |
| ["58401c195fe0a6204b42f5f90995ece5fab74ce7c69c67a24c61a057325af668"]="plain-crypto-js-4.2.1.tgz" | |
| ["92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a"]="WAVESHAPER.V2 macOS Binary" | |
| ["617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101"]="WAVESHAPER.V2 Windows Stage 1" | |
| ) | |
| # IPs y dominio C2 | |
| C2_IPS=("142.11.206.73" "23.254.167.216") | |
| C2_DOMAIN="sfrclak.com" | |
| C2_PORT="8000" | |
| # Paquete malicioso | |
| MALICIOUS_PKG="plain-crypto-js" | |
| MALICIOUS_PKG_VERSIONS=("4.2.0" "4.2.1") | |
| # Versiones comprometidas de axios | |
| AXIOS_BAD_VERSIONS=("1.14.1" "0.30.4") | |
| # Archivo backdoor en Linux | |
| LINUX_RAT="/tmp/ld.py" | |
| # User-Agent del beacon C2 | |
| C2_USERAGENT="mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)" | |
| # ============================================================================= | |
| clear | |
| log "" | |
| log "${BLD}╔══════════════════════════════════════════════════════════╗${RST}" | |
| log "${BLD}║ WAVESHAPER.V2 / axios Supply Chain — Detector Linux ║${RST}" | |
| log "${BLD}║ Amenaza: UNC1069 (Corea del Norte) — GTIG / Mandiant ║${RST}" | |
| log "${BLD}╚══════════════════════════════════════════════════════════╝${RST}" | |
| log "" | |
| info "Log guardado en: $LOG_FILE" | |
| info "Inicio: $(date)" | |
| sep | |
| # ============================================================================= | |
| # 1. ARCHIVO BACKDOOR PRINCIPAL (/tmp/ld.py) | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[1/8] Backdoor Python en /tmp/ld.py${RST}" | |
| sep | |
| if [[ -f "$LINUX_RAT" ]]; then | |
| hit "ENCONTRADO: $LINUX_RAT" | |
| HASH=$(sha256sum "$LINUX_RAT" | awk '{print $1}') | |
| if [[ "${MALICIOUS_HASHES[$HASH]+_}" ]]; then | |
| hit "SHA256 COINCIDE con IOC conocido: ${MALICIOUS_HASHES[$HASH]}" | |
| hit "Hash: $HASH" | |
| else | |
| warn "Archivo existe pero hash no coincide exactamente ($HASH)" | |
| warn "Puede ser una variante. Analizar manualmente." | |
| fi | |
| else | |
| ok "$LINUX_RAT no encontrado" | |
| fi | |
| # Búsqueda adicional de ld.py en /tmp y variantes | |
| for f in /tmp/ld.py /tmp/ld2.py /tmp/.ld.py /tmp/ldd.py; do | |
| [[ "$f" == "$LINUX_RAT" ]] && continue | |
| if [[ -f "$f" ]]; then | |
| warn "Archivo sospechoso adicional encontrado: $f" | |
| fi | |
| done | |
| # ============================================================================= | |
| # 2. ARCHIVOS CON HASHES IOC | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[2/8] Búsqueda de archivos con hashes maliciosos conocidos${RST}" | |
| sep | |
| SEARCH_DIRS=("/tmp" "/var/tmp" "/home" "/root" "/opt" "/srv") | |
| FOUND_FILES=() | |
| for dir in "${SEARCH_DIRS[@]}"; do | |
| if [[ -d "$dir" ]]; then | |
| while IFS= read -r -d '' file; do | |
| HASH=$(sha256sum "$file" 2>/dev/null | awk '{print $1}' || true) | |
| if [[ -n "$HASH" && "${MALICIOUS_HASHES[$HASH]+_}" ]]; then | |
| hit "MATCH IOC: $file → ${MALICIOUS_HASHES[$HASH]}" | |
| hit "Hash: $HASH" | |
| FOUND_FILES+=("$file") | |
| fi | |
| done < <(find "$dir" -maxdepth 5 -type f -print0 2>/dev/null) | |
| fi | |
| done | |
| # Buscar setup.js con nombre sospechoso | |
| while IFS= read -r -d '' file; do | |
| HASH=$(sha256sum "$file" 2>/dev/null | awk '{print $1}' || true) | |
| if [[ -n "$HASH" && "${MALICIOUS_HASHES[$HASH]+_}" ]]; then | |
| hit "MATCH IOC (setup.js): $file → ${MALICIOUS_HASHES[$HASH]}" | |
| FOUND_FILES+=("$file") | |
| fi | |
| done < <(find /tmp /home /root /opt -name "setup.js" -print0 2>/dev/null) | |
| if [[ ${#FOUND_FILES[@]} -eq 0 ]]; then | |
| ok "No se encontraron archivos con hashes IOC conocidos" | |
| fi | |
| # ============================================================================= | |
| # 3. PAQUETE MALICIOSO EN npm (plain-crypto-js) | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[3/8] Paquete npm malicioso: plain-crypto-js${RST}" | |
| sep | |
| # Buscar en node_modules de todo el sistema | |
| info "Buscando plain-crypto-js en node_modules..." | |
| FOUND_MODULES=() | |
| while IFS= read -r -d '' dir; do | |
| PKG_JSON="$dir/package.json" | |
| if [[ -f "$PKG_JSON" ]]; then | |
| PKG_VERSION=$(python3 -c "import json,sys; d=json.load(open('$PKG_JSON')); print(d.get('version','?'))" 2>/dev/null || true) | |
| hit "PAQUETE MALICIOSO encontrado: $dir (versión: $PKG_VERSION)" | |
| FOUND_MODULES+=("$dir") | |
| fi | |
| done < <(find / -path "*/node_modules/plain-crypto-js" -type d -print0 2>/dev/null) | |
| if [[ ${#FOUND_MODULES[@]} -eq 0 ]]; then | |
| ok "plain-crypto-js no encontrado en node_modules" | |
| fi | |
| # Buscar en package-lock.json y yarn.lock | |
| info "Buscando plain-crypto-js en lockfiles..." | |
| LOCKFILE_HITS=() | |
| while IFS= read -r file; do | |
| if grep -q "plain-crypto-js" "$file" 2>/dev/null; then | |
| hit "plain-crypto-js referenciado en: $file" | |
| grep -n "plain-crypto-js" "$file" | while read -r line; do | |
| hit " → $line" | |
| done | |
| LOCKFILE_HITS+=("$file") | |
| fi | |
| done < <(find /home /root /opt /srv /var/www -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" 2>/dev/null) | |
| if [[ ${#LOCKFILE_HITS[@]} -eq 0 ]]; then | |
| ok "plain-crypto-js no encontrado en lockfiles" | |
| fi | |
| # ============================================================================= | |
| # 4. VERSIONES COMPROMETIDAS DE AXIOS | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[4/8] Versiones comprometidas de axios (1.14.1 / 0.30.4)${RST}" | |
| sep | |
| AXIOS_HITS=() | |
| while IFS= read -r -d '' dir; do | |
| PKG_JSON="$dir/package.json" | |
| if [[ -f "$PKG_JSON" ]]; then | |
| PKG_VERSION=$(python3 -c "import json,sys; d=json.load(open('$PKG_JSON')); print(d.get('version',''))" 2>/dev/null || true) | |
| for bad_ver in "${AXIOS_BAD_VERSIONS[@]}"; do | |
| if [[ "$PKG_VERSION" == "$bad_ver" ]]; then | |
| hit "axios versión COMPROMETIDA ($bad_ver) en: $dir" | |
| AXIOS_HITS+=("$dir") | |
| fi | |
| done | |
| fi | |
| done < <(find / -path "*/node_modules/axios" -type d -print0 2>/dev/null) | |
| if [[ ${#AXIOS_HITS[@]} -eq 0 ]]; then | |
| ok "No se detectaron versiones comprometidas de axios" | |
| else | |
| warn "ACCIÓN REQUERIDA: Degradar axios a 1.14.0 o anterior / 0.30.3 o anterior" | |
| fi | |
| # ============================================================================= | |
| # 5. CACHÉ NPM CONTAMINADA | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[5/8] Caché npm/yarn/pnpm contaminada${RST}" | |
| sep | |
| NPM_CACHE_DIRS=() | |
| # npm | |
| for user_home in /root /home/*; do | |
| cache_dir="$user_home/.npm" | |
| [[ -d "$cache_dir" ]] && NPM_CACHE_DIRS+=("$cache_dir") | |
| done | |
| # yarn | |
| for user_home in /root /home/*; do | |
| cache_dir="$user_home/.yarn/cache" | |
| [[ -d "$cache_dir" ]] && NPM_CACHE_DIRS+=("$cache_dir") | |
| done | |
| CACHE_HITS=() | |
| for cache_dir in "${NPM_CACHE_DIRS[@]}"; do | |
| info "Inspeccionando caché: $cache_dir" | |
| # Buscar el tgz malicioso por hash | |
| while IFS= read -r -d '' f; do | |
| HASH=$(sha256sum "$f" 2>/dev/null | awk '{print $1}' || true) | |
| if [[ "${MALICIOUS_HASHES[$HASH]+_}" ]]; then | |
| hit "ARCHIVO MALICIOSO en caché: $f" | |
| hit "Tipo: ${MALICIOUS_HASHES[$HASH]}" | |
| CACHE_HITS+=("$f") | |
| fi | |
| done < <(find "$cache_dir" -type f -print0 2>/dev/null) | |
| # Buscar por nombre | |
| while IFS= read -r f; do | |
| hit "plain-crypto-js encontrado en caché: $f" | |
| CACHE_HITS+=("$f") | |
| done < <(find "$cache_dir" -name "*plain-crypto*" 2>/dev/null) | |
| done | |
| if [[ ${#CACHE_HITS[@]} -eq 0 ]]; then | |
| ok "No se encontraron artefactos maliciosos en caché npm/yarn" | |
| else | |
| warn "ACCIÓN: Limpiar caché con: npm cache clean --force && yarn cache clean" | |
| fi | |
| # ============================================================================= | |
| # 6. CONEXIONES DE RED AL C2 | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[6/8] Conexiones de red al C2 (sfrclak.com / 142.11.206.73)${RST}" | |
| sep | |
| NET_HITS=0 | |
| # Comprobar conexiones activas | |
| if command -v ss &>/dev/null; then | |
| for ip in "${C2_IPS[@]}"; do | |
| CONNS=$(ss -tnp 2>/dev/null | grep "$ip" || true) | |
| if [[ -n "$CONNS" ]]; then | |
| hit "CONEXIÓN ACTIVA al C2 ($ip):" | |
| echo "$CONNS" | while read -r line; do hit " $line"; done | |
| ((NET_HITS++)) || true | |
| fi | |
| done | |
| DOMAIN_CONNS=$(ss -tnp 2>/dev/null | grep ":$C2_PORT" || true) | |
| if [[ -n "$DOMAIN_CONNS" ]]; then | |
| warn "Conexiones activas al puerto $C2_PORT (puerto C2):" | |
| echo "$DOMAIN_CONNS" | while read -r line; do warn " $line"; done | |
| fi | |
| elif command -v netstat &>/dev/null; then | |
| for ip in "${C2_IPS[@]}"; do | |
| CONNS=$(netstat -tnp 2>/dev/null | grep "$ip" || true) | |
| if [[ -n "$CONNS" ]]; then | |
| hit "CONEXIÓN ACTIVA al C2 ($ip):" | |
| echo "$CONNS" | while read -r line; do hit " $line"; done | |
| ((NET_HITS++)) || true | |
| fi | |
| done | |
| fi | |
| # Comprobar resolución DNS del dominio C2 | |
| if command -v host &>/dev/null; then | |
| RESOLVED=$(host "$C2_DOMAIN" 2>/dev/null || true) | |
| if echo "$RESOLVED" | grep -q "142.11.206.73"; then | |
| warn "El dominio C2 ($C2_DOMAIN) resuelve a la IP maliciosa conocida" | |
| else | |
| ok "El dominio C2 ($C2_DOMAIN) no resuelve a IP conocida (puede estar bloqueado)" | |
| fi | |
| fi | |
| # Comprobar /etc/hosts por si hay entradas C2 | |
| if grep -qE "(sfrclak|142\.11\.206\.73|23\.254\.167\.216)" /etc/hosts 2>/dev/null; then | |
| warn "/etc/hosts contiene referencias al C2 — revisar manualmente" | |
| grep -E "(sfrclak|142\.11\.206\.73|23\.254\.167\.216)" /etc/hosts | while read -r line; do | |
| warn " /etc/hosts: $line" | |
| done | |
| else | |
| ok "/etc/hosts limpio" | |
| fi | |
| # Revisar logs de conexiones pasadas (si existen) | |
| for log_path in /var/log/syslog /var/log/auth.log /var/log/kern.log; do | |
| if [[ -f "$log_path" ]]; then | |
| PAST=$(grep -E "(sfrclak|142\.11\.206\.73|23\.254\.167\.216)" "$log_path" 2>/dev/null | tail -5 || true) | |
| if [[ -n "$PAST" ]]; then | |
| hit "Referencias al C2 encontradas en $log_path:" | |
| echo "$PAST" | while read -r line; do hit " $line"; done | |
| fi | |
| fi | |
| done | |
| [[ $NET_HITS -eq 0 ]] && ok "No se detectaron conexiones activas al C2" | |
| # ============================================================================= | |
| # 7. PROCESOS SOSPECHOSOS | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[7/8] Procesos sospechosos en ejecución${RST}" | |
| sep | |
| PROC_HITS=0 | |
| # Python ejecutando ld.py o scripts de /tmp | |
| SUSPICIOUS_PROCS=$(ps aux 2>/dev/null | grep -E "(python.*ld\.py|python.*/tmp/|node.*setup\.js)" | grep -v grep || true) | |
| if [[ -n "$SUSPICIOUS_PROCS" ]]; then | |
| hit "PROCESO SOSPECHOSO detectado:" | |
| echo "$SUSPICIOUS_PROCS" | while read -r line; do hit " $line"; done | |
| ((PROC_HITS++)) || true | |
| fi | |
| # Procesos con conexión al puerto 8000 (beacon C2) | |
| if command -v lsof &>/dev/null; then | |
| BEACON_PROCS=$(lsof -i ":$C2_PORT" 2>/dev/null | grep -v "^COMMAND" || true) | |
| if [[ -n "$BEACON_PROCS" ]]; then | |
| warn "Proceso usando puerto $C2_PORT (puerto beacon C2):" | |
| echo "$BEACON_PROCS" | while read -r line; do warn " $line"; done | |
| fi | |
| fi | |
| # Procesos node spawnando curl/python hacia /tmp | |
| NODE_CURL=$(ps aux 2>/dev/null | grep -E "node.*curl|node.*python" | grep -v grep || true) | |
| if [[ -n "$NODE_CURL" ]]; then | |
| warn "Node.js spawnando curl/python (patrón sospechoso):" | |
| echo "$NODE_CURL" | while read -r line; do warn " $line"; done | |
| fi | |
| [[ $PROC_HITS -eq 0 ]] && ok "No se encontraron procesos sospechosos activos" | |
| # ============================================================================= | |
| # 8. REVISIÓN POSTINSTALL EN package.json | |
| # ============================================================================= | |
| log "" | |
| log "${BLD}[8/8] Hooks postinstall sospechosos en package.json${RST}" | |
| sep | |
| POSTINSTALL_HITS=() | |
| while IFS= read -r file; do | |
| # Buscar "postinstall" que ejecute node setup.js u otros comandos sospechosos | |
| if grep -qE '"postinstall".*"node setup\.js"' "$file" 2>/dev/null; then | |
| hit "Hook postinstall malicioso en: $file" | |
| grep -n "postinstall" "$file" | while read -r line; do hit " $line"; done | |
| POSTINSTALL_HITS+=("$file") | |
| fi | |
| done < <(find / -path "*/node_modules/plain-crypto-js/package.json" 2>/dev/null) | |
| find /tmp /home /root /opt -name "package.json" 2>/dev/null | while read -r file; do | |
| if grep -qP '"postinstall"\s*:\s*"node setup\.js"' "$file" 2>/dev/null; then | |
| hit "Hook postinstall sospechoso en: $file" | |
| POSTINSTALL_HITS+=("$file") | |
| fi | |
| done | |
| if [[ ${#POSTINSTALL_HITS[@]} -eq 0 ]]; then | |
| ok "No se detectaron hooks postinstall maliciosos" | |
| fi | |
| # ============================================================================= | |
| # RESUMEN FINAL | |
| # ============================================================================= | |
| sep | |
| log "" | |
| log "${BLD}╔══════════════════════════════════╗${RST}" | |
| log "${BLD}║ RESULTADO DEL ANÁLISIS ║${RST}" | |
| log "${BLD}╚══════════════════════════════════╝${RST}" | |
| log "" | |
| if [[ $INFECTED -gt 0 ]]; then | |
| log "${RED}${BLD} ⚠ SISTEMA POSIBLEMENTE COMPROMETIDO${RST}" | |
| log "${RED} $INFECTED indicadores de compromiso detectados${RST}" | |
| log "" | |
| log "${BLD} PASOS INMEDIATOS:${RST}" | |
| log " 1. Aislar la máquina de la red" | |
| log " 2. Eliminar /tmp/ld.py si existe: ${RED}rm -f /tmp/ld.py${RST}" | |
| log " 3. Bloquear C2 en firewall:" | |
| log " ${YEL}sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP${RST}" | |
| log " ${YEL}sudo iptables -A OUTPUT -d 23.254.167.216 -j DROP${RST}" | |
| log " 4. Limpiar caché npm: ${YEL}npm cache clean --force${RST}" | |
| log " 5. Degradar axios a versión segura (≤1.14.0 / ≤0.30.3)" | |
| log " 6. Rotar TODOS los tokens, API keys y credenciales del sistema" | |
| log " 7. Revisar cron jobs: ${YEL}crontab -l && cat /etc/cron*/${RST}" | |
| elif [[ $WARNINGS -gt 0 ]]; then | |
| log "${YEL}${BLD} ⚡ ADVERTENCIAS — Revisar manualmente${RST}" | |
| log "${YEL} $WARNINGS avisos detectados (sin IOCs directos confirmados)${RST}" | |
| log "" | |
| log " Revisa los puntos marcados con [WARN] en este log." | |
| else | |
| log "${GRN}${BLD} ✓ SISTEMA LIMPIO${RST}" | |
| log "${GRN} No se detectaron IOCs del ataque WAVESHAPER.V2 / axios${RST}" | |
| log "" | |
| log " ${BLD}Recomendaciones preventivas:${RST}" | |
| log " • Asegúrate de usar axios ≤ 1.14.0 o ≤ 0.30.3 en todos tus proyectos" | |
| log " • Añade a tu firewall: iptables -A OUTPUT -d 142.11.206.73 -j DROP" | |
| log " • Usa 'npm audit' regularmente en tus proyectos Node.js" | |
| fi | |
| log "" | |
| log " Log completo guardado en: ${CYA}$LOG_FILE${RST}" | |
| log " Fecha: $(date)" | |
| sep |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment