Featured image of post Claude Code 扩展投毒自查与清理手册:几分钟判断你有没有中招,中招后按什么顺序处置

Claude Code 扩展投毒自查与清理手册:几分钟判断你有没有中招,中招后按什么顺序处置

昨天写的那篇讲的是这起事件的性质和它暴露的结构问题。这篇不重复那些,只讲动作:你现在该敲什么命令,看到什么算干净,看到什么该怎么清、按什么顺序清。

先说一个让人松一口气的事实,它决定了今天这件事的紧急程度:攻击者的服务器已经被拿下线了,反向隧道和回传都不通。所以现在被实时入侵的风险很低。但留在你机器上的东西不会因为对方下线而消失,SSH 公钥还在 authorized_keys 里,开机自启项还在,独立脚本还在跑。这就是为什么今天仍然值得花几分钟查一遍。

如果你是个人使用者,整个流程大概是这样:三分钟排查,十五分钟清理(如果中了),然后是轮换凭据和长期加固。

第 0 步:先确认你在不在暴露面内

三个条件同时成立,才需要往下走:

条件 具体值
从哪个市场装的 Cursor 的扩展市场(不是微软市场,也不是 Open VSX)
版本 2.1.312 或 2.1.313(Cursor 最初只报 313,后来把 312 一并纳入)
平台与时间窗 macOS 或 Windows,2026-10-29 02:27 到 13:35(太平洋时间)之间更新过

这里有一个很实用的细节:2.1.313 这个版本号,在 npm、Open VSX、微软市场里都不存在。OpenSourceMalware 对比时发现,那几个渠道的 Claude Code 版本是从 2.1.312 直接跳到 2.1.314 的。也就是说,313 这个版本号在事实上只以恶意构建的形式出现过。如果你在任何渠道装到过 2.1.313,都值得停下来查;如果是 2.1.312,要看来源是不是 Cursor 市场。

这条判断来自 OpenSourceMalware 追版本号时的发现,我把它挑出来是因为它对个人排查很省事:比记时间窗容易得多。

平台分支上还有一处要留意。载荷本身覆盖 macOS、Linux、Windows 三个平台,但 Cursor 通告里列入受影响构建的是 Windows 与 macOS,Phorion 的披露也主要讲 macOS。如果你在 Linux 上,找一遍不会有坏处,因为载荷的 Linux 分支是实打实写了的:/tmp/.stk_* 的私钥文件、crontab 里的 @reboot 条目,都在 IOC 清单上。官方没把 Linux 列进受影响构建,只说明他们没见到 Linux 的分发样本,不等于这段代码在 Linux 上不工作。

第 1 步:跑检查

官方给的最小检查只有两条命令,先跑这个,成本最低。

macOS 上:

grep -qs svc-telemetry ~/.ssh/authorized_keys && echo "svc-telemetry key found" || echo "No key found; machine not impacted"

Windows 上(PowerShell):

$files = "$HOME\.ssh\authorized_keys", "C:\ProgramData\ssh\administrators_authorized_keys" | Where-Object { Test-Path $_ }
if ($files -and (Select-String svc-telemetry $files)) { "svc-telemetry key found" } else { "No key found; machine not impacted" }

这两条查的是同一个东西:那个被追加上去的公钥在不在你的授权列表里。但只查这一项会漏掉三件事:持久化文件、独立脚本、隧道进程。这三样都不在 authorized_keys 里,而载荷是四样一起落的。

下面这份脚本覆盖了完整的 IOC:SSH 密钥、持久化文件、开机自启项、隧道进程、到 C2 的活动连接、本地 8099 端口、已安装扩展目录里的载荷特征,以及本机 SSH 服务状态。它只读,不删任何东西。

#!/usr/bin/env bash
# Claude Code 扩展投毒事件:本机自查(只读,不删任何文件)
set -u
KEY_COMMENT="svc-telemetry"
C2="134.209.82.225"
found=0
hit() { found=1; printf '  [!] %s\n' "$*"; }
ok()  { printf '  [ok] %s\n' "$*"; }

