在开发机与开发容器中核查 IDE 扩展篡改风险:从版本来源校验到 SSH 后门痕迹与凭据轮换

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
399
文章
0
粉丝
安全开发1 23字数 3198阅读10分39秒阅读模式
AI智能摘要
装了没报错就等于没有副作用?IDE 扩展运行在开发者权限下,能读写工作区、访问网络,甚至以扩展宿主身份拉起 node、ssh 或 autossh。版本号只是发布者声明的元数据,哈希才是可复核的指纹。文章用来源、版本指纹、主机痕迹三个信任断点排查,识别禁用 host-key 校验等异常信号。你的允许列表跟得上下次升级吗?
— AI 生成的文章内容摘要

开发者安装 IDE 扩展的动作,和 npm install 一样自然:搜索、点击安装、重启编辑器,然后继续写代码。容易被忽略的安全假设也藏在这个动作里——扩展市场里的包等同于可信包,版本号等同于代码指纹,装完没报错等同于没有副作用。实际上,IDE 扩展运行在开发者权限下,能读写工作区、调用终端、访问网络、修改 shell 配置,甚至以扩展宿主进程的身份拉起 node、ssh 或 autossh。MITRE ATT&CK 将这类行为归入 T1176.002,并给出检测策略 DET0561,核心思路不是只看扩展名称,而是把安装来源、文件落盘、进程创建和网络外连串成一条可复核的链。

开发机上的 IDE 扩展目录与终端排查场景

先划清三个信任断点

排查之前先明确边界:你检查的是“扩展从哪来、装的是什么、装完做了什么”。这三个断点分别对应来源、版本指纹和主机痕迹,任何一段缺失,后续结论都只能算猜测。

信任断点要回答的问题可复核证据
来源来自官方市场、内部镜像,还是侧载 VSIX?发布者是否与允许列表一致?扩展 ID、发布者、安装命令、内部制品库记录
版本指纹当前运行版本是否等于批准版本?安装包哈希是否被记录?extensions.json、VSIX SHA-256、内部镜像摘要
主机痕迹扩展宿主是否拉起了异常子进程、写了持久化项或建立隧道?进程树、网络连接、启动项、authorized_keys

单个扩展的版本号不能证明代码没被篡改。版本号是发布者声明的元数据,哈希才是你手里能复核的指纹。如果团队通过内部镜像分发 VSIX,至少应记录扩展 ID、版本、发布者、SHA-256 和批准日期;如果直接走市场安装,则要接受“无法逐包复核”的现实,用允许列表和进程行为监测补足。

准备:允许列表与版本固定

先不要急着全盘扫描。准备阶段的目标是让“什么该装”和“装的是不是它”变成可执行规则。下面是一个最小允许列表,字段故意保持简单,方便放进仓库或配置中心:

{
  "extensions": [
    {
      "id": "ms-python.python",
      "publisher": "ms-python",
      "version": "2024.6.0",
      "sha256": "填内部镜像记录的哈希",
      "approved_by": "devsecops",
      "approved_at": "2025-01-15"
    }
  ]
}

固定版本可以用 IDE 自带的安装参数完成,例如 VS Code 支持 code --install-extension ms-python.python@2024.6.0。这条命令的边界是:它只保证安装请求指向某个版本,不保证市场返回的包未被替换;因此版本固定要和哈希记录、内部镜像或制品库配合使用。副作用是升级不再自动发生,需要把版本评审纳入日常流程。

接下来写一个只读核对脚本。下面用 Node.js 读取 VS Code 的 extensions.json,输出不在允许列表中的扩展。它不删除、不禁用,只报告,适合先跑一段时间观察误报:

// check-extensions.mjs
// 适用:VS Code,路径因平台而异;只读,不修改扩展目录
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const allow = JSON.parse(fs.readFileSync("./allowlist.json", "utf8"));
const allowed = new Map(allow.extensions.map((e) => [e.id.toLowerCase(), e]));

const extDir = path.join(os.homedir(), ".vscode", "extensions");
const registry = path.join(extDir, "extensions.json");
const installed = JSON.parse(fs.readFileSync(registry, "utf8"));

for (const item of installed) {
  const id = item.identifier.id.toLowerCase();
  const hit = allowed.get(id);
  if (!hit) {
    console.log(`未批准: ${id}@${item.version}`);
    continue;
  }
  if (hit.version !== item.version) {
    console.log(`版本偏离: ${id} 期望 ${hit.version} 实际 ${item.version}`);
  }
}

