别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原

admin 2026年8月11日08:43:31评论177 views字数 7992阅读26分38秒阅读模式

本文分析Metabase CVSS 10.0零日漏洞:未认证密码重置接口在返回HTTP 400前已执行SQL注入,写入管理员会话。攻击者可接管管理员账号,官方确认主动利用。文章给出源码级调用链、流量关联模式及修复升级建议,强调先升级再排查的处置顺序。

一次未认证的密码重置请求表面返回 HTTP 400,应用数据库里的管理员会话却已经写入。随后带着已知会话访问 /api/user/current,服务端返回 HTTP 200,并给出 id: 1 与 is_superuser: true。

我刚看到这个漏洞的时候,最惊讶的不是 SQL 注入本身。是一个密码重置接口,在返回“无效 token”给客户端之前,已经悄悄地把请求里的东西写进了数据库。返回码和系统状态,从那一刻起就不是同一件事了。

400 为什么会和“利用成功”同时出现

公开实验仓库对调用链做了源码级说明。易受影响的密码重置流程本应只处理 token 与 password,但旧流程把调用方提交的完整 JSON 映射继续传进认证管线。调用方控制的 user-id 在认证失败后仍然存活,查询构造器又接受了可表达原始 SQL 的结构。

漏洞请求的形状可以压缩为:

POST /api/session/reset_password HTTP/1.1
Content-Type: application/json

{
  "token": "invalid",
  "password": "<VALUE>",
  "user-id": {
    "select": {
      "raw": "<SQL_EXPRESSION_OR_STACKED_STATEMENT>"
    }
  }
}

应用库先执行了查询和附带的数据库副作用,业务处理随后才把伪造的 reset token 判成无效。因此客户端最终看到 400,不代表前面的 SQL 没有执行。

官方给出的日志模式正是:

POST /api/session/reset_password  -> 400
GET  /api/user/current            -> 200

两个请求必须按来源、时间和会话字段关联分析。只按单条 400 告警降级,会漏掉已经发生的管理员会话接管。

官方背景:主动利用之后,升级窗口按小时算

官方公告已经确认三件事:漏洞允许未认证远程攻击者向 Metabase 应用数据库注入任意 SQL;攻击链可进一步取得实例管理员权限;Metabase 已确认主动利用,CVSS 评分为 10.0。Metabase 官方安全更新还披露,Metabase Cloud 曾遭到未知零日利用。

Metabase 连接的不是一份孤立数据。应用数据库保存用户、会话、API Key 和数据源连接配置;管理员又能通过已经配置的数据源读取或导出业务数据。

官方 GHSA 将影响写得很直接:攻击者取得管理员权限后,可以修改应用配置、窃取已保存的数据源凭据、读取连接可达的数据,并导出数据。公开媒体随后转述了 Framework 与 Tally 的客户数据泄露通知。后者属于外部事件报道,不属于本文的本地实验结论。

外网暴露、命中受影响版本、端点可达,三项条件同时出现时,升级窗口已经按小时计算。WAF 临时封禁能收窄入口,却不能替代升级和失陷排查。

**处置顺序不要反过来:**先升级对应分支的修复版本,再清空活动会话、复查管理员与 API Key、轮换已连接数据库的凭据,并检查数据仓库日志及 Metabase 查询历史。临时来不及升级时,先在入口处阻断 /api/session/reset_password。

官方列出的最低安全版本

  • 0.58 分支:0.58.24
  • 0.59 分支:0.59.21
  • 0.60 分支:0.60.17
  • 0.61 分支:0.61.11
  • 0.62 分支:0.62.9
  • 0.63 分支:0.63.5

截至 2026 年 8 月 10 日,GitHub Advisory 页面仍显示 No known CVE。资产排查时要同时检索 GHSA 编号与版本,不能只等 CVE 规则更新。

实验环境与成功判据

实验使用 Apple M3 Max 主机和 Docker Desktop。应用数据库为 PostgreSQL 16.14,Metabase 使用官方 OSS 镜像。

  • 易受影响组:Metabase v0.62.7
  • 修复对照组:Metabase v0.62.9
  • 请求工具:Yakit WebFuzzer
  • 流量分析:Wireshark,回环接口 lo0
  • 会话验证:GET /api/user/current