# 1. SSH 授权密钥
for ak in "$HOME/.ssh/authorized_keys" "/etc/ssh/authorized_keys"; do
  [ -f "$ak" ] || continue
  grep -qs "$KEY_COMMENT" "$ak" && hit "$ak 里存在 $KEY_COMMENT" || ok "$ak 干净"
done

# 2. 持久化文件与临时私钥
for f in "$HOME/.config/.smt" "$HOME/.config/.smtk"; do
  [ -e "$f" ] && hit "存在 $f" || ok "$f 不存在"
done
ls "${TMPDIR:-/tmp}"/.stk_* >/dev/null 2>&1 && hit "存在 .stk_*(解出的隧道私钥)" || ok ".stk_* 不存在"

# 3. 开机自启
crontab -l 2>/dev/null | grep -qs '\.smt' && hit "crontab 引用了 .smt" || ok "crontab 干净"
PL="$HOME/Library/LaunchAgents/com.apple.sysmond.plist"
[ -e "$PL" ] && hit "存在假 Apple 名 LaunchAgent:$PL" || ok "$PL 不存在"

# 4. 隧道进程
ps ax | grep -E -- '-R \*:0:localhost:22|autossh' | grep -qv grep \
  && hit "发现反向隧道进程" || ok "未发现隧道进程"

# 5. 网络侧
ss -tnp 2>/dev/null | grep -q "$C2" && hit "存在到 $C2 的连接" || ok "无到 $C2 的连接"

# 6. 扩展目录里的载荷特征
for d in "$HOME/.cursor/extensions" "$HOME/.vscode/extensions"; do
  [ -d "$d" ] || continue
  for e in "$d"/anthropic.claude-code-*; do
    [ -d "$e" ] || continue
    v=$(basename "$e" | sed 's/.*-//')
    case "$v" in 2.1.312|2.1.313) hit "受影响版本目录:$e";; esac
    js="$e/extension/extension.js"; [ -f "$js" ] || js="$e/extension.js"
    [ -f "$js" ] && grep -qs "$C2" "$js" && hit "extension.js 含 C2 地址"
  done
done

[ "$found" = "0" ] && echo "结论:未发现该事件 IOC" || echo "结论:见上方 [!],按第 3 步处置"

我在自己的 Linux 机器上跑了一遍,输出是这样:

=== 1. SSH 授权密钥 ===
  [ok] 本机没有 authorized_keys 文件(没有可被追加密钥的位置)
=== 2. 持久化文件 ===
  [ok] /home/<user>/.config/.smt 不存在
  [ok] /home/<user>/.config/.smtk 不存在
  [ok] /tmp/.stk_* 不存在
=== 3. 开机自启 ===
  [ok] crontab 未发现引用 .smt 的条目
  [ok] ~/Library/LaunchAgents/com.apple.sysmond.plist 不存在
=== 4. 运行中的隧道进程 ===
  [ok] 未发现反向隧道进程
=== 7. 已安装扩展目录里的载荷特征 ===
  [ok] 未发现 anthropic.claude-code 扩展目录
结论:本机未发现该事件的任何 IOC。

这份输出的证据边界要说清:只有 Linux 分支被真实执行过,脚本里的 macOS 路径和 Windows 路径是按官方 IOC 清单写的,没有实机验证。所以如果你在 macOS 或 Windows 上,请把脚本当检查清单用,路径逐个对,或者直接用官方清理脚本。

第 2 步:怎么读结果

结果只有三种,对应三种动作。

第一种,没命中任何一项,而且你不在暴露面内。结束,不用做别的。

第二种,没命中,但你在暴露面内。这也不等于没事。载荷的第一个动作就是往 authorized_keys 追加密钥,而如果那次没写成功,或者你后来重装过系统,痕迹可能已经不在。官方的处置建议里明确写着这一条:即使环境变量收集范围有限,也不能排除通过后门做后续凭据访问,所以按受影响处理、轮换凭据。

