TL; DR
Un grup de cinci pachete npm, publicat pe două numere de cont, a expediat un postinstall hook care citește acreditările cloud de la gazdă și le expediază în afara sistemului. Pachetele poartă nume fantomă și pirată — coral-wraith, ecto-corsair-whisper-6f3b9, ecto-corsair-flag-x9m4, ecto-rust-read-f3a9c1, ecto-nightly-spirit — și serializează tot ce colecționează într-un fals ecto_module: Manifestul YAML înainte de a-l transmite. Urmărim clusterul ca Ectoplasma.
Sarcina utilă rulează doar atunci când detectează un anumit mediu: o gazdă al cărei nume este un Șir hexadecimal de 12 caractere și un director de lucru sub /app/node_modules — forma unei versiuni containerizate sau a unui lucrător CI. Când acea poartă trece, hook-ul interoghează Serviciul de metadate al instanței AWS (IMDSv2) pentru acreditările rolului IAM, enumeră Manager de secrete AWS în trei regiuni, elimină variabile de mediu, citește fișiere sub /appși caută șiruri de caractere pentru capturarea steagului. Apoi exfiltrează rezultatul în două moduri: un beacon către un webhook.site colector și un manifest PUT către un endpoint cu IP brut, cu o listă de opțiuni de rezervă localhost-first.
Descrierile ulterioare ale pachetelor menționează „Payload CTF for verdaccio supply-chain testing” (sarcină utilă CTF pentru testarea lanțului de aprovizionare verdaccio). Raportăm această autodescriere ca un fapt observabil. Comportamentul în sine - ieșire live către o adresă IP publică, citiri reale ale acreditărilor IMDS, apeluri reale Secrets Manager - este ceea ce este, indiferent de etichetă și este motivul pentru care aceste versiuni au fost clasificate ca fiind rău intenționate.
Un nume din grup, coral-wraith, nu s-a oprit la o singură lansare. A republicat în succesiune rapidă prin zeci de versiuni în doar câteva ore — 1.0.0 urcând spre 6.0.0 — și o serie anterioară cu același nume folosise umflate 9999.0.x numerele de versiune, forma clasică a unui încercare de confuzie între dependențeÎn această perioadă de fluctuație, sarcina utilă a crescut vizibil: de la un semnal de alarmă unic pentru enumerarea gazdelor la un pivot complet al acreditărilor AWS, încorporat în verificări de mediu care îl mențin discret în afara țintei sale.
| Pachete | 5 nume; coral-wraith singur republicat în zeci de versiuni |
| SnowGem | NPM |
| Instalați vectorul | postinstall scriptul ciclului de viață |
| Ținta primară | Acreditările rolului AWS IAM + valorile secrete Secrets Manager, variabilele de mediu /app fișiere |
| Exfil | webhook.site beacon + IP brut C2 PUT |
| Poarta de declanșare | Nume de gazdă cu 12 hexadecimi + /app/node_modules cwd, plus o verificare a mediului care suprimă sarcina utilă în afara acelui context |
| Severitate | înalt — divulgarea acreditărilor în cloud și a secretelor gestionate de la medii de compilare și rulare containerizate |
Anatomia atacului
Fiecare pachet din cluster este construit în același mod: un pachet aproape gol index.js (module.exports = {}), o linie pachet.json scenariu — „postinstalare”: „nodul postinstalare.js” — și sarcina utilă în postinstall.jsInstalarea pachetului este suficientă pentru a rula hook-ul; nu este necesară nicio importare sau apelare.
Poarta de țintire. Înainte de a face orice, sarcina utilă a familiei ecto își verifică împrejurimile:
function isAppWorker(): host = os.hostname() if host does NOT match /^[0-9a-f]{12}$/ -> exit if cwd does NOT contain "/app/node_modules" -> exit if cwd contains "/tmp/npm-safe" -> exit otherwise -> proceed Un nume de gazdă cu 12 hexadecimi este forma implicită pe care Docker o atribuie unui container și /app/node_modules este o cale convențională de instalare în container. A treia clauză se închide dacă calea arată ca un director de extragere în sandbox. Efectul net este că sarcina utilă rămâne inactivă pe laptopul unui dezvoltator sau într-un sandbox de analiză și se activează doar în interiorul unui worker de compilare sau runtime containerizat - tipul de mediu cel mai probabil să transporte acreditări live în cloud. Cel mai vechi pachet din cluster, fantomă de coral, nu are o astfel de poartă și rulează colecția sa (mai simplă) necondiționat.
ColectieCând poarta trece, cârligul iese prin execFileSync(“/bin/sh”, [“-c”, …]) și execută o singură comandă compusă care, în ordinea:
1. PUT /latest/api/token to 169.254.169.254 (IMDSv2 token request) 2. GET .../iam/security-credentials/ (IAM role name) 3. GET .../iam/security-credentials/<role> (temporary credentials) 4. dump env | sort (environment variables) 5. list /app (excl. node_modules) + cat first 15 (application files) 6. aws secretsmanager list-secrets (us-east-1, eu-west-1, eu-central-1) 7. scrape readable files for HTB{...} (capture-the-flag strings) Pașii 1–3 sunt o recuperare IMDSv2 standard: solicitați un token de sesiune, apoi atașați-l ca X-aws-ec2-metadata-token antet pentru a extrage rolul IAM al instanței și cheile de acces temporare ale rolului respectiv. Opțiunea de a implementa IMDSv2 în locul IMDSv1, versiunea mai simplă și neautentificată GET este demn de remarcat - înseamnă că sarcina utilă funcționează chiar și pe instanțele configurate să necesite acces la metadate bazate pe token-uri, ceea ce reprezintă consolidarea recomandată de AWS. Acreditările returnate de pasul 3 sunt de scurtă durată. AccessKeyId/SecretAccessKey/Simbol triplete limitate la rolul instanței; orice poate face acel rol, deținătorul acelor chei poate face pe durata de viață a acreditării.
Pașii 4–6 lărgesc perspectiva. env dump-ul capturează tot ce a moștenit procesul de compilare sau de execuție — în practică, aici se află cel mai adesea token-urile de registry, șirurile de conexiune la baza de date și cheile API. / aplicație citirea prin parcurgere a fișierelor citește până la cincisprezece fișiere de aplicație în afara nod_module, care poate afișa configurația suprafeței, .și V fișiere sau sursă. Apeluri la pasul 6 aws secretsmanager listă-secrete în trei regiuni; acreditările extrase în pașii 1-3 sunt exact ceea ce autentifică acele apeluri, astfel încât citirea IMDS și enumerarea Secrets Manager se unesc într-o singură escaladare: rol instanță → inventar secret gestionat. Pasul 7 este o aluzie la cadrul de captură a steagului - când un HTB{…} Dacă se găsește flag-ul, acesta este trimis singur, altfel blob-ul brut colectat este împărțit în patru bucăți și trimis.
Versiunile ulterioare din cluster duc escalarea mai departe. În loc să se oprească la un inventar, acestea analizează răspunsul IMDS, exportă cheile temporare ca AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN variabile de mediu, confirmați identitatea cu aws sts get-caller-identity, și apoi parcurge fiecare secret returnat de listă-secrete apel aws secretsmanager get-secret-value pe fiecare — recuperând conținutul secret, nu doar numele lor. Aceleași versiuni citesc și fișiere binare cu steaguri substituite de proces (/citește flag și prieteni) și încercați o rulare de încărcare împotriva oricărui proiect Rust găsit sub / aplicație, extinzând recolta dincolo de acreditările în cloud, la orice expune mediul de compilare.
Aceste versiuni ulterioare se auto-protejează mai agresiv. Pe lângă numele de gazdă cu 12 hexadecimi și /app/node_modules verificări, sarcina utilă inspectează configurația registrului de pachete active și calea directorului de lucru și se închide silențios atunci când acestea indică un context de analiză sau oglindă, mai degrabă decât o țintă activă. Efectul combinat este o sarcină utilă care nu face nimic observabil în majoritatea mediilor de inspecție și își rulează întreaga colecție doar acolo unde consideră că se află pe o gazdă containerizată autentică.
ExfiltrareaDatele colectate părăsesc gazda pe două canale. Mai întâi, un beacon POST la un fix webhook.site colector, care conține numele gazdei, UID-ul numeric, directorul de lucru și până la 120 KB de date colectate. În al doilea rând, datele sunt incluse într-un „manifest de modul” YAML fals și PUT la /api/module/ pe un server țintă:
ecto_module: name: "<flag-or-chunk-0>" version: "1.0.0" power_level: "<chunk-1>" ship_deck: "<chunk-2>" cargo_hold: "<chunk-3>" Numele câmpurilor manifest (nivel_de_putere, punte_navă, cală de marfă) sunt decorațiuni — datele furate se află în interiorul valorilor de tip șir, motiv pentru care un monitor de rețea vede ceea ce pare a fi o încărcare benignă a manifestului de registru al pachetelor, mai degrabă decât o descărcare evidentă de date. Canalul beacon transportă mai multe: POST corp la webhook.site include numele gazdei, UID-ul numeric, directorul de lucru și până la 120 KB din blob-ul colectat, astfel încât chiar și un singur beacon reușit oferă o imagine completă. webhook.site este un serviciu gratuit de inspecție a cererilor; utilizarea sa ca și colector înseamnă că operatorul nu trebuie niciodată să își instaleze propria infrastructură de recepție pentru canalul respectiv, iar cererile înregistrate persistă în coșul de gunoi al serviciului.
Manifestul PUT parcurge o listă de rezervă care începe cu mai multe 127.0.0.1/localhost porturi și apoi ajunge la trei adrese publice din `154.57.164.0/24` interval, oprindu-se la primul punct final care răspunde cu statusul 2xx. Ordonarea localhost-first este consistentă cu autodescrierea „verdaccio testing” (un registru local pe loopback), dar opțiunile de rezervă cu IP public înseamnă că datele părăsesc gazda ori de câte ori loopback-ul nu ascultă - adică, pe orice mașină care nu este propria platformă de testare a autorului.
Companiei
Clusterul prezintă o creștere incrementală a capacității, mai degrabă decât o singură scădere. Îl ordonăm după comportamentul observat, nu după ceasul de zi al publicării:
| Etapă | Pachete / Versiuni | Comportament |
|---|---|---|
| Cursă timpurie | coral-wraith 9999.0.x | Numere de versiune umflate, compatibile cu o încercare de confuzie a dependenței; enumerare și exfil la momentul instalării |
| Sămânţă | coral-wraith 1.0.0 | postinstalarea colectează fișiere id/env/flag; PUT unic către 154[.]57[.]164[.]71:30782marker ECT-472839 |
| Iterație rapidă | coral-wraith 1.0.1 → 6.0.0 | Zeci de eliberări în câteva ore; sarcina utilă câștigă isAppWorker() poartă, extragerea acreditărilor IMDSv2, complet get-secret-value pivot, verificarea mediului registry/cale și markere dual sink |
| Nume paralele | ecto-corsair-whisper-6f3b9 1.0.14–1.0.18 | Aceeași sarcină utilă cu portiță; webhook.site listă de beacon și multi-endpoint backback |
| variante | ecto-rust-read-f3a9c1 1.0.1–1.0.2 | Adaugă markeri de chiuvetă suplimentari ECT-987654, ECT-654321, ECT-839201 |
| variante | ecto-corsair-flag-x9m4 1.0.0, ecto-nightly-spirit 1.1.0 | Aceeași sarcină utilă cu porți, același C2 și far |
Trăsătura definitorie a clusterului este ritmul său de publicare: în loc de un pachet și o singură versiune, același nume este republicat iar și iar în succesiune rapidă, fiecare versiune fiind o mică variație a ultimei, alături de o mână de versiuni similare cu nume diferite, care poartă aceeași sarcină utilă. În cadrul șoaptă familie, codul este împărțit în două amprente apropiate — un set declanșează două detectări critice, altul trei (un senzor suplimentar de citire a fișierelor) — dar ambele se rezolvă la aceeași sarcină utilă; diferența este deriva de cod, nu o bifurcație comportamentală. Versiuni de șoaptă dincolo de intervalul analizat (până la cel puțin 1.0.25 la momentul scrierii) au fost observate în direct în registru, iar fantomă de coral numele a continuat să urce propria scară de versiune peste aceeași fereastră.
Indicatori de compromis
Toți indicatorii de mai jos au fost extrași din sursa pachetului de pe disc. Indicatorii de rețea sunt dezactivați.
Reţea
| Indicator | Rol |
|---|---|
hxxp://154[.]57[.]164[.]71:30782 | Ținta C2 PUT (coral-wraith) |
hxxp://154[.]57[.]164[.]80:30543 | C2 PUT alternativ (ecto-*) |
hxxp://154[.]57[.]164[.]82:31250 | C2 PUT alternativ (ecto-*) |
hxxp://154[.]57[.]164[.]71:31289 | C2 PUT alternativ (ecto-*) |
hxxps://webhook[.]site/602a4c72-7033-4e28-92ea-dc66e59206e5 | Colector de balize |
169[.]254[.]169[.]254/latest/... | Citirea acreditărilor IMDSv2 (metadate AWS pe partea țintă) |
Comportamental/fișier
| Indicator | Rol |
|---|---|
"postinstall": "node postinstall.js" | Instalați vectorul |
ecto_module: YAML cu power_level / ship_deck / cargo_hold chei | Schema manifestului Exfil |
Marcaje de chiuvetă ECT-472839, ECT-987654, ECT-654321, ECT-839201 | Segmentul de cale C2 /api/modules/<marker> |
isAppWorker() poartă: gazdă /^[0-9a-f]{12}$/, cwd conține /app/node_modules | Condiție de activare |
aws secretsmanager list-secrets peste us-east-1, eu-west-1, eu-central-1 | Enumerarea secretelor |
HTB{...} extragerea expresiilor regulate | Recolta de capturare a steagului |
Hash-uri de fișiere (sha256, capturate la momentul analizei)
| Fișier | sha256 |
|---|---|
coral-wraith/postinstall.js | ce5ff035cfdfed1d0015446424b352c27b66bcb77e9fdb0a51e4245199146824 |
ecto-corsair-whisper-6f3b9 1.0.18/postinstall.js | b58432acba376aa6976f0490d9a1c04257ccdbc856d8390260c50322d63e31c3 |
Atribuire și comportament observat
Cele cinci nume de pachete au fost publicate sub două handle-uri de cont npm, dar au suficientă infrastructură în comun pentru a fi tratate ca un singur cluster: același ecto_module schema manifestă, aceeași ECT-472839 marker principal de chiuvetă, același webhook.site ID-ul colectorului și punctele finale C2 în același Blocul 154.57.164.0/24`. Pachetul de semințe (`fantomă de corali`, mai simplă și fără porți) și ecto-familia cu porți sunt astfel interpretate ca iterații pe un singur set de instrumente, mai degrabă decât ca eforturi independente.
Pachetele se descriu, în versiunile ulterioare, ca fiind „Sarcină utilă CTF pentru testarea lanțului de aprovizionare Verdaccio.” Prezentăm această etichetă ca pe un fapt observabil și nu o reiterăm ca pe o constatare despre scop. Ceea ce face codul este lipsit de ambiguitate și independent de modul în care este etichetat: citește acreditările rolului IAM din serviciul de metadate al instanței, enumeră secretele gestionate în trei regiuni AWS și transmite rezultatele către o adresă IP publică și un colector de webhook-uri terțe. Un sistem de testare autentic, doar pentru loopback, nu ar avea nevoie de lista de adrese IP publice de rezervă, de citirile de acreditări IMDS sau de apelurile Secrets Manager între regiuni. Deoarece ieșirea și acoperirea acreditărilor sunt reale, versiunile cu porți au fost clasificate ca fiind rău intenționate.
Poarta „doar pentru containere” este cea mai notabilă caracteristică din punct de vedere operațional. Este atât o măsură de evitare — păstrarea tăcerii pe laptopuri și în sandbox-urile de analiză — cât și o măsură de direcționare, declanșându-se doar acolo unde este cel mai probabil să fie prezent un rol IAM real și secrete live. Analiștii care rulează aceste pachete într-un sandbox generic nu ar observa nimic; comportamentul se manifestă doar sub un nume de gazdă în stil Docker și o cale de instalare în container.
Impact, tendințe și îndrumări pentru apărători
Expunerea aici constă în divulgarea acreditărilor cloud și a secretelor în interiorul containerelor de compilare și de execuție. O acreditare de rol IAM extrasă din IMDS conține permisiunile pe care le deține rolul respectiv; secretsmanager:ListSecrets (și orice continuare GetSecretValue) extinde acest lucru la secretele stocate ale aplicațiilor. Dump-urile variabilelor de mediu conțin frecvent token-uri de registry, adrese URL ale bazei de date și chei API. Într-un context CI sau container - exact ceea ce selectează poarta - o singură instalare tranzitivă a unuia dintre aceste pachete este suficientă pentru a divulga acel material.
Ectoplasm se încadrează într-un tipar pe care îl observăm în continuare: sarcini utile în timpul instalării care accesează metadate în cloud și secrete gestionate în loc de fișiere locale și care se autodeclanșează pentru a se declanșa doar în medii cu valoare ridicată. Urmează două observații defensive.
- Forma este detectabilăUn hook de instalare npm/PyPI al cărui graf de apeluri ajunge atât la un API cloud secrets (manager de secrete AWS, secrete gcloud, seiful de chei az) sau adresa IMDS și un receptor de ieșire din rețea este un model îngust, cu semnal ridicat — aproape niciodată nu apare într-un script legitim al ciclului de viață. Analiza statică a fluxului îl poate semnala fără a depinde de vreun domeniu sau IP specific.
- Întărirea mediului îl tocește. Aplicarea IMDSv2 cu o limită de salt de 1 împiedică accesul la metadatele instanței de către sarcinile de lucru ale containerelor; definirea rolurilor IAM la cel mai mic privilegiu limitează raza de explozie a oricărei acreditări care are scurgeri; și rularea instalărilor cu –Ignore-scripturi În CI, vectorul install-hook este eliminat complet pentru pachetele care nu îl necesită.
Pentru apărători, verificările practice sunt: alerta privind conexiunile de ieșire de la containere build/CI către IP-uri publice nepermise în timpul npm install; fiți atenți la accesul IMDS provenit din scripturile ciclului de viață al pachetului; și tratați orice hook de instalare care se trimite către o interfață CLI în cloud ca fiind suspect până la proba contrarie.
Două note suplimentare specifice acestui cluster. În primul rând, deoarece activarea este limitată la mediile containerizate, un pachet care pare inert atunci când este verificat pe o stație de lucru poate fi în continuare activ în producție - verificarea trebuie să reproducă numele de gazdă al containerului și condițiile căii sau să citească direct sursa, în loc să se bazeze pe „L-am instalat și nu s-a întâmplat nimic”. În al doilea rând, utilizarea unui serviciu public de inspecție a cererilor ca și colector de beacon înseamnă că o parte din datele exfiltrate pot fi recuperabile pentru răspunsul la incidente: o organizație care găsește unul dintre aceste pachete în arborele său de dependențe poate înțelege ce ar fi conținut un beacon de succes din logica de colectare a sarcinii utile și ar trebui să rotească orice acreditări de rol IAM, token-uri de registry și secrete gestionate care erau accesibile din mediul de compilare sau de execuție afectat. Rotația acreditărilor, nu eliminarea pachetului, este remedierea operativă odată ce o instalare a fost executată.