我在发包前定义了三层成功判据:应用数据库出现 SQL 标记;core_session 出现预先指定的会话值;带着这个会话访问用户接口返回 200,且 JSON 明确给出管理员身份。

修复组只改 Metabase 版本。相同输入如果没有标记、没有会话,用户接口返回 401,才算完成负对照。

实验说明: Redwood 会话无法直接访问 Docker Desktop 发布到宿主机的 127.0.0.1:3001。Yakit 因此连接 127.0.0.1:33001,一个透明 TCP bridge 再转到容器内的 127.0.0.1:3000。桥接过程没有改动 HTTP method、URI、请求体、状态码或响应体;Wireshark 按 tcp port 33001 抓取回环流量。桥接只解决本机可达性,不参与 SQL 构造和响应改写。

Yakit 复现:400 已经不是终点

Yakit WebFuzzer 向 /api/session/reset_password 发送未认证 JSON 请求。请求体包含无效 token、密码字段,以及用于触发应用库查询的额外 user-id 结构。

别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原
图 1|Yakit 原生界面。左侧是发往 v0.62.7 的请求,右侧服务端返回 HTTP/1.1 400。单看状态码会得到错误判断。

响应体只有一条业务错误:Invalid reset token。服务端没有在响应里回显 SQL 执行结果。

别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原
图 2|Yakit 原生界面中的 400 响应体。业务层拒绝 token 的时间晚于应用库副作用。

数据库查询确认预设会话已经进入 core_session。我随后用同一个值作为 X-Metabase-Session 请求 /api/user/current。

Yakit 收到 200,JSON 中可见 email: [email protected]、id: 1 和 is_superuser: true。本地成功判据到此全部满足。

别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原
图 3|Yakit 原生界面。/api/user/current 返回管理员身份;截图只覆盖本地 v0.62.7 实验实例。

实验结束后,我删除了临时插入的会话行,并再次查询确认计数为 0。

Wireshark 还原两条 HTTP 流

抓包文件包含 24 个数据包,持续 52.732850 秒,共 7,792 字节。显示过滤器 http 能看到四个关键帧:

  • frame 5:POST /api/session/reset_password
  • frame 7:HTTP 400
  • frame 17:GET /api/user/current
  • frame 19:HTTP 200
别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原
图 4|Wireshark 原生界面。四个关键帧分属两条 TCP 流,不能把单个 TCP 包等同于一次完整业务动作。

Follow HTTP Stream 0 后,请求与 400 响应位于同一条流。红色部分是未认证 POST,蓝色部分是服务端 400。原始流量再次确认:返回码和 SQL 副作用并不矛盾。

别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原
图 5|Wireshark 的 HTTP Stream 0。请求结构、目标端点与 400 响应可以在同一窗口核对。

Follow HTTP Stream 1 后,第二条流包含已知 X-Metabase-Session 和 200 响应。响应 JSON 在流量层同样给出 id: 1、管理员邮箱与 is_superuser: true。

别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原
图 6|Wireshark 的 HTTP Stream 1。管理员 JSON 来自本地实验流量,不代表任何生产实例。

原始 PCAP 的 SHA-256 为:

00df06d9c80b8262a3e032b2418daafaf02ffa8aa3c61ae8ca2a680f38a0661b

修复版负对照挡住了同样输入

v0.62.9 接收相同请求时,在 API schema 边界把 user-id 判为意外字段。应用数据库没有出现 SQL 标记,没有新增预设会话,带着同一个会话值访问 /api/user/current 返回 401。

v0.62.7

reset_password 表面响应400

SQL 标记有

预设管理员会话有

/api/user/current200

管理员身份is_superuser: true

v0.62.9

reset_password 表面响应拒绝额外字段

SQL 标记无

预设管理员会话无

/api/user/current401

管理员身份未建立会话

差分结果排除了“Yakit 把 400 误判成成功”和“会话原本就存在”两种解释。漏洞是否命中,要看目标侧副作用和后续认证状态,不能只看首个响应码。

从攻击链倒推处置动作

先切断入口