第三种,命中了任意一项。进入第 3 步,而且不要先急着删。

判读时最容易误判的是「隧道连不上所以没事」这个推论。载荷在启用 SSH 服务这一步写错了:macOS 上它用 systemsetup -getremotelogin 检查,这条命令以普通用户身份跑会返回权限错误、退出码却是 0,输出里没有 Off,而脚本只在看到 Off 时才去启用 SSH,于是跳过了;Windows 上它想通过 /bin/sh 调 PowerShell 装 OpenSSH Server,标准 Windows 没有这个 shell,也失败。所以绝大多数机器上隧道确实通不了。

但隧道通不了只说明它没能给你开一个入口,不说明它没动过你的机器。它已经做了三件事:把攻击者的公钥写进了你的授权列表,落了持久化脚本,把主机信息发出去了。边缘情况也确实存在,Phorion 说得很直接,在本来就开着远程登录的 Mac 上,或者用户事后自己打开的情况下,后门就是现成可用的。Cursor 也补了一条:除非 Cursor 是以管理员权限启动的,否则这一步会失败,反过来说,用管理员权限开 IDE 的人就在危险那一侧。

第 3 步:命中之后,先别删

顺序比命令重要。先取证,再断网,再清,最后轮换。

第一件事是留档。OpenSourceMalware 的建议是先断网、保留现场,删掉已知文件之后再评估要不要重建主机。理由很实在:这个载荷给的是一个可交互的 SSH 入口,你能看到的文件不一定代表全部改动,删完之后你连查的线索都没了。

值得留下的东西不多,五样就够:authorized_keys 的完整副本、ps 输出里带隧道参数的那几行、crontab -l 的输出、LaunchAgent 的 plist 内容或计划任务的导出、以及扩展目录里 extension.js 的原件加哈希。另外记下你发现它的时间,这个时间戳后面判断「有没有人进来过」要用。

想确认有没有人真的登进来过,Phorion 给了方法:查 SSH 日志里来自 localhost、用的正是 svc-telemetry 这个 key 的登录记录。如果没有这条记录,说明至少没有人成功用过这个后门;如果有,就按已经失陷处理。

然后是四个层次的东西要清,我按平台列全:

层次 macOS Linux Windows
SSH 公钥 ~/.ssh/authorized_keys 里 svc-telemetry 那条 同左 %USERPROFILE%\.ssh\authorized_keys,以及 C:\ProgramData\ssh\administrators_authorized_keys
持久化文件 ~/.config/.smt、~/.config/.smtk 同左 %USERPROFILE%\.config\smt.cmd、%USERPROFILE%\.config\smtk
临时私钥 $TMPDIR/.stk_<10 位十六进制> /tmp/.stk_<10 位十六进制> %TEMP%\.stk_<10 位十六进制>
开机自启 ~/Library/LaunchAgents/com.apple.sysmond.plist(label 是 com.apple.sysmond,名字冒充 Apple 组件,实际不是,要用 launchctl unload -w 卸掉) crontab 里 @reboot /bin/sh "<HOME>/.config/.smt" 名为 SysMon 的计划任务(试图以 SYSTEM 运行),以及回退项 %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\sysmon.cmd

进程可以用命令行特征找,载荷用的参数很显眼:-R '*:0:localhost:22' 那句端口转发、StrictHostKeyChecking=no、UserKnownHostsFile=/dev/null、ExitOnForwardFailure=yes,以及 Unix 上优先用 autossh。还有一个不显眼的:macOS 那个 LaunchAgent 用的是 RunAtLoad 加 KeepAlive,意思是开机就起、挂了就重启,所以你杀掉进程它还会回来,得先把自启项卸掉。