这段代码的局限要说清楚:extensions.json 是 IDE 自己维护的注册表,攻击者如果直接改写扩展目录,注册表可能被同步篡改;它适合做基线核对,不适合作为唯一取证依据。Windows 的路径通常是 %USERPROFILE%\.vscode\extensions,JetBrains 系列则使用不同的插件目录,脚本需要按环境调整。

排查一:进程树与扩展宿主

扩展被安装后,真正有权限的是扩展宿主进程。先看进程,不要先看扩展列表。重点不是“有没有 node”,而是“IDE 是否拉起了不该出现的子进程”。

# Linux/macOS:查看 IDE 相关进程及父子关系
ps -eo pid,ppid,user,etime,command | grep -Ei 'code|idea|eclipse|node|ssh|autossh' | grep -v grep

判断时注意几个边界。第一,VS Code Remote SSH、JetBrains Gateway 等合法场景也会出现 ssh 子进程,不能一见 SSH 就判恶意。第二,语言服务器、调试适配器、测试运行器都可能拉起 node,需要结合命令行参数和父进程判断。第三,autossh 在开发机上不是常见依赖,如果它由 IDE 扩展宿主拉起,且参数包含 -M 0、-N 或远端 tunnel@ 地址,应优先隔离再核实。

Coralogix 的威胁通告描述了一个具体链条:扩展解码内嵌私钥到 ${TMPDIR}/.stk_,运行 autossh 并回退到 ssh,连接远端隧道地址,禁用 host-key 检查,每 60 秒重连。这个描述的价值不在于记住某个 IP,而在于提醒你:临时目录里的隐藏私钥文件、禁用 host-key 校验的参数、固定间隔重连,都是比“扩展名称可疑”更稳定的行为信号。具体 IOC 和影响范围应以厂商公告为准,不要把单次事件里的地址当成通用规则。

排查二:启动项、shell 配置与计划任务

持久化项是扩展植入后最容易被忽略的部分。攻击者不一定需要 root,用户级启动项就足够在每次登录时恢复连接。