升级到对应分支的最低安全版本或更高版本。变更窗口尚未到来时,在反向代理、WAF 或入口网关暂时阻断 POST /api/session/reset_password,并确认没有旁路地址继续暴露。

再清理已经建立的访问能力

升级只关闭后续入口,不会自动撤销攻击者已经拿到的会话和凭据。官方要求删除应用数据库 core_session 表中的所有活动会话,同时复查管理员账号和 API Key。

按数据链路轮换凭据

管理员权限可以触达已保存的数据源连接。所有连接数据库的口令、Token 或证书都要进入轮换清单。轮换完成后,还要检查旧凭据是否在其他系统复用。

追查 400 后面的 200

检测逻辑不要固定匹配某段 SQL 字符串。更稳妥的行为线索是:同一来源短时间访问 reset_password 并收到 400,随后携带新会话访问 user/current 并收到 200。

网关日志只能确认入口行为。应用数据库里的 core_session、管理员变更、API Key、Metabase 活动记录、查询历史和数据仓库审计日志,负责回答权限是否建立、数据是否被访问、凭据是否被使用。

两条规则:一条止血,一条盯住行为

正文只保留两套最有代表性的规则:Nginx 负责临时切断入口,Suricata 负责识别官方公布的“400 后跟 200”。ModSecurity、SIEM 和应用数据库排查模板收在文末附录。规则不能替代版本升级,也不能回答攻击者是否已经导出数据。

Nginx:临时封禁整个重置端点

官方给出的临时措施是阻断 /api/session/reset_password。Nginx 可以在通用 location / 之前加入精确匹配:

location = /api/session/reset_password {
    return 403;
}

应用部署在子路径时,要把子路径加入匹配。加载配置前先执行:

nginx-t&&nginx-s reload

我把片段放进最小 server 配置,并使用 Nginx 1.28-alpine 执行 nginx -t,语法检查通过。

精确封禁会同时暂停正常的密码重置功能。升级并完成失陷排查后,再按业务变更流程撤掉临时规则。

Suricata:跨连接记录 POST → GET → 200

第一条规则识别 reset_password 请求体中的异常 user-id 与 raw 结构,并按源 IP 写入 120 秒 xbit。第二条规则在同一来源访问 user/current 时给当前 TCP 流打标。第三条规则只在这条流返回 200 时告警。

alert http any any -> any any (
  msg:"LOCAL Metabase GHSA-vwf4 suspicious reset_password body";
  flow:established,to_server;
  http.method; content:"POST";
  http.uri; content:"/api/session/reset_password"; startswith;
  http.request_body;
  pcre:"/"user-id"\s*:\s*\{.{0,1024}"raw"\s*:/is";
  xbits:set,metabase_vwf4_stage1,track ip_src,expire 120;
  noalert;
  sid:42026001; rev:1;
)

alert http any any -> any any (
  msg:"LOCAL Metabase GHSA-vwf4 follow-up user current";
  flow:established,to_server;
  http.method; content:"GET";
  http.uri; content:"/api/user/current"; startswith;
  xbits:isset,metabase_vwf4_stage1,track ip_src;
  flowbits:set,metabase_vwf4_admin_check;
  noalert;
  sid:42026002; rev:1;
)

alert http any any -> any any (
  msg:"LOCAL Metabase GHSA-vwf4 suspected admin session established";
  flow:established,to_client;
  http.stat_code; content:"200";
  flowbits:isset,metabase_vwf4_admin_check;
  flowbits:unset,metabase_vwf4_admin_check;
  classtype:attempted-admin;
  sid:42026003; rev:1;
)

track ip_src 让第一阶段标记跨 TCP 连接保留;flowbit 把第二次 GET 与同一 TCP 流里的 200 响应关联。部署时还要把检测范围收敛到 Metabase 服务地址,避免同一传感器上的无关 HTTP 流量增加状态表压力。

启用前先检查语法,再用留存 PCAP 回放:

suricata-T-c /etc/suricata/suricata.yaml -S metabase-ghsa-vwf4.rules
suricata-k none -r yakit-metabase-ghsa-vwf4.pcapng 
-c /etc/suricata/suricata.yaml 
-S metabase-ghsa-vwf4.rules -l ./suricata-out