手上不想敲这些,官方直接给了清理脚本,两个平台各一个:

  • macOS:下载 remediate-macos.sh,以受影响的用户身份运行,不要用 root,然后 bash remediate-macos.sh
  • Windows:下载 remediate-windows.ps1,在以管理员身份打开的 PowerShell 里跑 powershell -ExecutionPolicy Bypass -File .\remediate-windows.ps1

这两个脚本会移除恶意扩展、持久化项、隧道进程和攻击者的 SSH 公钥,而且如果还有残留会返回非零退出码,方便你确认清干净了。地址在 Cursor 的安全通告里。

有一条必须记住:卸载扩展不够。载荷会落下独立脚本继续跑,所以清理要按主机侧做,不能只在 IDE 里点卸载。这也是官方通告里专门加粗提醒的一句。

第 4 步:清完之后,评估要不要重建

清理和重建是两个不同量级的选择,官方和第三方给的倾向不完全一样。Cursor 的说法是「评估这台主机是否需要重建」,OpenSourceMalware 说得更直接:重建主机比假设「删掉已知文件就恢复了信任」更安全。

我的判断标准是这样,谈不上权威,但比「要不要重装」这种问题好回答:

如果命中了,并且满足下面任一条,我会倾向直接重建:那 11 小时窗口里这台机器的 SSH 服务在跑;或者你平时用管理员权限启动 Cursor;或者 SSH 日志里出现了用 svc-telemetry 从 localhost 登入的记录。这三种情况意味着隧道有机会被实际使用过,而用过之后对方看到了什么、做了什么,你从文件层面查不出来。

如果只命中了 key 和持久化文件,SSH 服务从来没开过,日志里也没有那次登录,那么清理加完整轮换通常够用,不必重建。代价是你要接受「基于已知 IOC 清理」这件事本身有残余风险。

重建之外还有一个折中:把受影响机器上的开发凭据全部作废并重新签发,等于把「这台机器曾经能碰到的东西」重建成空集,这比修复文件更接近根治。

第 5 步:整改,从轮换凭据开始

清理只解决了「东西还在不在」,不解决「对方拿走了什么」。所以这一步是轮换。

官方的顺序建议是:在一台可信的机器上,轮换受影响的这台机器能够访问到的凭据和会话。为什么要换机器,因为被污染的主机上任何操作都可能被看着,包括你输密码那一刻。

问题是要轮换的东西太多,从哪开始?这次事件的载荷给了一个现成的优先级依据。它收集的是环境变量名字,匹配规则是名字里包含 KEY、TOKEN、SECRET、PASS、AWS 或 GCP,最多 40 个。也就是说,攻击者手上有一份「哪些机器配了哪类凭据」的名单。

你可以用同一条规则给自己列清单:

env | cut -d= -f1 | grep -Ei 'KEY|TOKEN|SECRET|PASS|AWS|GCP'

这条命令只打印变量名,不打印值,可以放心跑。打印出来的是「如果这台机器在 10 月 29 日被碰过,那这些凭据要当作已泄露」。按这个清单去轮换,比凭感觉删一通要准得多。

轮换的顺序建议是:云凭据(AWS/GCP)→ 代码托管 token(GitHub/GitLab)→ 模型 API key → 其他服务 token → 最后是这台机器上所有登录会话(浏览器会话、SSH 私钥)。

为什么个人用户的处置门槛比想象中高

做完上面这些,我意识到难点不在命令,在于一个个人开发者要回答三个问题,而每个问题都不是一条命令能答的。

第一个是「我在不在暴露面内」。这要求你记得自己装过哪个版本、从哪个市场装的。现实是绝大多数人不记得,因为扩展是自动更新的,装的时候也不会有人提醒你记下来。这次能靠版本号跳变(313 在别的渠道不存在)取巧判断,下次未必有这个便宜可占。

第二个是「我有没有被留下东西」。这要求你在四个几乎从不变动的位置上有基线,不然你只知道「现在有什么」,不知道「什么时候多出来的」。