# macOS 用户级 LaunchAgents
ls -la ~/Library/LaunchAgents/
plutil -p ~/Library/LaunchAgents/*.plist 2>/dev/null | grep -Ei 'RunAtLoad|ProgramArguments|Label'

# Linux 用户级 systemd 与 cron
systemctl --user list-units --type=service --all
crontab -l 2>/dev/null
ls -la ~/.config/systemd/user/ 2>/dev/null

Coralogix 通告里提到的 macOS 持久化路径是 ~/Library/LaunchAgents/com.apple.sysmond.plist,用 com.apple.sysmond 这类接近系统组件的名称伪装。检查时不要只看文件名,要看 ProgramArguments 指向的可执行文件、RunAtLoad 是否为 true、标准输出是否被重定向到隐藏路径。linux 上同理,用户级 systemd 服务可以以开发者权限常驻,ExecStart 指向临时目录或家目录隐藏文件时值得进一步核实。

shell 配置也常被忽略。扩展可以通过修改 ~/.zshrc、~/.bashrc、~/.profile 注入命令,或者写入 IDE 的 tasks、launch 配置。检查时优先看最近修改时间和异常的行尾追加:

grep -nE 'curl|wget|base64|ssh|autossh|/tmp/|.stk' ~/.zshrc ~/.bashrc ~/.profile 2>/dev/null

这不是说出现 ssh 就一定有问题,而是把“配置文件的变更”与“进程和网络证据”放在同一时间窗口里对照。单独一条 grep 结果很容易误报。

主机持久化痕迹与网络外连排查流程示意

排查三:authorized_keys、SSH 配置与临时私钥

SSH 后门排查要区分“被写入公钥”和“被拿走私钥”。前者意味着攻击者可以再次登录,后者意味着攻击者可以主动外连。两边都要看。

# 查看授权公钥及其选项,不要直接打印私钥内容
awk '{print NR": "$1" "$2" "$3}' ~/.ssh/authorized_keys 2>/dev/null
ls -la ~/.ssh/
grep -RniE 'ProxyCommand|HostName|IdentityFile|StrictHostKeyChecking' ~/.ssh/config 2>/dev/null

authorized_keys 中出现不认识的 key、command= 强制命令、from= 限制来源、或注释字段包含异常字符串时,应先记录再处理。不要直接删除文件,先复制一份到隔离目录并记录哈希,避免破坏取证线索。~/.ssh/config 中的 ProxyCommand 和 StrictHostKeyChecking no 也值得核对,尤其是你从未配置过的主机别名。

临时目录里的隐藏私钥文件需要单独检查。通告中提到的 ${TMPDIR}/.stk_ 是一个具体例子,实际排查可以按“近期创建的隐藏文件 + 私钥头”组合筛选:

find "${TMPDIR:-/tmp}" "$HOME" -maxdepth 3 -type f -name '.*' -mtime -7 2>/dev/null 
  | xargs -r grep -l 'BEGIN OPENSSH PRIVATE KEY' 2>/dev/null

这条命令会扫描家目录和临时目录下最近七天修改的隐藏文件,可能命中合法的密钥备份,因此结果只作为线索。发现私钥副本后,不要把它复制到聊天工具或工单系统;记录路径、权限、创建时间和哈希即可。

排查四:网络外连与隧道特征

网络检查的目标不是封掉所有外连,而是找出“IDE 主机不应主动发起的连接”。先看稳定连接和监听端口:

ss -tpn 2>/dev/null | grep -Ei 'code|node|ssh'
lsof -nP -iTCP -sTCP:ESTABLISHED 2>/dev/null | grep -Ei 'code|node|ssh'

判断时把连接分成三类:市场与更新域名、代码托管与包仓库、未知远端。前两类通常是开发流程的一部分,第三类需要结合进程和启动项确认。MITRE DET0561 的分析链提到,扩展安装或更新后可能出现对市场域名的外连,也可能出现由 IDE 拉起的 SSH/WebSocket 隧道端点;检测时要把文件写入、进程创建和网络流放在同一个时间窗口里关联,而不是单独看某个域名。

host-key 校验被禁用是一个更强的信号。正常的 SSH 客户端首次连接会提示指纹,脚本化场景可能使用 StrictHostKeyChecking=accept-new,但 StrictHostKeyChecking=no 加 UserKnownHostsFile=/dev/null 的组合,通常意味着连接方不关心对端身份。若这种参数出现在 IDE 扩展拉起的 ssh 或 autossh 命令行里,应优先隔离主机。

隔离、轮换与回归验证

确认异常后,顺序比动作更重要。建议按“隔离、取证、清除、轮换、回归”推进,避免在攻击者仍在线时轮换凭据。

  1. 隔离:断开网络或挂起主机,保留内存和磁盘现场;不要先卸载扩展或删除启动项。如果主机承载生产密钥,先评估是否需要从其他可信终端撤销会话。
  2. 取证:复制扩展目录、extensions.json、启动项、authorized_keys、shell 配置和近期网络连接记录,记录文件哈希与时间戳。对疑似私钥文件只记录元数据,不扩散内容。
  3. 清除:卸载异常扩展,移除未知启动项和 authorized_keys 条目,恢复被修改的 shell 配置;如果无法确认清除完整,重建开发环境比修补更可靠。
  4. 轮换:先撤销 SSH 密钥和会话,再轮换云 IAM 凭据、CI/CD token、Git 托管 token、包仓库 token、数据库密码。轮换顺序应从权限最大、传播最广的凭据开始,并检查这些凭据是否已被写入构建产物或推送到远端仓库。
  5. 回归:用允许列表中的固定版本重新安装,核对 VSIX 哈希,观察进程树和网络连接一段时间,再把检查脚本接入日常流程。

回归验证不要只看“扩展能正常启动”。至少确认三件事:安装来源与批准记录一致;扩展宿主没有拉起未知的 ssh、autossh 或隐藏临时文件;启动项和 authorized_keys 没有复发。若团队使用 CI/CD,可以把允许列表核对做成非阻断报告,运行一到两周后再决定是否阻断,避免因路径差异和合法远程开发场景造成大量误报。

把一次响应变成准入控制

单次事件响应解决的是“这一台机器有没有问题”,长期准入控制解决的是“下一台机器会不会重蹈覆辙”。两者不能互相替代。允许列表、版本固定、哈希记录、进程与网络监测、凭据轮换预案,应该组合成开发环境供应链的常规控制,而不是事件发生后才临时拼凑的检查表。

对开发者来说,最实际的动作是把“安装扩展”从个人习惯变成可复核的变更:记录来源,固定版本,保留哈希,知道怎么查进程和启动项。对 DevSecOps 团队来说,重点不是追求零误报,而是让误报可解释、可回退、可审计。扩展供应链的风险不会因为一次排查结束而消失,但每一次可复核的排查都会让下一次响应更快、更准。

 
枫少@KillBoy
    • 火爆猴
      火爆猴 1

      扩展市场确实不能默认全信,我们组之前也讨论过允许列表

    匿名

    发表评论

    匿名网友

    拖动滑块以完成验证