我用 Suricata 8.0.3 对三条规则做了语法检查,配置加载成功。随后回放前文的 24 包 PCAP,SID 42026003 产生一条 Attempted Administrator Privilege Gain 告警。告警事务对应 /api/user/current、HTTP 200 和 778 字节响应体。

本地 PCAP 来自 macOS 回环接口,校验和由协议栈后续处理,离线文件里会显示无效校验和,因此回放命令加入 -k none。在线传感器是否需要这个参数,应按抓包位置和网卡卸载配置判断。

HTTPS 流量在没有解密点时看不到 URI 与 JSON。规则应部署在 TLS 终止后的镜像口、反向代理侧或具备解密能力的检测设备上。

HTTP 400 只说明密码重置业务最终拒绝了 token。v0.62.7 的数据库副作用发生得更早,管理员会话已经越过了响应码这道表象。

Yakit 给出了请求端和会话端结果,Wireshark 保留了两条原始 HTTP 流,v0.62.9 提供了只改版本的负对照。三组证据指向同一个判断:安全验证要追踪服务器实际改变了什么,而不是停在客户端看到了什么。

受影响实例的优先级也很清楚:升级、撤销会话、轮换凭据、查历史。四项动作缺少任何一项,都可能留下已经建立的访问能力。

更多检测规则

下面三套模板留给需要直接落地的应急团队。公众号正文默认收起,完整文件也保存在随文规则包中。

ModSecurity:入口网关紧急阻断

使用 ModSecurity 或兼容 SecLang 的网关时,可以按 URI 在 phase 1 直接拒绝请求:

SecRule REQUEST_URI "@rx ^/api/session/reset_password(?:\?.*)?$" 
  "id:42026010,phase:1,t:none,deny,status:403,log,msg:'Temporary block for GHSA-vwf4-m7j8-wcjf'"

规则 ID 使用本地自定义区间,部署前要避开现有规则。反向代理如果会重写 URI,应匹配重写后的实际变量,并在审计日志中确认规则命中。

SIEM:按来源关联官方攻击模式

接入 ECS 风格的反向代理或应用访问日志后,可以用 EQL 关联同一源 IP 两分钟内的两个事件:

sequence by source.ip with maxspan=2m
  [any where
    http.request.method == "POST" and
    url.path == "/api/session/reset_password" and
    http.response.status_code == 400]
  [any where
    http.request.method == "GET" and
    url.path == "/api/user/current" and
    http.response.status_code == 200]

字段名要按日志平台映射。生产环境还应加入 destination.ip、虚拟主机、实例 ID 或 Kubernetes workload,防止 NAT 后的共享源 IP 把两个实例的事件误关联。告警触发后,继续核对第二次请求中的会话字段、管理员变更和数据查询行为。

应用数据库:检查异常会话与管理员

不同版本可能调整字段。先查询列名,再按事件窗口检查 core_session 和管理员账号:

SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_name IN ('core_session', 'core_user')
ORDERBY table_name, ordinal_position;

SELECTid, user_id, created_at, key_hashed
FROM core_session
WHERE created_at >=TIMESTAMP'<UTC_START>'
ORDERBY created_at DESC;

SELECTid, email, is_superuser, is_active
FROM core_user
WHERE is_superuser ISTRUE
ORDERBYid;

时间范围至少从实例首次对外暴露或漏洞影响版本上线开始。会话清理前先按事件响应流程保存数据库快照和查询结果;证据保全结束后,再执行官方要求的会话撤销。

参考资料

  1. Metabase 官方安全更新:Security update available for Metabase
  2. GitHub Security Advisory:GHSA-vwf4-m7j8-wcjf
  3. 社区实验仓库:dinosn/metabase-ghsa-vwf4-lab
  4. BleepingComputer:Framework、Tally 披露 Metabase 数据窃取事件
  5. Suricata 官方文档:Xbits Keyword
  6. OWASP Core Rule Set:SecRule 编写说明
  7. Elastic 官方文档:EQL sequence 语法

原文始发于微信公众号(MessFreeSecurity):别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2026年8月11日08:43:31
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   别被 HTTP 400 骗了:Metabase CVSS 10 零日复现与流量还原https://cn-sec.com/archives/5386797.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息