第三个是「我该轮换什么」。这要求你知道这台机器上的凭据分布,而凭据通常散在环境变量、配置文件、IDE 设置和 shell profile 里,没有一份清单。

三个问题对应三个缺位:安装记录没有版本历史、持久化位置没有基线、凭据没有清单。这三样都是可以在平静期补上的,成本很低,而它们决定了出事那天的处置是从三分钟开始,还是从三天开始。第 5 步里那几条加固,本质上就是在补这三个缺位。

第 6 步:以后怎么少踩

下面是个人使用者能做的,按性价比从高到低排。前两条这次事件里能直接救你,后四条是长期收益。

  1. 把凭据从环境变量里挪走。这是这次事件最直接的教训。它的第一轮收集就是扫环境变量名,而环境变量是绝大多数开发者放 API key 的默认位置,因为方便,也因为它会被 IDE、终端、构建脚本和 agent 一起继承。挪走的方式有很多种,macOS 用 Keychain,跨平台用 1Password CLI 或 pass,或者退一步:放进权限 0600 的文件、由工具按需读取,并确保它在 .gitignore 里。用哪种方案都可以,要的是让凭据不再出现在 env 的输出里。

  2. 监控那四个持久化位置。这次事件里,最直接能抓住它的检测点在这四个位置被改动,跟扩展本身长什么样无关:~/.ssh/authorized_keys、~/Library/LaunchAgents/(或 ~/.config/systemd/user/)、crontab -l、以及 Windows 的计划任务。这四样在正常的一天里几乎不会变。个人也做得到,写个每天跑一次的脚本对它们算哈希存下来,变了就提醒你。这条的价值在于它是行为层检测,不依赖你认得出哪个发布者是真的。

  3. 关掉插件自动更新,改成固定版本。这次的传播完全靠自动更新:11 小时窗口里最多 5,000 台机器,没有人是被说服之后装的。把扩展当依赖管理,固定版本、需要时手动升,你就在这个链条上留了一个人工闸门。代价是失去及时获得安全修复的便利,所以合理的折中是自动更新关掉,但订阅官方的安全公告。

  4. 让 SSH 服务保持关闭。这次事件里那个 bug 之所以救了一大批人,恰恰是因为大多数 macOS 和 Windows 主机没有在跑 SSH 服务,隧道连不进来。这个默认关闭是你的资产,别为了偶尔远程登录把它长期打开。

  5. 不要用管理员权限启动 IDE。官方明确写了,唯一会让载荷成功开 SSH 的场景,就是 Cursor 以管理员权限启动。编辑器本来不需要这个权限。

  6. 把已装插件清单化,并且只从官方市场装。不用做复杂,一份文本文档记下装了哪些扩展、来自哪个市场、版本几号就够。它的作用在出事那天显出来:你能在三十秒内回答「我在不在暴露面内」,而不是去翻缓存目录猜。

一句总结和一点边界

这起事件对个人使用者最实际的启示是:你无法通过「这个发布者是官方」来判断安全,只能通过「这个东西的行为是否合理」。一个代码补全插件往你的 SSH 授权列表里写东西、拉起一条常驻隧道、在开机时自启,这三件事没有一件是它该做的。这次是扩展,下次可能是别的东西,行为基线这条防线不随载体变化。

证据边界说清楚:本文的命令、路径、哈希、指纹全部来自 Cursor 的安全通告和 OpenSourceMalware 的技术分析,我没有能力独立验证那些远端材料;我自己的检查脚本只在本机 Linux 上真实跑过,macOS 与 Windows 分支是按官方 IOC 清单写的,未经实机验证。如果你在受影响的平台且命中任何一项,优先用官方的清理脚本,而不是照着我这份清单手敲。

事件本身的性质、以及它为什么会发生,见昨天那篇《Claude Code 官方扩展被投毒》;供应链这一类问题在别的载体上的表现,见《MCP 供应链安全》和《LiteLLM 又被投毒了》。

By AI博士 万戈