TL; DR
Venti pacchetti npm pubblicati in due giorni tra la fine di luglio e l'inizio di agosto 2026 si presentano come librerie di componenti UI interne e utilità backend condivise. Nessuno di essi implementa la funzionalità che pubblicizzano. Ognuno fornisce una classe stub, un README che descrive il pacchetto come interno e un file chiamato _polyfill.js.
La parte interessante è da dove viene richiamato quel file. Non è un hook di installazione — i manifest dichiarano l'assenza di preinstallazione, installazione o postinstallazione. Invece, l'ultima riga di index.js si legge: try { require("./_polyfill"); } catch (_) {}. Il payload viene eseguito la prima volta che il codice dell'applicazione importa il pacchetto. Installazione con --ignore-scripts, il standard consigli per ridurre l'esposizione della catena di fornitura al momento dell'installazione, non lo impedisce.
Che _polyfill.js Il processo consiste nel recuperare un eseguibile specifico della piattaforma da uno dei tre host Cloudflare Workers, scriverlo in una directory temporanea con un nome casuale, contrassegnarlo come eseguibile e avviarlo separatamente dal processo padre. Tale eseguibile è a sua volta un caricatore: enumera i prodotti di sicurezza degli endpoint in base al nome del processo, controlla il nome utente corrente rispetto agli account noti dell'ambiente di analisi, applica patch alle interfacce di telemetria AMSI ed ETW in memoria, installa la persistenza con il nome di un componente audio di Windows e recupera un terzo binario di diversi megabyte. La terza fase arriva crittografata ed è decrittografato ed eseguito in memoria anziché scritto su disco.
Il 1° agosto, circa quattro ore dopo che i pacchetti erano stati confermati come dannosi in seguito a una revisione manuale, diciannove dei venti nomi sono stati rimossi da npm dal loro stesso editore in una sequenza di venticinque secondi. Il 2 agosto uno di questi nomi è stato registrato nuovamente, da un account diverso, con lo stesso codice. Al momento della stesura di questo documento, l'infrastruttura di consegna serviva tutte e tre le fasi.
| personalizzati | 20 nomi npm, circa 74 versioni pubblicate |
| Denominazione delle tecnologie | beaver-ui-* (13), accounts-* (3), più afisha-storybook-default, bcore-bravo-eslint-config, arbocrate-sla-prober-arbocrate-sla-prober-core, a.poltoradnev-package-c |
| grilletto | Importazione del modulo (require), non uno script del ciclo di vita npm |
| Stage 1 | _polyfill.js — recupero e lancio |
| Stage 2 | Caricatore collegato staticamente, impacchettato con UPX su Linux; enumerazione della sicurezza degli endpoint, applicazione di patch AMSI/ETW, persistenza |
| Stage 3 | beacon_linux.bin / beacon_mac.bin / update_win.exe, 4–13 MB; consegnato crittografato, eseguito in memoria |
| Piattaforme | Linux x64 e arm64, macOS universale, Windows x64 |
| Editori | Due account, un unico codice sorgente |
| Stato | 19 dei 20 nomi non sono stati pubblicati dall'operatore; 1 ripubblicato e attivo; infrastruttura di consegna attiva |
Attacco anatomico
Fase 1 — il pacchetto
Ogni membro spedisce la stessa configurazione a tre file. index.js esporta una classe con un dentro() che imposta un valore booleano, un versione(), e un configurare() che unisce un oggetto opzioni. Nient'altro. README.md fornisce una riga di installazione, un richiedere riga e la frase "Pacchetto interno. Vedi CONTRIBUTING.md per le linee guida" — CONTRIBUTO.md non è presente nel file tar.gz.
_polyfill.js È di poco inferiore a 5 KB e si presenta come un normale codice infrastrutturale. Si apre con tre controlli ambientali:
if (process.env.DISABLE_TELEMETRY ||
process.env.ANALYTICS_OPT_OUT ||
process.env.DO_NOT_TRACK)
return;Queste variabili sono controlli di privacy convenzionali. Qui controllano se un file binario viene scaricato ed eseguito. Una macchina che imposta NON TRACCIARE viene completamente saltato.
Successivamente viene un timbro eseguibile una sola volta: un file a /tmp/.analytics_state, o stato_analisi per % TEMP% Su Windows, il cui tempo di modifica viene confrontato con una finestra temporale di 20,357 secondi. All'interno di tale finestra, il modulo restituisce immediatamente un risultato, quindi le importazioni ripetute durante una compilazione producono un solo tentativo di recupero anziché decine.
Gli host di destinazione non sono presenti come valori letterali. Vengono assemblati in fase di esecuzione a partire da frammenti:
["oob-worker.cf102-","baf.workers.d","ev"].join("")
Tre host vengono costruiti in questo modo e mescolati prima dell'uso, quindi l'ordine varia per ogni esecuzione. Il recupero è un HTTPS GET bloccato su IPv4 con un User-Agent of node-fetch/2.6 — un valore plausibile che un processo Node può emettere. Una risposta inferiore a 1,000 byte viene scartata e si tenta con l'host successivo.
Se tutti e tre falliscono, il modulo ricorre al DNS. Emette una query TXT per C. per ottenere un conteggio dei blocchi, quindi le richieste 0. attraverso N. In gruppi di dieci, concatena le risposte e decodifica il risultato in base64. Questo percorso trasporta lo stesso eseguibile attraverso i record DNS TXT, che sopravvive in ambienti in cui il traffico HTTP in uscita viene filtrato ma la risoluzione DNS no.
I byte recuperati vengono scritti su /var/tmp/.cache_<8-hex> su Unix, o %TEMP%\dotnet_diag_<8-hex>.exe su Windows — un nome file che viene letto come un artefatto di diagnostica .NET. Su Unix al file viene assegnata la modalità 0755. L'avvio è uova con distaccato: vero, stdio: “ignora”e unref(), quindi il figlio sopravvive al processo Node che lo ha avviato; su Windows l'invocazione aggiunge windowsHide: true.
Due dettagli completano il quadro. Un aiutante definito come const _l = (…a) => process.stderr.write(“”) viene chiamata ad ogni passaggio del recupero: scrive una stringa vuota e scarta i suoi argomenti, occupando la posizione in cui si troverebbe il logging. E ogni pacchetto spedisce un file di 81 KB a lib/telemetry.js il cui commento nell'intestazione descrive un "SDK di analisi" con "trasporto HTTPS con rotazione e ritentativo degli endpoint" e "individuazione dei servizi basata su DNS per il failover". Nella build attuale non è richiesto da nulla.
Fase 2 — il caricatore
Il binario per Linux x64 è un file ELF collegato staticamente e compresso con UPX 3.96. La versione per macOS è un binario universale che supporta sia x86_64 che arm64; la versione per Windows è un eseguibile console PE32+ semplificato. Tutti e quattro sono stati recuperati ed esaminati staticamente; nessuno è stato eseguito.
Le importazioni e la tabella delle stringhe della build di Windows descrivono chiaramente il suo comportamento. Risolve IsDebuggerPresent, CheckRemoteDebuggerPresente NtQueryInformationProcesse legge SYSTEM\CurrentControlSet\Services\Disk\Enum — un percorso del registro i cui valori identificano i controller del disco virtuale. Chiama GetDiskFreeSpaceExW and GlobalMemoryStatusEx, coerentemente con la verifica che le risorse dell'host corrispondano a un'istanza di analisi di breve durata.
Contiene un elenco di nomi utente: Utente attuale, Amministratore IT, Hong Lee, TESTBOX, hapubws, miloz, timmy, Admin, il malware, campione, virus, Industria XNUMXMolti di questi sono artefatti di ambienti di analisi documentati pubblicamente.
Contiene inoltre un elenco di prodotti per la sicurezza degli endpoint e i relativi nomi di processo, numerati attraverso CreateToolhelp32Snapshot and Process32FirstW/Process32NextW:
- Colpo di folla Falcon (
csfalconservice,CSFalconContainer) - Carbon Black (
cb.exe) - Cylance (
cylancesvc.exe) - Bitdefender (
bdagent.exe) - Kaspersky (
avp.exe,kavfs.exe) - Trend Micro e Deep Security (
coreServiceShell,ds_agent.exe) - FireEye/Trellix (
xagt.exe) - Tanium (
taniumclient.exe) - Hitman Pro (
hmpalert.exe) - Elastico (
elastic-agent.exe,elastic-endpoint,filebeat.exe,winlogbeat.exe) - Wazuh e OSSEC (
wazuh-agent.exe,ossec-agent.exe) - Check Point
- Windows Defender
Sono presenti altri due simboli: AmsiScanBuffer, il punto di ingresso che Windows espone per la scansione del contenuto in memoria, e EtwEventWrite, la funzione tramite cui Event Tracing per Windows registra gli eventi. Entrambe sono bersagli comuni per modifiche in-process che sopprimono la telemetria che tali interfacce produrrebbero altrimenti.
La persistenza appare due volte su Windows. Il binario viene copiato in %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exee due meccanismi vi fanno riferimento: un valore sotto SOFTWARE\Microsoft\Windows\CurrentVersion\Rune un'attività pianificata registrata come
schtasks /Create /F /SC ONLOGON
/TN "\Microsoft\Windows\Multimedia\SystemSoundsService"
/TR "\"%s\""
/RL HIGHESTIl percorso dell'attività, il nome dell'attività e il nome del file eseguibile rispecchiano fedelmente un componente audio autentico di Windows. /RL PIÙ ALTO richiede il livello di privilegio più elevato disponibile.
La build per macOS segue lo stesso schema con nomi appropriati alla piattaforma: scrive un elenco di proprietà su ~/Library/LaunchAgents/com.apple.windowserver.helper.plist e posiziona il suo eseguibile in ~/.local/share/runtime/com.apple.runtime. Controlla per /Applicazioni/VMware Fusion.app and /Libreria/Supporto applicazioni/Strumenti VMware.
La capacità di rete si estende su tre protocolli di trasporto: WININET.dll per HTTPS con un https://%s:%d%s stringa di formato, WS2_32.dll prese e DNSAPI.dll con DnsQuery_A a fianco di a c.%s.%s stringa di formato: la stessa struttura di query con conteggio dei blocchi utilizzata nella prima fase.
La descrizione di cui sopra è ciò che le importazioni, le stringhe e i percorsi del registro del binario mostrano a un'ispezione. Un'analisi multi-motore pubblica dello stesso campione di Windows aggiunge dettagli che una lettura statica di una tabella di importazione non può stabilire da sola, e viene qui attribuita anziché essere riproposta come osservazione diretta. Tale analisi riporta che i riferimenti AMSI ed ETW non sono semplicemente risolti ma anche corretti — AmsiScanBuffer in amsi.dll and EtwEventWrite in ntdll.dll — che sopprime la scansione del contenuto in memoria e il tracciamento degli eventi per il processo. Segnala i controlli anti-analisi come basati sui tempi, oltre a CheckRemoteDebuggerPresente identifica il OttieniContesto del thread chiamata come controllo del breakpoint hardware leggendo i registri di debug. Segnala anche un mutex a istanza singola, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}e nomi ServizioSuoniDiSistema come valore Run-key. Tale valore del mutex non era tra le stringhe recuperate in questo caso — il filtro di estrazione utilizzato escludeva i token esadecimali maiuscoli tra parentesi graffe — ed è registrato nella tabella degli indicatori sulla base di tale analisi pubblica piuttosto che di un'osservazione diretta.
Stage 3
Il caricatore fa riferimento a un ulteriore insieme di percorsi sugli stessi host. Tutti erano raggiungibili durante l'analisi: beacon_linux.bin a 5.3 MB, beacon_mac.bin a 13.3 MB, update_win.exe a 10.3 MB e arm.bin di 4.3 MB. Questi non sono stati recuperati e il loro comportamento è sconosciuto.
Per il percorso Windows, l'analisi pubblica del caricatore descrive come viene gestita la terza fase: viene recuperata tramite WinINet, arriva crittografata e viene decrittografata in memoria ed eseguita in modo riflessivo: memoria allocata, importazioni risolte, rilocazioni elaborate e controllo passato a un nuovo thread tramite Crea discussionePer questo motivo, la terza fase non viene mai scritta sul filesystem, quindi il rilevamento basato su file e l'analisi forense del disco successiva all'incidente non hanno nulla da recuperare da essa. Ciò significa anche che il recupero dei byte dall'host di destinazione produrrebbe testo cifrato, non un eseguibile analizzabile.
timeline
Tutti i timestamp sono in formato UTC e provengono dai metadati del registro npm, che registrano la creazione e la rimozione dei pacchetti indipendentemente da qualsiasi osservatore.
| Quando | Che |
|---|---|
| 2026-07-31 09:25:05 → 09:29:56 | Dieci nomi creati, ciascuno a partire dalla versione 0.0.1, a circa tredici secondi di distanza l'uno dall'altro. |
| 2026-08-01 09:38 → 11:29 | I nomi esistenti salgono attraverso numeri di versione gonfiati, 6.1.9 attraverso 6.1.12, 9.9.9 attraverso 9.9.12, 8.9.3 attraverso 8.9.6, 5.4.6 attraverso 5.4.9. |
| 2026-08-01 13:04:14 → 13:04:18 | Altri nove beaver-ui-* nomi creati in un lampo di quattro secondi, ciascuno pubblicato direttamente su una versione 12.x senza alcuna cronologia precedente. |
| 2026-08-01 ~13:00–14:30 | Versioni classificate come dannose in seguito a revisione manuale. |
| 2026-08-01 17:39:49 → 17:40:14 | Diciannove dei venti nomi non pubblicati in venticinque secondi, in sequenza, a circa 1.2 secondi di distanza l'uno dall'altro. |
| 2026-08-02 18:58:41 | a.poltoradnev-package-c creato di nuovo con un account manutentore diverso, versione 0.0.2. |
| 2026-08-02 19:20:28 | Versione 33.9.5 pubblicato con lo stesso nome. |
| 2026-08-02 20:14 | Tutti e tre gli host di consegna gestiscono le fasi due e tre; il fallback DNS risponde con il conteggio dei chunk. |
Due di queste voci meritano un approfondimento.
L'esplosione delle 13:04 ha messo nove pacchetti nel registro in quattro secondi, ognuno con una singola versione nei dodici e nessun 0.0.1 al di sotto di esso. I numeri di versione su questa scala, con nomi vecchi di un solo giorno, non descrivono una cronologia delle versioni.
La sequenza di rimozione dalla pubblicazione è altrettanto meccanica: diciannove nomi, venticinque secondi, spaziatura uniforme. npm registra questi eventi come normali eventi di rimozione dalla pubblicazione, anziché come il segnaposto lasciato da una rimozione avviata dal registro, il che attribuisce l'azione all'account di pubblicazione anziché al registro.
Indicatori di compromesso
Gli host e i percorsi sono stati resi inutilizzabili. Non risolverli né recuperarli da un sistema di produzione.
| Tipo | Valore |
|---|---|
| Host della consegna | oob-worker[.]cf99-9b3[.]workers[.]dev |
| Host della consegna | oob-worker[.]cf102-baf[.]workers[.]dev |
| Host della consegna | oob-worker[.]cf103-070[.]workers[.]dev |
| Trasporto DNS | tin[.]dl[.]well1[.]site (Linux x64), tina[.] (arm64), ldr[.] (Mac OS), win[.] (Windows) — conteggio dei blocchi a c.<domain>, dati a <n>.<domain> |
| Percorsi di Fase 2 | /pkg/package, /pkg/package-arm64, /pkg/loader_mac, /pkg/package.exe |
| Percorsi di Fase 3 | /pkg/beacon_linux.bin, /pkg/beacon_mac.bin, /pkg/update_win.exe, /pkg/arm.bin |
| Percorso di rilascio (Unix) | /var/tmp/.cache_<8-hex> |
| Percorso di rilascio (Windows) | %TEMP%\dotnet_diag_<8-hex>.exe |
| Persistenza (Windows) | %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe; compito \Microsoft\Windows\Multimedia\SystemSoundsServiceValore chiave di esecuzione SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SystemSoundsService |
| Mutex (Windows) | Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9} |
| Persistenza (macOS) | ~/Library/LaunchAgents/com.apple.windowserver.helper.plist; binario a ~/.local/share/runtime/com.apple.runtime |
| Francobollo a esecuzione singola | /tmp/.analytics_state, %TEMP%\analytics_state (finestra temporale di 20,357 secondi) |
| Richiesta di firma | HTTPS GET, IPv4-pinned, User-Agent: node-fetch/2.6 |
| File di Fase 1 | _polyfill.js, SHA-256 392dc9ec85042ce1796bfa9b8c7bf6e05c7710579aab8b7b1eb73788683883af |
| File non referenziato | lib/telemetry.js, SHA-256 2266c9b8e199daebe31a9afcb61d9b0d76706b63c92312fc8223d22de42e63c7 |
| Fase 2 (Linux x64) | SHA-256 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3 |
| Fase 2 (Linux arm64) | SHA-256 aafc37589c0d21fb729761a1579ca3b278b95bce152aacfa29243ffd27f5eeb7 |
| Fase 2 (macOS) | SHA-256 c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00 |
| Fase 2 (Windows) | SHA-256 d632c2c0c7801c3da57cf849fe4b4ca8351d9a7fe73c31050bf5dbcc96a16791 |
Nomi dei pacchetti
Attribuzione e comportamento osservato
Sono coinvolti due account npm. I diciannove nomi non pubblicati sono stati pubblicati da un account; a.poltoradnev-package-c È stato registrato nuovamente il 2 agosto con un nome account e un dominio email diversi. Il codice ha la stessa origine in entrambi i casi e la seconda registrazione presenta gli stessi server di consegna, gli stessi domini DNS di fallback, la stessa struttura del percorso di consegna e lo stesso timbro "run-once".
La coppia ripubblicata mostra anche come è stato assemblato. Versioni 0.0.2 and 33.9.5 sono identici byte per byte a parte la stringa della versione in pacchetto.json index.js spedito come 0.0.2 dichiara const VERSIONE = “33.9.5”Entrambi i manufatti sono stati tagliati da un unico albero e numerati successivamente. Non c'è 0.0.2 storia dello sviluppo per il 0.0.2 rilascio per rappresentare.
La numerazione delle versioni sull'intero set segue la stessa logica. I nomi dei pacchetti in questa campagna sembrano cose che esisterebbero all'interno di un'organizzazione piuttosto che su un registro pubblico: un set di componenti dell'interfaccia utente, una configurazione ESLint, un prober SLA, un pacchetto che prende il nome da una persona. Quando una build risolve una dipendenza per nome e consulta un registro pubblico insieme a uno privato, il numero di versione più alto è quello che vince. Un nome creato a 0.0.1 lunedì e portando 12.9.6 Martedì è la data ideale per quel confronto.
Le due ondate di pubblicazione differiscono anche in un modo che suggerisce un'iterazione. Il codice precedente ha denominato il suo file di payload setup.js e ha elencato cinque host di consegna; il file corrente si chiama _polyfill.js e ne elenca tre. La versione precedente rimuoveva il file rilasciato cinque secondi dopo l'avvio, lasciando il processo in esecuzione da un inode eliminato; la versione attuale non lo fa. Il non referenziato lib/telemetry.js rimane nella palla di catrame su entrambi, ancora spedito, non più cablato.
Un'osservazione sugli interruttori di sicurezza è opportuno precisare primacisely, perché contrasta con un'interpretazione comune. DISABILITA_TELEMETRIA, ANALISI_DISATTIVAe NON TRACCIARE vengono rispettati e controllati prima che accada qualsiasi altra cosa. Un ricercatore il cui ambiente di analisi imposta uno qualsiasi di questi parametri osserva un pacchetto che non fa assolutamente nulla.
Impatto e cosa possono fare i difensori
Le conseguenze pratiche del meccanismo di attivazione in fase di importazione sono limitate e meritano di essere illustrate separatamente. npm install –ignore-scripts impedisce l'esecuzione degli script del ciclo di vita, ed è un consiglio valido. In questo caso non ha effetto, perché questo codice non utilizza uno script del ciclo di vita. Il payload viene eseguito quando il codice dell'applicazione, un'esecuzione di test, un bundler o un linter importano per la prima volta il modulo. Su un runner CI che installa e poi compila immediatamente, i due eventi sono distanziati di pochi secondi.
Nella caccia, i segnali duraturi sono di natura strutturale piuttosto che letterale. Gli host, il nome del file e i percorsi di rilascio cambieranno; queste forme sono più difficili da variare:
- Un punto di ingresso del modulo la cui dichiarazione finale è un try { require(“./x”); } catch (_) {} attorno a un file che non fa parte dell'API esportata. Un pacchetto con un polyfill autentico lo importa all'inizio del file e ne utilizza il risultato. Ignorare ogni errore proveniente da un'importazione in fondo al file non è una gestione degli errori.
- Stringhe host assemblate da .giuntura("") da una serie di frammenti. Un codice legittimo non ha motivo di suddividere un nome host tra gli elementi di un array.
- Un pacchetto le cui dipendenze dichiarate sono vuote ma che raggiunge https, dnse processo_figlio al momento dell'importazione. Il recupero e l'esecuzione in un pacchetto che pubblicizza un componente dell'interfaccia utente costituiscono l'intera scoperta.
- Numeri di versione non coerenti con l'età del pacchetto. Un nome pubblicato per la prima volta giorni fa in una versione principale a due cifre, senza rilasci intermedi, descrive qualcosa di diverso dal suo sviluppo.
- Scrive a / Var / tmp or % TEMP% seguita da una chmod alle 07:55 e uno spawn separato. La sequenza è breve, ordinata e raramente benigna nel suo insieme.
Sui sistemi host Windows sono presenti due controlli economici e affidabili. Gli artefatti di persistenza sono stringhe fisse e nessuno di essi appartiene a un sistema pulito: un'attività pianificata a \Microsoft\Windows\Multimedia\SystemSoundsService, un valore Run-key denominato ServizioSuoniDiSistemae un eseguibile con quel nome sotto %APPDATA%\Microsoft\Windows\Themes\. Il servizio audio di Windows autentico non risiede in nessuno di questi posti. In secondo luogo, il caricatore utilizza un mutex fisso a istanza singola, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}La sua presenza su un host è un indicatore e, poiché il caricatore considera un mutex esistente come "già in esecuzione", la sua pre-creazione rappresenta una valida soluzione temporanea in attesa della risoluzione del problema.
Il rilevamento nella terza fase è un problema più complesso e merita di essere chiarito. Secondo le analisi pubbliche, il file viene decrittografato e mappato in memoria anziché scritto su disco, pertanto le scansioni dei file e le analisi forensi del disco non lo rileveranno. Ciò che rimane osservabile è l'impronta del loader stesso: le voci di persistenza, il mutex e le richieste in uscita verso gli host di distribuzione o i domini DNS-TXT.
Per gli ambienti che risolvono le dipendenze sia da un registro privato che da uno pubblico, la soluzione rimane invariata: limitare l'ambito dei nomi interni in modo che un nome pubblico non possa soddisfarli e bloccare la risoluzione al registro interno per tali ambiti. Il comportamento di inflazione della versione descritto sopra funziona solo quando una versione pubblica superiore può prevalere su un nome.
Infine, vale la pena leggere attentamente la nota di rimozione, piuttosto che considerarla una conclusione. Diciannove nomi sono stati rimossi dal registro in venticinque secondi e uno è tornato il giorno successivo con un nuovo account, senza che l'infrastruttura di distribuzione venisse intaccata. La rimozione dei pacchetti non equivale alla rimozione dell'operazione; gli host hanno continuato a gestire ogni fase mentre questo documento veniva redatto. Qualsiasi albero di dipendenza che abbia risolto uno di questi nomi tra il 31 luglio e il 1° agosto deve essere considerato come avente avuto la possibilità di eseguire la prima fase, indipendentemente dal fatto che il nome esista ancora o meno.
Referenze
- Analisi multi-motore del caricatore di Windows — fonte per le informazioni relative al patching AMSI/ETW, all'esecuzione in memoria riflessiva e ai dettagli sui mutex citati sopra.
- documentazione degli script npm — gli eventi del ciclo di vita che questa campagna non utilizza.
- documentazione di installazione di npm — copre –Ignore-scripts e la sua portata.







