TL; DR
En enda npm-utgivare levererade åtta små paket vars namn lästes som vardagliga byggstenar för blockkedje- och plånboksutveckling, base58-utils, abi-encode, eth-dev, arb-kit, layer2-sdk, solana-key-utils, eth-wallet-helpersoch crypto-validate-libVarje modulen innehåller ett fungerande verktyg. Efter verktyget läggs dock till ett självanropande kodblock som körs i samma ögonblick som modulen importeras.
Ungefär 37 sekunder efter importen, avkodar det blocket en nyttolast som skickas inuti paketet förklädd till en testfixtur, skriver den till en dold fil under användarens hemkatalog, registrerar sig själv för omstart varje gång login i Windows, macOS och Linux, och startar det avkodade skriptet som en fristående process. Ingenting med detta utlöses under npm-installationen; det väntar på att koden ska importeras och köras, vilket är ett lugnare ögonblick än installationen.
Nyttalasten är inte ogenomskinlig. Den levereras som vanlig base64, så avkodning av "testfixturen" återställer hela det andra steget: a krypto-plånbok och hemlighetstjuv som väntar på att maskinen ska stå i viloläge, skördar privata nycklar och fröfraser, krypterar varje fynd med en hårdkodad RSA-4096-nyckel och exfiltrerar den genom att fästa den till en offentlig IPFS-lagring, och skickar tillbaka beacon var 12:e timme.
Vi följer klustret som PhantomSyncAlla åtta paket fanns tillgängliga i npm-registret vid tidpunkten för analysen, publicerade under ett enda konto.
| Ekosystem | npm |
| Paket | base58-utils, abi-encode, eth-dev, arb-kit, layer2-sdk, solana-key-utils, eth-wallet-helpers, crypto-validate-lib |
| Plattformar som riktas in sig på | Windows, macOS, Linux |
| Kärnbeteende | Fördröjd importtidsdropper dold som testfixtur; installerar plattformsoberoende persistens |
| nyttolast | Kryptoplånbok och hemlighetstjuv, RSA-4096-kryptering, exfiltrering via offentlig IPFS-pinning |
Attackanatomi
Varje paket ser oansenligt ut vid första genomläsningen. base58-verktyg, till exempel, är några kilobyte av Base58 / Bitcoin-WIF-hjälpkod utan beroende – precis vad namnet utlovar. Den relevanta koden sitter efter modulens modul.exporter, där en läsare som skummar igenom filens topp sannolikt inte kommer att leta: en självanropande funktion som schemalägger sig själv med en timer.
Operationskedjan, rekonstruerad från paketets källkod, går till så här:
1. Package is required() by the host project
2. A self-invoking function schedules a callback ~37,000 ms later (setTimeout)
3. On fire, the callback reads test/fixtures/keypairs.dat (a base64 blob)
4. It base64-decodes that blob into a Node.js script
5. It writes the script to ~/.cache-db/.node-sync/syncd.js (mode 0o700)
6. It installs login-persistence for that script (see below)
7. It spawns "node syncd.js" as a detached processTvå designval sticker ut.
Nyttalasten färdas som en testfixtur. Det andra steget är inte skrivet som uppenbar kod. Det lever i test/fixtures/nyckelpar.dat, en base64-fil vars namn smälter in i ett paket som påstår sig hantera nyckelpar. För en människa som skummar igenom tar-filen ser det ut som exempeldata; för den bifogade koden är det ett skript att avkoda och köra. Själva droppern innehåller ingen nätverksadress – de finns i det andra steget – men det finns ingen ytterligare förvirring: fixturen är ett enda lager av base64, så avkodning av den (base64 -d) återställer hela syncd.js och dess nätverksbeteende. Nästa avsnitt går igenom vad det återställda steget gör.
Detonationen är fördröjd och knuten till import, inte installation. Eftersom utlösaren är kräva () plus en timer på ~37 sekunder istället för en installationshook, kringgår beteendet kontroller som bara tittar på npm installera steg, och fördröjningen varar längre än många kortlivade sandbox- och CI-körningar. När något väl körs är installationen som hämtade paketet sedan länge klar.
När det avkodade skriptet finns på disken kl. ~/.cache-db/.node-sync/syncd.js — en sökväg vald för att läsas som en vanlig cachekatalog — droppern gör att den överlever omstarter på alla tre större plattformar:
- Linux: en cron-post som startar om skriptet. Posten installeras genom att filtrera den befintliga crontabben genom grep -v syncd, vilket har bieffekten att den nya posten utelämnas från en vanlig lista som använder greps för samma namn.
- Windows: en schemalagd uppgift med namnet WinNodeSync, inställd på att spelas om med 12 minuters intervall.
- MacOS: ett launchd-jobb märkt com.apple.syncd, som finns i flera av paketen – en etikett som imiterar en legitim Apple-systemtjänst.
Skriptet startas sedan omedelbart som en fristående process, så det fortsätter att köras efter att importprogrammet avslutas.
Vad det andra steget gör
Eftersom fixturen är ett enda base64-lager avkodas det andra steget tydligt och kan läsas i sin helhet. Alla åtta paket innehåller en av tre varianter av samma skript, vilket identifierar sig i en rubrikkommentar som phantom syncd v3 — topo durmiente (”sovande mullvad”). Dess uppgift är att stjäla material från kryptovalutaplånböcker och utvecklarhemligheter och skicka dem från maskinen. Den körs i dessa steg:
- 1. Vänta tills maskinen är inaktiv. Innan du gör någonting, syncd.js kontrollerar hur länge användaren har varit inaktiv och fortsätter bara efter en viss gräns (cirka 15 minuter) — xprintidle på Linux, ioreg HIDIdleTime på macOS och en PowerShell-fråga vid inaktivitetstid på Windows. Insamling tenderar därför att ske när ingen sitter vid tangentbordet.
- 2. Hämta en fjärrstyrd aktiveringsbrytareSkriptet hämtar en liten konfigurationsfil från en dead-drop innan det agerar. Den primära källan är en GitHub gist raw URL (gist.githubusercontent.com/juang55/…/cfg.txt); skriptet aktiveras bara om den konfigurationen läser aktiv=1Om kärnpunkten inte är tillgänglig används tre hårdkodade IPFS-innehållsidentifierare som hämtas via de offentliga gatewayerna. gateway.pinata.cloud, ipfs.iooch cloudflare-ipfs.comDetta ger operatören en efterhandskontroll för aktivering/avaktivering och en nedmonteringssäker reserv. (Den minsta varianten, levererad i kryptovalidering-bibliotek, eth-plånbokshjälpareoch solana-key-utils, har gist-lagret borttaget och förlitar sig enbart på IPFS-identifierarna.)
- 3. Samla plånboksmaterial och hemligheter. Skriptet går igenom användarens hemkatalog — ~/.config/solana, ~/.ethereum/nyckelbutik, ~/.gjuteri, ~/.hjälm, ~ / .sshoch Desktop/Dokument/Nedladdningar (inklusive mappnamn på spanska), plus ~/.env och shell rc-filer, och motsvarande AppData platser på Windows. Den riktar sig mot Ethereums privata nycklar, Bitcoins WIF-nycklar, BIP-39-fröfraser, Solana-nyckelpar, Ethereums nyckellager-JSON, SSH-nycklar och hemlighetsbärande miljövariabler. Den större varianten (i abi-koda, base58-verktyg, eth-utveckling) innehåller den fullständiga BIP-39-ordboken med 2048 ord och använder reguljära uttryck för att extrahera individuella nycklar och validerade fröfraser från all text den läser; de två mindre varianterna matchar istället filer efter nyckelord (frö, mnemonic, plånbok, metamask, fantom-, huvudbok, trezor, …) och ladda upp hela filer.
- 4. Kryptera och exfiltrera via en offentlig pinningtjänst. Varje fynd är buntat med ett värdfingeravtryck (användarnamn@värdnamn, plattform, tidsstämpel) och krypteras med en hårdkodad RSA-4096 offentlig nyckel inbäddad i skriptet. Den krypterade posten laddas sedan upp genom att fästa den till IPFS via api.pinata.cloud/pinning/pinJSONToIPFS, autentiserad med hårdkodade Pinata API-inloggningsuppgifter. Det finns ingen specialanpassad C2-server att beslagta: den stulna informationen parkeras i offentlig decentraliserad lagring, som operatören kan hämta med hjälp av de resulterande innehållshasharna. Uppladdningar sker med några sekunders slumpmässig jitter, och en lokal logg över tidsstämpel | nyckeltyp | returnerad hash hålls vid ~/.cache-db/.node-sync/.sl.
- 5. Fortsätt och sända med en 12-timmarscykel. Det andra steget återställer sin egen persistens — en cron-post på Linux, en com.apple.syncd launchd-jobbet på macOS och en schemalagd uppgift med namnet WindowsNodeSync på Windows — inställd på att köras om var 12:e timme. Observera att detta är en olika Windows-aktivitetsnamn från den som droppern installerar (WinNodeSync); båda är värda att leta efter.
tidslinje
De åtta paketen publicerades i en tät period den 2026-07-13 och 2026-07-14. Flera har mer än en version; droppern är identisk mellan versionerna, med endast linjeförskjutningar som ändras när storleken på den godartade verktygskoden ovanför ändras.
| Datum | Event |
|---|---|
| 2026-07-13 | De första paketen i klustret visas under en utgivare (solana-key-utils, eth-wallet-helpers, crypto-validate-lib och tidiga versioner av resten) |
| 2026-07-13 → 07-14 | Återstående namn och uppföljningsversioner publicerade; samma bifogade dropper-paket levereras i varje |
| 2026-07-14 | Alla åtta flaggade och analyserade; varje paket kan fortfarande installeras från registret |
Under hela explosionen förändrades utgivarens registerrykte från ungefär neutralt i början av klustret till starkt negativt i takt med att detektioner ackumulerades – en observerbar bieffekt av att paketen flaggades, inte en designad funktion.
Indikatorer för kompromiss
Alla indikatorer nedan bekräftades vara närvarande vid analystillfället — de i tabellerna för "andra steget" genom avkodning test/fixtures/nyckelpar.dat och läser det återvunna syncd.js.
Filer och sökvägar
| Indikator | Roll |
|---|---|
~/.cache-db/.node-sync/syncd.js | Avkodad andra etapp, skriven med mod 0o700 |
~/.cache-db/.node-sync/.sl | Lokal exfiltreringslogg (tidsstämpel | nyckeltyp | IPFS-hash) |
test/fixtures/keypairs.dat | Base64-kodad nyttolast placerad inuti tarballen som en "testfixtur" |
Nätverksinfrastruktur i andra steget (återställd från syncd.js)
| Indikator | Roll |
|---|---|
gist.githubusercontent.com/juang55/b298754cb72942b1cdcf02ccd45cde2f/raw/cfg.txt | Aktivering utan stopp; skript körs bara om konfigurationen läser active=1 |
Qmcqz3w8j4qFQXDAXAxnrdc2oSX3nzBT4NqtpTqL8mr1ga | IPFS-konfigurationsreserv (CID) |
QmdTXoqVmTHY1i4ZWLdLkoQ9YChp5TXPh5cWXwnAYZt5iF | IPFS-konfigurationsreserv (CID) |
QmfJkLU5gdCpqbbqEjWYC2anXW9FmuEeSLLeLiHVJKYUjp | IPFS-konfigurationsreserv (CID) |
gateway.pinata.cloud, ipfs.io, cloudflare-ipfs.com | IPFS-gateways som används för att hämta konfigurationsalternativet |
api.pinata.cloud/pinning/pinJSONToIPFS | Exfiltreringsslutpunkt, stulen data fäst på offentlig IPFS |
| Pinata API-nyckel | 13c766575b9270a9825d, hårdkodad exfiltreringsautentiseringsuppgifter |
Andra stegets samlingsmål (återställda från syncd.js)
| Indikator | Roll |
|---|---|
~/.config/solana, ~/.ethereum/keystore, ~/.foundry, ~/.hardhat, ~/.ssh, ~/.env, shell rc-filer | Kataloger/filer söktes efter nycklar och hemligheter |
AppData\Roaming\Solana, AppData\Local\ethereum\keystore | Sökta Windows-motsvarigheter |
| Artefakttyper som skördats | ETH privata nycklar, Bitcoin WIF, BIP-39 seedphrases, Solana-nyckelpar, Ethereum-nyckellager JSON, SSH-nycklar, hemliga miljövariabler |
Persistensartefakter
| plattform | Indikator |
|---|---|
| Linux | cron-post lanseras syncd.jsinstallerad via crontab filtrerad igenom grep -v syncd |
| Windows | schemalagd uppgift WinNodeSync (droppare) och WindowsNodeSync (andra steget) |
| MacOS | launchd-etikett com.apple.syncd |
| Alla | Det andra steget fortsätter i en 12-timmarscykel |
Behavioral
- Självanropande funktion tillagd efter modul.exporter, schemaläggning a setTimeout på ~37 000 ms vid import.
- barnprocess spawn("nod", ) med den fristående alternativuppsättningen.
- Tomgångsstyrd aktivering i andra steget (xprintidle / ioreg HIDIdleTime / PowerShell-vilotid; ~15-minuters tröskelvärde).
- RSA-4096-kryptering per sökning, sedan HTTPS POST till ett offentligt IPFS-pinning-API.
Paket och versioner (npm, utgivare solbuilder_io)
| Paket | versioner |
|---|---|
base58-utils | 1.0.0, 1.0.1, 1.0.3 |
abi-encode | 1.0.0, 1.0.1, 1.0.2 |
eth-dev | 1.0.0, 1.0.1, 1.0.2 |
arb-kit | 1.0.0, 1.0.1 |
layer2-sdk | 1.0.0, 1.0.1 |
solana-key-utils | 1.0.0 |
eth-wallet-helpers | 1.0.0 |
crypto-validate-lib | 1.0.0 |
Publisher
- solbuilder_io - angel_lopez89[@]proton[.]mig, e-postadress overifierad, inget verifierat källkontrollkonto, inget länkat arkiv.
Attribuering och observerat beteende
De åtta paketen delar ett publiceringskonto och en nyttolast. Varje paket är ett trivialt paket – några kilobyte riktig verktygskod med samma dropper tillagd – publicerat utan ett länkat arkiv och under en overifierad e-postadress av engångstyp. Den enhetligheten, den delade droppsökvägen, de delade persistensetiketterna och den delade nyckelpar.dat Staging-filer är det som binder samman klustret.
Paketen är ovanligt uppriktiga om sitt eget beteende. solana-key-utils innehåller inline-kommentarer som beskriver det bifogade blocket i enkla termer – man märker det FANTOM: osynlig uthållighet, en annan (på spanska) läser Mata ut topografi i bakgrunden, ”kör mullvaden i bakgrunden.” Dessa är kodens egna anteckningar om vad den gör; kampanjnamnet i det här inlägget är hämtat från den första etiketten tillsammans med den falska noden ”sync daemon” (synkroniserad) som persistensmaskineriet imiterar.
Vi beskriver bara vad koden observerbart gör. Namngivningen av paketen – alla krypto-, plånboks- och blockkedjeverktygstermer – indikerar vilken utvecklarpublik som är mest sannolikt att hämta dem vid namn; det fastställer inte i sig vem utgivaren är. Det andra steget var helt återställbart genom att avkoda base64-fixturen, och dess beteende beskrivs ovan: den samlar in plånboksnycklar, seedfraser och hemligheter och exfiltrerar dem, RSA-krypterade, till offentlig IPFS-lagring via Pinata. Ett anmärkningsvärt designval är avsaknaden av en privat C2-server – konfigurationen kommer från en GitHub-gist och IPFS, och stulen data parkeras i offentlig decentraliserad lagring med nyckel via innehållshash, vilka båda är svårare att beslagta än en enda angriparkontrollerad värd. Ingen tidigare offentlig rapportering som matchade detta kluster hittades i skrivande stund.
Påverkan, trender och vägledning för försvarare
Vem som är exponerad. Alla som lade till ett av dessa paket i ett Node-projekt och sedan körde kod som importerar det. Eftersom detonation sker under importtid snarare än installationstid räcker det inte att bara ha paketet närvarande – utan all normal användning som laddar modulen når nyttolasten. Lockelsens namn riktar sig till utvecklare som bygger på Ethereum, Solana, Arbitrum, Layer-2 och allmänna plånboks-/kodningsverktyg – den population som är mest sannolikt att ha exakt de tillgångar som det andra steget letar efter. Alla som körde en av dessa moduler på en maskin som innehöll plånboksnycklar, seedfraser, nyckellager, SSH-nycklar eller ... .env hemligheter bör behandla dessa inloggningsuppgifter som komprometterade och rotera dem.
Två mönster värda att internalisera. Först, nyttolast-som-fixtur: skickar det andra steget som base64 inuti en datafil med ett rimligt namn (test/fixtures/nyckelpar.dat) håller paketets synliga källa ren och placerar det skadliga innehållet i en fil som granskningsverktyg och mänskliga skimläsare ofta behandlar som inert data. För det andra, importtidsfördröjd körning med plattformsoberoende persistensAtt flytta utlösaren från installationskroken, lägga till en timer och sedan spara över cron, schemalagda uppgifter och launchd är ett medvetet steg bort från de mer bullriga installationsskripttekniker som automatiserad registerskanning övervakar noggrant.
Vägledning för försvarare och ansvariga:
- Behandla tillagd kod efteråt modul.exporter som en granskningsprioritet — dropper-logik döljer sig ofta under den "riktiga" moduleytan.
- Anta inte att datafiler är inerta. En base64-blob under test/matcher/ som läses vid körning och avkodas är exekverbar-angränsande; flagga runtime-läsningar av fixturfiler som matar en Funktion /eval/skriv-sedan-rom kedja.
- Jaga efter droppvägen ~/.cache-db/.node-sync/ och för persistensenheterna WinNodeSync (schemalagd uppgift) och com.apple.syncd (launchd) på utvecklarmaskiner som hämtade dessa namn.
- Avisering om nodprocesser som skapar cron-poster, schemalagda uppgifter eller launchd-jobb – legitima bibliotek gör sällan detta vid import.
- Föredra låsfiler och fästa versioner, och granska skillnaden för alla nya små "verktygs"-beroenden, särskilt nollberoendepaket publicerade av overifierade konton utan länkat arkiv.
För registerförsvarare är slutsatsen att övervakning av installationshooken är nödvändig men inte tillräcklig: en importtids-, timerfördröjd dropper som är placerad inuti en datafil kommer att klara en kontroll endast vid installationstid, och persistenssteget är ofta den starkaste observerbara signalen som är kvar att fånga upp.







