|
#!/usr/bin/env bash |
|
# ============================================================================ |
|
# check-axios-compromise.sh |
|
# Détecte les versions compromises d'axios (1.14.1, 0.30.4) et les IOCs |
|
# associés à l'attaque supply chain du 31 mars 2026. |
|
# |
|
# Ref: https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan |
|
# |
|
# Usage: |
|
# ./check-axios-compromise.sh [SCAN_DIR] |
|
# |
|
# SCAN_DIR Répertoire racine à scanner (défaut: répertoire courant) |
|
# |
|
# Le script vérifie : |
|
# 1. package.json / package-lock.json / yarn.lock / pnpm-lock.yaml |
|
# pour les versions axios@1.14.1 et axios@0.30.4 |
|
# 2. La présence de plain-crypto-js (dépendance malveillante injectée) |
|
# 3. Les node_modules installés pour plain-crypto-js |
|
# 4. Les IOCs filesystem (RAT artifacts) sur la machine locale |
|
# 5. Les IOCs réseau (domaine C2 sfrclak.com) |
|
# |
|
# Codes retour : |
|
# 0 Aucun indicateur trouvé |
|
# 1 Indicateur(s) de compromission détecté(s) |
|
# 2 Erreur d'exécution |
|
# ============================================================================ |
|
|
|
set -euo pipefail |
|
|
|
# --- Configuration ----------------------------------------------------------- |
|
|
|
MALICIOUS_AXIOS_VERSIONS="1\.14\.1|0\.30\.4" |
|
MALICIOUS_DEP="plain-crypto-js" |
|
C2_DOMAIN="sfrclak.com" |
|
C2_IP="142.11.206.73" |
|
|
|
MALICIOUS_SHASUMS=( |
|
"2553649f232204966871cea80a5d0d6adc700ca" # axios@1.14.1 |
|
"d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71" # axios@0.30.4 |
|
"07d889e2dadce6f3910dcbc253317d28ca61c766" # plain-crypto-js@4.2.1 |
|
) |
|
|
|
RAT_ARTIFACTS_LINUX=( |
|
"/tmp/ld.py" |
|
) |
|
RAT_ARTIFACTS_MACOS=( |
|
"/Library/Caches/com.apple.act.mond" |
|
) |
|
|
|
# --- Couleurs ---------------------------------------------------------------- |
|
|
|
RED='\033[0;31m' |
|
YELLOW='\033[1;33m' |
|
GREEN='\033[0;32m' |
|
CYAN='\033[0;36m' |
|
BOLD='\033[1m' |
|
NC='\033[0m' |
|
|
|
# --- Helpers ----------------------------------------------------------------- |
|
|
|
FOUND_ISSUES=0 |
|
SCAN_DIR="${1:-.}" |
|
|
|
banner() { |
|
echo "" |
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}" |
|
echo -e "${BOLD}║ axios supply chain compromise checker — 2026-03-31 ║${NC}" |
|
echo -e "${BOLD}║ Versions malveillantes: axios@1.14.1, axios@0.30.4 ║${NC}" |
|
echo -e "${BOLD}║ Dépendance injectée: plain-crypto-js@4.2.1 ║${NC}" |
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}" |
|
echo "" |
|
} |
|
|
|
alert() { |
|
echo -e " ${RED}✗ ALERTE${NC} $1" |
|
FOUND_ISSUES=1 |
|
} |
|
|
|
warn() { |
|
echo -e " ${YELLOW}⚠ AVERTISSEMENT${NC} $1" |
|
} |
|
|
|
ok() { |
|
echo -e " ${GREEN}✓${NC} $1" |
|
} |
|
|
|
info() { |
|
echo -e " ${CYAN}ℹ${NC} $1" |
|
} |
|
|
|
section() { |
|
echo "" |
|
echo -e "${BOLD}── $1 ──${NC}" |
|
} |
|
|
|
# --- Checks ------------------------------------------------------------------ |
|
|
|
check_package_json_files() { |
|
section "1/5 Scan des package.json" |
|
|
|
local count=0 |
|
local hit=0 |
|
|
|
while IFS= read -r -d '' pjson; do |
|
count=$((count + 1)) |
|
|
|
# Vérifier axios avec versions compromises |
|
if grep -qE "\"axios\"[[:space:]]*:[[:space:]]*\"[^\"]*($MALICIOUS_AXIOS_VERSIONS)" "$pjson" 2>/dev/null; then |
|
alert "$pjson → axios version compromise détectée !" |
|
grep -nE "\"axios\"" "$pjson" | head -5 | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
# Vérifier présence de plain-crypto-js |
|
if grep -q "$MALICIOUS_DEP" "$pjson" 2>/dev/null; then |
|
alert "$pjson → dépendance malveillante '$MALICIOUS_DEP' présente !" |
|
grep -n "$MALICIOUS_DEP" "$pjson" | head -5 | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
done < <(find "$SCAN_DIR" -name "package.json" -not -path "*/node_modules/*" -print0 2>/dev/null) |
|
|
|
if [[ $hit -eq 0 ]]; then |
|
ok "Aucune version compromise dans $count fichier(s) package.json" |
|
fi |
|
} |
|
|
|
check_lockfiles() { |
|
section "2/5 Scan des lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml)" |
|
|
|
local count=0 |
|
local hit=0 |
|
|
|
# package-lock.json |
|
while IFS= read -r -d '' lockfile; do |
|
count=$((count + 1)) |
|
|
|
# Versions compromises d'axios |
|
if grep -qE "\"axios-($MALICIOUS_AXIOS_VERSIONS)\"|\"version\"[[:space:]]*:[[:space:]]*\"($MALICIOUS_AXIOS_VERSIONS)\"" "$lockfile" 2>/dev/null; then |
|
# Grep plus large pour attraper le contexte |
|
if grep -B2 -A2 -nE "($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" 2>/dev/null | grep -qi "axios"; then |
|
alert "$lockfile → version axios compromise lockée !" |
|
grep -n -E "($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" | grep -i "axios" | head -5 | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
fi |
|
|
|
# plain-crypto-js |
|
if grep -q "$MALICIOUS_DEP" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → '$MALICIOUS_DEP' présent dans le lockfile !" |
|
grep -n "$MALICIOUS_DEP" "$lockfile" | head -5 | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
# Vérifier les shasums connues |
|
for sha in "${MALICIOUS_SHASUMS[@]}"; do |
|
if grep -q "$sha" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → shasum malveillante détectée: $sha" |
|
hit=$((hit + 1)) |
|
fi |
|
done |
|
|
|
done < <(find "$SCAN_DIR" -name "package-lock.json" -not -path "*/node_modules/*" -print0 2>/dev/null) |
|
|
|
# yarn.lock |
|
while IFS= read -r -d '' lockfile; do |
|
count=$((count + 1)) |
|
|
|
if grep -qE "axios@.*($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → version axios compromise lockée !" |
|
grep -n -E "axios.*($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" | head -5 | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
if grep -q "$MALICIOUS_DEP" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → '$MALICIOUS_DEP' dans yarn.lock !" |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
for sha in "${MALICIOUS_SHASUMS[@]}"; do |
|
if grep -q "$sha" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → shasum malveillante: $sha" |
|
hit=$((hit + 1)) |
|
fi |
|
done |
|
|
|
done < <(find "$SCAN_DIR" -name "yarn.lock" -not -path "*/node_modules/*" -print0 2>/dev/null) |
|
|
|
# pnpm-lock.yaml |
|
while IFS= read -r -d '' lockfile; do |
|
count=$((count + 1)) |
|
|
|
if grep -qE "axios.*($MALICIOUS_AXIOS_VERSIONS)" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → version axios compromise dans pnpm-lock !" |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
if grep -q "$MALICIOUS_DEP" "$lockfile" 2>/dev/null; then |
|
alert "$lockfile → '$MALICIOUS_DEP' dans pnpm-lock !" |
|
hit=$((hit + 1)) |
|
fi |
|
|
|
done < <(find "$SCAN_DIR" -name "pnpm-lock.yaml" -not -path "*/node_modules/*" -print0 2>/dev/null) |
|
|
|
if [[ $hit -eq 0 ]]; then |
|
ok "Aucune trace dans $count lockfile(s)" |
|
fi |
|
} |
|
|
|
check_node_modules() { |
|
section "3/5 Scan des node_modules installés" |
|
|
|
local count=0 |
|
local hit=0 |
|
|
|
# Chercher plain-crypto-js dans les node_modules |
|
while IFS= read -r -d '' moddir; do |
|
alert "Répertoire '$MALICIOUS_DEP' trouvé : $moddir" |
|
info "→ Si ce répertoire existe, le dropper a probablement été exécuté." |
|
info "→ Même si package.json semble propre (le malware se nettoie)." |
|
|
|
# Vérifier si setup.js existe encore (rare, le malware le supprime) |
|
if [[ -f "$moddir/setup.js" ]]; then |
|
alert "setup.js encore présent dans $moddir — dropper non nettoyé !" |
|
fi |
|
|
|
# Vérifier la version dans package.json |
|
if [[ -f "$moddir/package.json" ]]; then |
|
local ver |
|
ver=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$moddir/package.json" 2>/dev/null | head -1 || true) |
|
if echo "$ver" | grep -q "4.2.0"; then |
|
warn "$moddir/package.json affiche 4.2.0 — c'est le stub de nettoyage post-exploit !" |
|
elif echo "$ver" | grep -q "4.2.1"; then |
|
alert "$moddir/package.json est la version malveillante 4.2.1 !" |
|
fi |
|
fi |
|
|
|
hit=$((hit + 1)) |
|
done < <(find "$SCAN_DIR" -type d -name "$MALICIOUS_DEP" -path "*/node_modules/*" -print0 2>/dev/null) |
|
|
|
# Vérifier les versions axios installées dans node_modules |
|
while IFS= read -r -d '' axpkg; do |
|
count=$((count + 1)) |
|
if grep -qE "\"version\"[[:space:]]*:[[:space:]]*\"($MALICIOUS_AXIOS_VERSIONS)\"" "$axpkg" 2>/dev/null; then |
|
alert "axios compromis installé : $axpkg" |
|
grep -n "version" "$axpkg" | head -1 | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
done < <(find "$SCAN_DIR" -path "*/node_modules/axios/package.json" -print0 2>/dev/null) |
|
|
|
if [[ $hit -eq 0 ]]; then |
|
ok "Aucun '$MALICIOUS_DEP' ni axios compromis dans les node_modules ($count installations axios vérifiées)" |
|
fi |
|
} |
|
|
|
check_system_iocs() { |
|
section "4/5 Vérification des IOCs système (RAT artifacts)" |
|
|
|
local hit=0 |
|
local platform |
|
platform="$(uname -s 2>/dev/null || echo "Unknown")" |
|
|
|
case "$platform" in |
|
Linux) |
|
for artifact in "${RAT_ARTIFACTS_LINUX[@]}"; do |
|
if [[ -e "$artifact" ]]; then |
|
alert "RAT artifact trouvé : $artifact — SYSTÈME COMPROMIS" |
|
ls -la "$artifact" 2>/dev/null | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
done |
|
;; |
|
Darwin) |
|
for artifact in "${RAT_ARTIFACTS_MACOS[@]}"; do |
|
if [[ -e "$artifact" ]]; then |
|
alert "RAT artifact trouvé : $artifact — SYSTÈME COMPROMIS" |
|
ls -la "$artifact" 2>/dev/null | while read -r line; do |
|
echo -e " ${RED}$line${NC}" |
|
done |
|
hit=$((hit + 1)) |
|
fi |
|
done |
|
for artifact in "${RAT_ARTIFACTS_LINUX[@]}"; do |
|
if [[ -e "$artifact" ]]; then |
|
alert "RAT artifact trouvé : $artifact — SYSTÈME COMPROMIS" |
|
hit=$((hit + 1)) |
|
fi |
|
done |
|
;; |
|
*) |
|
info "Plateforme '$platform' — vérification manuelle recommandée" |
|
info "Windows: vérifier %PROGRAMDATA%\\wt.exe, %TEMP%\\6202033.vbs, %TEMP%\\6202033.ps1" |
|
;; |
|
esac |
|
|
|
# Vérifier /etc/hosts pour le blocage (avant DNS, car host/dig bypasse /etc/hosts) |
|
local hosts_blocked=0 |
|
if [[ -f /etc/hosts ]] && grep -q "$C2_DOMAIN" /etc/hosts 2>/dev/null; then |
|
ok "$C2_DOMAIN est bloqué dans /etc/hosts" |
|
hosts_blocked=1 |
|
fi |
|
|
|
# Vérifier si le domaine C2 est résolvable (seulement si pas déjà bloqué localement) |
|
if [[ $hosts_blocked -eq 0 ]]; then |
|
if command -v host &>/dev/null; then |
|
if host "$C2_DOMAIN" &>/dev/null; then |
|
warn "Le domaine C2 $C2_DOMAIN est résolvable — considérer un blocage DNS/firewall" |
|
else |
|
ok "Le domaine C2 $C2_DOMAIN n'est pas résolvable (bloqué ou down)" |
|
fi |
|
elif command -v dig &>/dev/null; then |
|
if dig +short "$C2_DOMAIN" 2>/dev/null | grep -q .; then |
|
warn "Le domaine C2 $C2_DOMAIN est résolvable — considérer un blocage DNS/firewall" |
|
else |
|
ok "Le domaine C2 $C2_DOMAIN n'est pas résolvable" |
|
fi |
|
elif command -v nslookup &>/dev/null; then |
|
if nslookup "$C2_DOMAIN" &>/dev/null; then |
|
warn "Le domaine C2 $C2_DOMAIN est résolvable" |
|
else |
|
ok "Le domaine C2 $C2_DOMAIN n'est pas résolvable" |
|
fi |
|
else |
|
info "Aucun outil DNS disponible — vérification C2 ignorée" |
|
fi |
|
fi |
|
|
|
# Vérifier les connexions actives vers le C2 |
|
if command -v ss &>/dev/null; then |
|
if ss -tnp 2>/dev/null | grep -q "$C2_IP"; then |
|
alert "Connexion active détectée vers $C2_IP — SYSTÈME COMPROMIS" |
|
hit=$((hit + 1)) |
|
fi |
|
elif command -v netstat &>/dev/null; then |
|
if netstat -tnp 2>/dev/null | grep -q "$C2_IP"; then |
|
alert "Connexion active détectée vers $C2_IP — SYSTÈME COMPROMIS" |
|
hit=$((hit + 1)) |
|
fi |
|
fi |
|
|
|
if [[ $hit -eq 0 ]]; then |
|
ok "Aucun IOC système détecté" |
|
fi |
|
} |
|
|
|
check_npm_cache() { |
|
section "5/5 Vérification du cache npm global" |
|
|
|
local hit=0 |
|
local npm_cache="${NPM_CONFIG_CACHE:-$HOME/.npm}" |
|
|
|
if [[ -d "$npm_cache" ]]; then |
|
info "Scan du cache npm : $npm_cache" |
|
|
|
# Vérifier via npm cache ls (instantané, interroge l'index interne) |
|
if command -v npm &>/dev/null; then |
|
if npm cache ls axios 2>/dev/null | grep -qE "axios-(1\.14\.1|0\.30\.4)"; then |
|
alert "Archive axios compromise dans le cache npm" |
|
info "→ npm cache clean --force" |
|
hit=$((hit + 1)) |
|
fi |
|
if npm cache ls "$MALICIOUS_DEP" 2>/dev/null | grep -q "$MALICIOUS_DEP"; then |
|
alert "'$MALICIOUS_DEP' dans le cache npm" |
|
info "→ npm cache clean --force" |
|
hit=$((hit + 1)) |
|
fi |
|
fi |
|
|
|
# Vérifier les chemins connus directement (pas de find/traversal) |
|
for d in "$npm_cache/$MALICIOUS_DEP" "$npm_cache/_cacache/$MALICIOUS_DEP"; do |
|
if [[ -e "$d" ]]; then |
|
alert "'$MALICIOUS_DEP' trouvé : $d" |
|
info "→ npm cache clean --force" |
|
hit=$((hit + 1)) |
|
fi |
|
done |
|
else |
|
info "Cache npm non trouvé à $npm_cache — ignoré" |
|
fi |
|
|
|
# Vérifier le cache yarn si présent |
|
local yarn_cache |
|
if command -v yarn &>/dev/null; then |
|
yarn_cache="$(yarn cache dir 2>/dev/null || echo "")" |
|
if [[ -n "$yarn_cache" && -d "$yarn_cache" ]]; then |
|
info "Scan du cache yarn : $yarn_cache" |
|
if [[ -d "$yarn_cache/$MALICIOUS_DEP" ]] || [[ -d "$yarn_cache/npm-$MALICIOUS_DEP" ]]; then |
|
alert "'$MALICIOUS_DEP' trouvé dans le cache yarn" |
|
info "→ yarn cache clean" |
|
hit=$((hit + 1)) |
|
fi |
|
fi |
|
fi |
|
|
|
if [[ $hit -eq 0 ]]; then |
|
ok "Aucune trace dans les caches de packages" |
|
fi |
|
} |
|
|
|
# --- Résumé ------------------------------------------------------------------- |
|
|
|
print_summary() { |
|
echo "" |
|
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}" |
|
if [[ $FOUND_ISSUES -gt 0 ]]; then |
|
echo -e "${RED}${BOLD} ✗ COMPROMISSION POTENTIELLE DÉTECTÉE${NC}" |
|
echo "" |
|
echo -e " ${BOLD}Actions immédiates :${NC}" |
|
echo -e " 1. Downgrader axios → ${GREEN}1.14.0${NC} (1.x) ou ${GREEN}0.30.3${NC} (0.x)" |
|
echo -e " 2. Supprimer node_modules/${MALICIOUS_DEP}" |
|
echo -e " 3. Si RAT artifact trouvé → ${RED}machine compromise, rebuild total${NC}" |
|
echo -e " 4. Rotation de TOUS les secrets/tokens/clés accessibles" |
|
echo -e " 5. Auditer les pipelines CI/CD qui ont pu npm install" |
|
echo "" |
|
echo -e " ${BOLD}Lockdown recommandé dans package.json :${NC}" |
|
echo ' {' |
|
echo ' "overrides": { "axios": "1.14.0" },' |
|
echo ' "resolutions": { "axios": "1.14.0" }' |
|
echo ' }' |
|
echo "" |
|
echo -e " ${BOLD}Blocage C2 :${NC}" |
|
echo " echo '0.0.0.0 sfrclak.com' | sudo tee -a /etc/hosts" |
|
echo " sudo iptables -A OUTPUT -d 142.11.206.73 -j DROP" |
|
else |
|
echo -e "${GREEN}${BOLD} ✓ Aucun indicateur de compromission détecté${NC}" |
|
echo "" |
|
echo -e " Versions saines : axios@1.14.0 (1.x), axios@0.30.3 (0.x)" |
|
echo -e " Bonne pratique CI : npm ci --ignore-scripts" |
|
fi |
|
echo -e "${BOLD}════════════════════════════════════════════════════════════════${NC}" |
|
echo "" |
|
} |
|
|
|
# --- Main --------------------------------------------------------------------- |
|
|
|
main() { |
|
banner |
|
|
|
if [[ ! -d "$SCAN_DIR" ]]; then |
|
echo -e "${RED}Erreur: '$SCAN_DIR' n'est pas un répertoire valide${NC}" >&2 |
|
exit 2 |
|
fi |
|
|
|
SCAN_DIR="$(cd "$SCAN_DIR" && pwd)" |
|
info "Scan de : $SCAN_DIR" |
|
info "Date : $(date -u '+%Y-%m-%d %H:%M:%S UTC')" |
|
|
|
check_package_json_files |
|
check_lockfiles |
|
check_node_modules |
|
check_system_iocs |
|
check_npm_cache |
|
print_summary |
|
|
|
exit $FOUND_ISSUES |
|
} |
|
|
|
main "$@" |