Raktų medžiotojas: kaip Shai-Hulud nutekėjimas pavertė CI/CD paslapčių vagystę prieinamu įrankiu.
Raktų medžiotojas: kaip Shai-Hulud nutekėjimas pavertė CI/CD paslapčių vagystę prieinamu įrankiu.
Serveris veikia, nginx aptarnauja užklausas, fail2ban blokuoja botus, nftables uždaro viską ko nereikia. GitHub sukonfigūruotas self-hosted runner — diegimas vienu paspaudimu. Patogu. Ir kaip tik per šį runner kažkas skaito jūsų CI/CD proceso atmintį, ištraukia SSH raktus, tokenus ir aplinkos paslaptis, supakuoja viską į šifruotą archyvą — ir dingsta. Be nė vienos eilutės jūsų žurnaluose.
Tai ne hipotetinis scenarijus. Tai yra tai, ką geba daryti Shai-Hulud — ir dabar jo šaltinio kodas yra viešai prieinamas.
KAM SKIRTAS ŠITAS STRAIPSNIS
Straipsnis aktualus, jei jūsų serveryje yra bent vienas iš šių dalykų:
— self-hosted GitHub Actions runner
— kūrėjai su tiesioginiu SSH prisijungimu prie serverio
— diegimas per CI/CD su paslaptimis aplinkos kintamuosiuose
— deploy SSH raktai ~/.ssh/ kataloge serveryje
Jei turite švarų production serverį — nginx, duomenų bazė, fail2ban, be runner ir be tiesioginės kūrėjų prieigos — grėsmė minimali. Bet patikrinimo skyrių vis tiek verta perskaityti: SSH raktų kompromitavimas ir paslapčių nutekėjimas liečia bet kurį serverį.
KAS TIESIOGIAI LIEČIA SISTEMOS ADMINISTRATORIUS
Shai-Hulud pirmiausia skirtas atakuoti kūrėjus. Tačiau keletas dalykų smogia būtent į serverio pusę.
SSH raktai. Framework’as surenka viską iš ~/.ssh/ — įskaitant deploy raktus, suteikiančius prieigą prie production. Jei sukompromituota kūrėjo darbo stotis, kurios savininkas turi raktą nuo jūsų serverio — serveris irgi pavojuje. SSH raktų rotacija visuose serveriuose po incidento yra privaloma.
Persistence per systemd. Linux sistemoje Shai-Hulud įsitvirtina per naudotojo lygio systemd unit’ą ~/.config/systemd/user/ kataloge. Nereikia root teisių, nematomas paprastame systemctl status. Jei serveryje veikia sukompromituota paskyra — unit’as tyliai dirba savo darbą.
Deadman switch. Atšaukus GitHub tokeną prieš tai neišjungus persistence — naudotojo namų katalogas ištrinamas. Jei runner’is veikia atskiroje paskyroje, prarandami konfigūracijos failai, raktai, istorija.
Self-hosted runner kaip pagrindinis įėjimo taškas. Jei runner’is veikia tame pačiame serveryje kaip ir production — runner’io kompromitavimas reiškia viso aplinkos kompromitavimą: kintamųjų, paslapčių, prieigos prie duomenų bazės, vidinių paslaugų.
KAS NUTIKO
TeamPCP grupė, atsakinga už atakų seriją prieš npm ir PyPI, išleido savo pagrindinio įrankio šaltinio kodą GitHub platformoje. Saugykla buvo greitai pašalinta, tačiau forkai jau buvo paplitę tinkle. OX Security ir Datadog Security Labs išsaugojo kopiją ir išanalizavo ją detaliai.
Saugykla suformuota sąmoningai: visi commit’ai datuoti 2099 m. sausio 1 d., pasirašyti TeamPCP_OSS paskyra. Klasikinis triukas — automatizuotos stebėjimo sistemos painiojasi su data, git log atrodo kaip šlamštas. Prie saugyklos jau pradėjo jungtis pašaliniai dalyviai, vienas iš kurių pasiūlė pridėti FreeBSD palaikymą.
Anksčiau Shai-Hulud buvo vienos grupės įrankis su apibrėžtais tikslais. Dabar jo architektūra ir mechanikos prieinamos visiems.
GRĖSMĖS MASTAS IR BŪKLĖ
2026 m. gegužės 15 d. Saugykla pasirodė GitHub platformoje ir greitai dingo — tačiau forkai jau išplito. Kodo nebegalima atšaukti.
Kiek npm paketų iš tikrųjų užkrėsta — nežinoma. OX Security ir Datadog analizavo patį framework’ą, o ne jo panaudojimo pasekmes. Pilno vaizdo neturi niekas.
Domenas git-tanstack[.]com analizės metu buvo aktyvus. Dabartinę būklę tikrinkite patys: host git-tanstack.com arba per threat intelligence šaltinius.
Esminis pokytis: anksčiau šį įrankį naudojo viena grupė. Dabar jo logiką gali kopijuoti bet kas — ir, sprendžiant iš forkų aktyvumo, jau kopijuoja. Pataisymo nebus: tai ne CVE su fiksavimu, tai įrankis. Apsauga yra tik prevencinė.
KAS TAI YRA
Ne skriptų rinkinys — pilnavertis modulinis TypeScript/Bun framework’as. Architektūra padalinta į nepriklausomas dalis: įkroviklius, paslapčių rinkėjus, pristatymo dispečerį, eksfiltracija kanalus ir platinimo modulius. Dalis galima keisti neperrašant visko — tai inžinerinis darbas, o ne savaitgalio projektas.
Ko jis ieško užkrėstoje sistemoje:
— SSH raktai (viskas, ką randa ~/.ssh/ kataloge)
— .env failai
— GitHub ir npm tokenai
— AWS kredencialai (~/.aws/credentials)
— Kubernetes konfigūracijos (~/.kube/config)
— HashiCorp Vault paslaptys
— Docker credentials
— GCP ir Azure debesų paskyros
— Kriptovaliutų piniginės
— Pasiuntinių programų duomenys
— AI kūrimo įrankių konfigūracijos
PAGRINDINĖ TECHNIKA: ATMINTIES SKAITYMAS PER /proc
Tai esminis momentas norint suprasti, kodėl Shai-Hulud ypač pavojingas serveriuose su self-hosted GitHub Actions runner.
GitHub maskuoja paslaptis žurnaluose: jei tokenas patenka į stdout, jis pakeičiamas žvaigždutėmis. Tačiau maskavimas veikia tik žurnalų lygmeniu. Proceso atmintis lieka nepaliestas.
Runner.Worker yra paprastas Linux procesas su PID. Per /proc/[pid]/mem procesas, veikiantis tuo pačiu UID, gali tiesiogiai skaityti jo atmintį. Shai-Hulud veikia to paties naudotojo teisėmis kaip ir runner — tarkime, github-runner — randa Runner.Worker PID, skaito atmintį ir ištraukia visas paslaptis dar prieš tai, kai bet koks maskavimas spėja suveikti. Tokenas niekada nepasirodė žurnaluose, bet jau prarastas.
Tai ne branduolio išnaudojimas. Tai teisėtas Linux mechanizmas naudojamas ne pagal paskirtį. Būtent todėl standartiniai įrankiai sunkiai tai aptinka.
Ši technika aktuali tik tuo atveju, jei jūsų Ubuntu serveryje veikia self-hosted GitHub Actions runner. Jei runner’io nėra — šis vektorius jūsų neliečia.
PERSISTENCE PER SYSTEMD
Linux sistemoje Shai-Hulud įsitvirtina per naudotojo lygio systemd unit’ą. Ne sisteminį — naudotojo, ~/.config/systemd/user/ kataloge. Tai svarbu: kuriant nereikia root teisių, jis nematomas paprastame systemctl status be --user vėliavėlės ir aktyvuojamas naudotojo prisijungimo metu.
# Patikrinti dabartinio naudotojo lygio unit'us
systemctl --user list-units --type=service
# Tiesiogiai patikrinti unit failų katalogą (be sudo)
ls -la ~/.config/systemd/user/
# Patikrinti ir timer'ius
systemctl --user list-timers
# Patikrinti lingering — rodo, kad unit'ai sukonfigūruoti išgyventi be aktyvios sesijos
loginctl show-user $USER | grep Linger
DEADMAN SWITCH — PAVOJINGIAUSIA DALIS
Jei Shai-Hulud įsitvirtino per gh-token-monitor komponentą, GitHub tokeno atšaukimas be išankstinio pasiruošimo paleidžia naudotojo namų katalogo ištrynimą.
Mechanizmas paprastas: komponentas stebi tokeno galiojimą. Kai tik tokenas nustoja veikti — vykdoma destruktyvi komanda. Skaičiuojama, kad administratorius, aptikęs kompromisą, pirmiausia eis atšaukti tokenų.
Teisingas veiksmų eiliškumas įtariant kompromisą:
- Pirma — rasti ir išjungti persistence:
# Sustabdyti ir išjungti įtartinus naudotojo lygio unit'us
systemctl --user stop [unit-pavadinimas]
systemctl --user disable [unit-pavadinimas]
# Ištrinti unit failą
rm ~/.config/systemd/user/[unit-pavadinimas].service
systemctl --user daemon-reload
- Tik po to — atšaukti tokenus ir keisti raktus.
Jei padarysite atvirkščiai — pirma atšauksite tokeną, paskui ieškote persistence — galite prarasti namų katalogą su visais vietiniais raktais, konfigūracijomis ir duomenimis.
KAIP IŠFILTRUOJA DUOMENIS
Trys eksfiltracijų kanalai — kiekvienas aktyvuojamas kai ankstesnis neveikia.
Pagrindinis: duomenys suglaudinami, šifruojami AES-256-GCM, raktas uždaromas RSA-4096 ir viskas siunčiama į git-tanstack[.]com. Domenas sąmoningai pasirinktas imituoti teisėtą GitHub infrastruktūrą.
Atsarginis: jei C2 nepasiekiamas, framework’as ieško GitHub platformoje specialiai pasirašytų commit’ų su atsarginio serverio adresu. Adresas ne konfigūracijoje — commit’o metaduomenyse. Standartinis stebėjimas to nepastebi.
Avarinis: nėra nei pagrindinio, nei atsarginio — pavogti duomenys keliauja į šifruotus JSON failus viešai sukurtose GitHub saugyklose per pavogtą tokeną. Duomenys prieinami viešai, bet šifruoti. Užpuolikas juos pasiims vėliau.
KAIP PLINTA TOLIAU
Sukompromitavęs vieną saugyklą, Shai-Hulud juda toliau — užkrečia gretimus projektus ir npm paketus, kurie nuo jų priklauso.
Užkrėtimas per saugyklą. Shai-Hulud įdeda du failus į saugyklas, prie kurių turi prieigą: .vscode/tasks.json ir .claude/setup.mjs. Pirmasis automatiškai paleidžiamas atidarius projektą VSCode, antrasis — Claude Code sesijos pradžioje. Jei jūsų CI/CD klonuoja užkrėstą saugyklą ir pipeline’e yra žingsnis, aktyvuojantis šiuos įėjimo taškus — kenkėjiškas kodas vykdomas serveryje runner’io kontekste.
Užkrėsti npm paketai su suklastota atestacija. Čia įdomiau. Shai-Hulud publikuoja užkrėstas npm paketų versijas dviem būdais. Paprastas: publikuoti naują versiją per pavogtą tokeną. Gudresnis: piktnaudžiauja OIDC Trusted Publishing mechanizmu, generuodamas teisingą Sigstore kilmę per Fulcio ir Rekor. Rezultatas — kenkėjiškas paketas su patvirtintu skaitmeniniu parašu, atrodantis kaip teisėta versija iš patikimo CI/CD šaltinio. Standartinis npm audit to nesugaus — paketas techniškai pasirašytas teisingai.
Jei jūsų CI/CD vykdo npm install su priklausomybėmis, kurios galėjo būti sukompromituotos — tai aktyvus vektorius. Ypač pavojinga, kai versijos nėra griežtai fiksuotos per package-lock.json arba nurodytos su diapazonu (^1.2.0).
Svarbu: nei npm audit, nei npm audit signatures šio vektoriaus neaptiks. Paketas paskelbtas per teisėtą npm mechanizmą ir turi galiojantį registro parašą — techniškai viskas švaru. Apsauga tik prevencinė: neleisti netikėtoms versijoms patekti į surinkimą.
# Fiksuoti versijas — pašalinti ^ ir ~ iš package.json
# "express": "4.18.2" — teisingai
# "express": "^4.18.2" — pavojingai, gali įtraukti užkrėstą 4.18.3
# CI/CD pipeline'e naudoti npm ci vietoj npm install
# npm ci diegia griežtai pagal package-lock.json, neatnaujina priklausomybių
npm ci
# Patikrinti, ar package-lock.json yra įkeltas į saugyklą
git status package-lock.json
KAIP PATIKRINTI SERVERĮ
Kompromiso indikatoriai, kuriuos galite patikrinti dabar pat:
# Framework'o failai
find ~ -name "setup.mjs" -path "*/.claude/*" 2>/dev/null
find ~ -name "setup.mjs" -path "*/.vscode/*" 2>/dev/null
# Įtartini commit'ai serverio saugyklose
find /var/www /opt /srv -name ".git" -type d 2>/dev/null | while read gitdir; do
git -C "$(dirname $gitdir)" log --all --format="%ae %s" 2>/dev/null | \
grep "[email protected]"
done
# Naudotojo lygio systemd unit'ai visiems naudotojams
find /home -path "*/.config/systemd/user/*.service" 2>/dev/null
find /home -path "*/.config/systemd/user/*.timer" 2>/dev/null
# Lingering patikrinimas — be jo unit'ai neišgyvena be aktyvios sesijos
ls /var/lib/systemd/linger/
loginctl show-user NAUDOTOJAS | grep Linger
# Tinklo ryšiai su įtartinu domenu
host git-tanstack.com
ss -tnp | grep "ITARTINAS_IP"
grep "git-tanstack" /var/log/syslog 2>/dev/null
Jei serveryje yra self-hosted runner:
# tasks.json paieška projektų kataloguose
find /opt /var/www /srv -name "tasks.json" -path "*/.vscode/*" 2>/dev/null | \
xargs -r grep -l "setup\|mjs" 2>/dev/null
# Runner'io žurnalai
journalctl -u actions.runner.* --since "7 days ago" | grep -i "proc\|mem\|secret"
KĄ DARYTI DABAR
Jei serveryje yra self-hosted runner — uždaryti /proc atakos vektorių:
# Patikrinti dabartinę reikšmę
cat /proc/sys/kernel/yama/ptrace_scope
# Ubuntu pagal numatytuosius nustatymus nustato 1 nuo versijos 10.10
# Jei reikšmė 0 — serveris neapsaugotas, nustatyti 1:
echo 1 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# Padaryti nuolatinį per sysctl.d
echo "kernel.yama.ptrace_scope = 1" | sudo tee /etc/sysctl.d/99-ptrace.conf
sudo sysctl -p /etc/sysctl.d/99-ptrace.conf
Reikšmės: 0 — neapsaugota, 1 — apsaugota, 2 — tik root gali naudoti ptrace, 3 — ptrace visiškai išjungtas.
Self-hosted runner’iams rekomenduojama minimali reikšmė 1. Reikšmė 2 gali sulaužyti teisėtus derinimo įrankius — išbandykite staging aplinkoje.
Bendros priemonės nepriklausomai nuo runner’io:
# Patikrinti SSH authorized_keys dėl anomalijų
for f in /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys; do
[ -f "$f" ] && echo "=== $f ===" && cat "$f"
done
Užfiksuoti npm priklausomybes. Jei CI/CD naudoja npm — įsitikinti, kad package-lock.json yra saugykloje ir pipeline’as naudoja npm ci vietoj npm install.
Raktų ir tokenų rotacija įtariant kompromisą — griežtai tokia tvarka:
- Išjungti persistence (naudotojo systemd unit’ai)
- Keisti deploy SSH raktus visuose serveriuose — pirma pridėti naują raktą į authorized_keys, tada ištrinti seną
- Atšaukti GitHub ir npm tokenus
- Keisti CI/CD aplinkos paslaptis (GitHub Secrets, runner kintamieji)
- Patikrinti AWS/debesų kredencialus ir keisti jei reikia
# Pridėti naują deploy raktą prieš ištrinant seną
echo "naujas-viešasis-raktas" >> ~/.ssh/authorized_keys
# Įsitikinti, kad naujas raktas veikia — tik tada trinti seną
# Ištrinti pagal rakto komentarą (paskutinis laukas authorized_keys eilutėje)
sed -i '/seno-rakto-komentaras$/d' ~/.ssh/authorized_keys
IŠVADA
Shai-Hulud nėra branduolio pažeidžiamumas su CVE ir pataisymu. Tai įrankis, kuris piktnaudžiauja teisėtais mechanizmais: /proc atminties prieigai, systemd persistence užtikrinimui, Sigstore kenkėjiškiems npm paketams užmaskuoti kaip teisėtoms versijoms.
Ubuntu serveriui be self-hosted runner’io grėsmė minimali. Jei runner’is yra — ptrace_scope = 1 uždaro pagrindinį atakos vektorių, bet nepakeičia būtinybės patikrinti sistemą pagal aukščiau pateiktą sąrašą.
Pagrindinė pamoka: šaltinio kodo nutekėjimas reiškia, kad šias technikas dabar kopijuos mažiau kvalifikuoti užpuolikai. Supply chain atakos per CI/CD nebėra egzotika.
