ARTEX渗透平台密码被改引出的0DAY"

admin 2026-10-22 13:00:11 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

ARTEX 渗透平台密码被改引出的0DAY

原创

Kratos
Kratos

Kratos Sec

2026年10月18日 09:00
北京

去阅读

ARTEX渗透平台密码被改引出的0DAY攻击

00. 事情原因

先交代一下主角。ARTEX(0.3.7)是自部署的一个渗透测试指挥平台。主要是测试一下AI渗透能力,该平台包括扫描任务调度、资产台账、漏洞 findings 归档。

平台只有一个用户,密码只存在自己的密码管理器里,没有落在任何第三方。

某天下午,像往常一样打开登录页想看看放了好几天是否挖到漏洞,输入密码。

登录失败。

再输一次。还是失败。第三次,我把密码管理器打开对着敲。

还是失败。

排除了 Caps Lock,排除了记错,剩下唯一的解释指向一个我不想接受的方向:数据库里存的哈希变了。 bcrypt 不可逆,我没法\"看\"密码是什么,但密码学上\"登录一直失败 + 我确定自己没输错\"只有一种成立方式——

有人改了我的密码。

ARTEX 登录页

这是个只有我在用的平台,跑在我自己的服务器上,PostgreSQL 端口根本没发布到公网。攻击者是怎么进来的?

后来的答案比想象中严重得多:这不是一次撞库,不是一次弱口令,也不是一次供应链投毒,而是 ARTEX 自己的一个 0-day——一个可以让任何人在零凭证前提下接管管理员账号的认证绕过漏洞。而攻击者早在正式动手前一天,就在我眼皮底下完成了一次 20 分钟的完整彩排。


01. 铁证:一个 UPDATE 的时间戳

应急第一步不是扑上去改密码,而是固定证据。

ARTEX 的登录密码存在 PostgreSQL 的 settings 表里。这张表是个键值结构,key='auth.password_hash' 的那行 value 存 bcrypt 哈希,updated_at 在每次 UPDATE 时自动刷新。

直接查库:

SELECT updated_at FROM settings WHERE key='auth.password_hash';
-- 2026-10-11 11:29:15 UTC   ← 没有做过任何操作

一个时间戳能说明什么?说明很多:

  1. 写入时间精确到秒——11:29:15,这是后续所有取证工作的锚点,所有日志分析都以这个时刻为圆心展开;
  2. 那个时间点我没有登录过服务器,没有碰过数据库,Web 界面也没有人操作——应用层当时还有日志可查,零记录;
  3. updated_at 只有 UPDATE 会刷,SELECT 不会——这行数据被人写过,不是幻觉。

但光有时间戳还不够。我想知道攻击者写入的是什么。bcrypt 不可逆,那就换一条路:从 PostgreSQL 的尸体堆里把旧版本翻出来。

这里补一点背景知识。PostgreSQL 的 MVCC 机制下,UPDATE 从来不是原地覆盖:数据库会写入一个新版本的行,把旧版本打上事务标记变成\"死元组\"(dead tuple),死元组的尸体留在数据页里,等 VACUUM 来收尸。只要还没被收走,历史就完整地躺在磁盘上。

pageinspect 扩展可以直接读原始数据页:

CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT lp, t_xmin, t_xmax, t_field3
FROM heap_page_items(get_raw_page('settings', 0));

逐页翻过去,在 lp=55 的位置找到了目标死元组——xmin=217280,正是 11:29:15 那次写入留下的版本。把 tuple data 解出来,就是攻击者设置的那枚哈希:

攻击者写入的哈希

$2a$10$zuhxhObqCgtq2822q8Zn3e3.rpuZYlhY8hkIA6u30gLCl7KcDpx6C

拿到哈希能干什么?离线爆破。$2a$10$ 的 cost factor 是 10,单机跑 184 个候选口令很快。我塞进去 184 个常见弱口令——top 弱口令表、键盘\_walk 模式、和这个平台相关的词——零命中。

这个结果本身就是情报:

  • 攻击者设置的是一个随机强口令,不是\"admin123\"这种顺手一改;
  • 他的目的不是\"改着玩\",而是把我锁在外面、把自己锁在里面;
  • 这不是脚本小子的随手一试,这是一次有备而来的定向动作。

到这里,事件定性完成:10-11 11:29:15,一个未知攻击者通过未知路径修改了管理员密码。接下来是整个应急里最关键的问题——他是怎么进来的?


02. 排除法:五条\"不可能\"路线,逐条排掉

改密码的前提是先拿到某种权限。把传统入侵路径挨个过一遍,每一条都留下了排查记录:

路线一:SSH 爆破。

$ grep -c 'Accepted' /var/log/secure     # 10-09 ~ 10-12 全程
0

四天日志零条 Accepted,authorized_keys 为空。没有人从 SSH 进来过。

路线二:直连数据库。 5432 端口没有发布到宿主机,docker inspect 里该端口映射为空,公网实测拒连。PostgreSQL 从物理上不可直达。

路线三:容器/宿主机植入。 进程清单干净,没有陌生进程;crontab 里只有腾讯云监控组件;docker ps 里只有平台自身的容器。没有 Webshell 落地的迹象。

路线四:默认 jwt.key。 有些自部署工具会被\"镜像里写死的默认签名密钥\"打穿(全网同一把钥匙,伪造 token 畅通无阻)。查了:这个镜像里不存在预置 jwt.key,首次启动时随机生成——这条路不存在。

路线五:init / change-password 接口直接绕过。 正常状态下这两个接口守卫齐全,我试了空 body、缺字段、畸形 Content-Type、路径大小写变换、重放——全部被正确拦截。接口本身,在数据库健康的时候,没有问题。

五条路线,全部排除。于是出现了一个很拧巴的局面:

攻击者没有进入服务器,没有碰过数据库端口,没有植入任何东西,甚至可能从来没有成功登录过任何服务——但他就是把管理员密码改了。

这不合理。而安全事件里,\"不合理\"通常意味着一件事:存在一条我不知道的路径。找到它。


03. 18625 次 FATAL:数据库日志里的完整脚印

第二取证现场是 PostgreSQL 容器日志。pg 拒绝连接时会记 FATAL,把这些 FATAL 按小时做直方图,一行管道就够:

$ docker logs artex-postgres-1 2>&1 \\
    | grep -oE \"2026-10-(09|10|11).*FATAL\" | cut -c1-13 | sort | uniq -c

FATAL 按小时统计

结果是一个教科书级的三段式:

时段 FATAL 次数 语义
10-09 11 时 20 试探性访问
10-10 14 时 114 加码验证
10-10 16:03–16:22 18625 全流程彩排,约 15 连接/秒持续 20 分钟
10-11 11 时 45 总攻爆发,3 秒后密码被写入

逐段拆开看。

10-09 的 20 次,是低成本试探:这个端口能不能通、这个代理能不能用、拒绝时会留下什么痕迹。

10-10 14 时的 114 次,参数校准:确认上一次的行为可复现,量级开始爬坡。

10-10 16:03 的 18625 次是重头戏。这些 FATAL 全是同一个错误:FATAL: sorry, too many clients already——PostgreSQL 的连接槽(本机 max_connections=100)被打满后,后续所有新连接一律拒绝。18625 次 ÷ 20 分钟 ≈ 每秒 15 个新连接,持续不断。

注意时间:彩排在 16:22 结束。第二天 11:29:12,攻击者回来,只打了 45 次 FATAL,3 秒之后——11:29:15——密码哈希落库。

时间线严丝合缝。20 分钟的彩排和 45 次的总攻之间隔了大概 19 个小时,这个节奏说明攻击者用彩排验证了三件事:隧道能不能通、连接池能不能打满、打满之后有没有告警。 三个问题的答案都令他满意——因为没有任何告警响。

对比一下彩排和总攻的形态差异,还能读出更多东西:彩排是 20 分钟的轰炸,总攻只有 45 次的短促突击。他不是在蛮干,第一天测好了所有参数,第二天回来打的是精确制导。

顺带一个毛骨悚然的细节。平台的应用日志服务 logsink,恰好死在 10-10 16:22——彩排结束的那一刻。此后它再也没有被拉起来,应用层日志从攻击前夜开始就永久沉默。正式攻击发生时,应用层痕迹为零。是压力波及的巧合,还是刻意的反取证?没有证据能定论。但结果等价:在攻击者动手的那个上午,我是瞎的。


04. 根因:一个 fail-open 的守卫

排除法排掉了所有传统路径之后,嫌疑集中到两个仍然暴露在公网的入口上:8787 的 Web 应用,和 8788 的一个 HTTP 代理。谜底在 8787 的 /api/auth/init 接口。

先说这个接口的设计。ARTEX 首次部署时,管理员通过 POST /api/auth/init 设置初始账号密码。为了防止别人在你初始化之后再次调用它重置密码,接口入口处有一个守卫——先读数据库,确认\"是否已初始化\",已初始化就 403 拒绝。

问题出在这段守卫的错误处理上。还原出来的逻辑:

initialized, err := db.GetAuthStatus()   // SELECT ... WHERE key='auth.password_hash'
if err != nil {
    // 读取失败时的处理 ← 灾难在这里:错误被静默吞掉,没有 return
}
if !initialized {
    // 放行:进入\"首次设置密码\"流程
    setPassword(w, r)                     // ← 攻击者的输入直接落库
    return
}
http.Error(w, \"already initialized\", 403)

当数据库读取报错时,错误被吞掉,initialized 保持零值 false——\\\"读不到\"被当成了\"不存在\"\\。

用安全工程的术语说,这叫 fail-open:守卫在异常状态下不是关门,而是开门。与之相对的 fail-close 是:读不到状态时,拒绝放行。认证系统的所有守卫,原则上必须 fail-close,因为\"不确定\"和\"安全\"之间,只能选安全。

这也解释了为什么这个漏洞能活到生产环境:所有开发自测、所有 QA 用例,跑在\"数据库健康\"的世界里。 守卫在数据库正常时无懈可击(我 02 节的畸形请求测试也证明了这点)。它的触发条件恰好是\"数据库不可用\"——而这是一个攻击者可以人为制造的状态。

于是整条攻击链闭环了。逐步拆解:

第一步:找到内网桥。 平台的 8788 端口跑着一个 HTTP 代理,无鉴权、无目标白名单。HTTP 代理的 CONNECT 方法本来是给 HTTPS 穿透用的——CONNECT 172.19.0.2:5432 一发,代理就和 Docker 内网的 PostgreSQL 建立了一条 TCP 隧道。\"5432 不对公网开放\"这道防线,被 8788 直接归零。

第二步:造墙。 通过隧道和 pg 建立连接,但只发半截启动包。PostgreSQL 的握手分两段:TCP 连接建立后,服务端立刻 fork 一个后端进程(占住一个连接槽),然后等客户端发完整的 StartupMessage 才进入认证。发一半不发了,后端进程就傻等——槽位被占满,且全程不需要任何凭证。彩排证明 110 条这样的隧道就能灌满 100 个槽。

第三步:制造 fail-open。 应用自己的连接池被墙挤干,init 守卫的那次 SELECT 报错,错误被吞,接口认定\"系统未初始化\"。

第四步:泄压写入。 这是最精妙的一步,也是 45 次 FATAL 只打 3 秒的原因。守卫放行之后,设置密码的 UPDATE 也需要一个连接槽才能落库——墙不拆,写不进。攻击者的节奏是:短促打满 → 守卫读取失败的瞬间把 init 请求送进去 → 立刻泄压 → UPDATE 落进空出来的槽位。11:29:12 爆发,11:29:15 落库,3 秒完成。

第五步:收下战利品。 init 成功的响应里直接返回了管理员 JWT。原密码即刻作废。

全程零凭证、零 JWT、零落盘、零成功登录。 这就是为什么 02 节所有传统入侵路径的排查全是空——攻击者压根没有\"入侵\",他只是让数据库\"喘不上气\"了 3 秒钟。

定性之后还差一块拼图:可验证的直接证据。取证阶段做了一个可逆实验——用 iptables 瞬间掐断应用与数据库之间的所有连接(效果等价于连接池被打满,但完全可控可逆),然后观察:

  • /api/auth/status 立刻从 {\"initialized\":true}(见下图)翻转为 {\"initialized\":false};
  • POST /api/auth/init 越过守卫,直达写入步骤。

/api/auth/status

fail-open 的直接证据,可复现,可录像。


05. 复现

到这里,事件在\"定性\"层面已经闭环:代码审读找到了 fail-open,可逆实验证明了行为。但对一份要交给上游的 0-day 报告来说,还差最后一块:真实攻击者视角的端到端利用证明——公网、零特权、不知道任何内部信息,能不能稳定打出来?

最终产出是一个 URL 参数化的 PoC 脚本:

PoC 文件头

文件头的漏洞链描述和参数默认值如上。核心参数表:

参数 值 含义
pg 172.19.0.2:5432 隧道 CONNECT 的内网目标
holders 340 墙的规模(彩排证明 110 条可打满,340 条为竞态窗口内的冗余压制)
release_at 0.06 发射 init 后多少秒释放占位连接
release_n 2 释放几个占位连接给写入让路
rounds 3 远程模式的最大尝试轮数

PoC 支持两种模式,设计意图完全不同:

本地模式(服务器本机,有 root,用于确定性验证)。时序是编排好的:

T0        iptables 精确杀掉应用↔pg 的连接(应用连接池瞬间蒸发)
T0+ε      守卫读取失败 → gate 打开
T0+δ      POST /api/auth/init(携带新密码)
T0+δ+0.06s  只释放 2 个占位连接 ← pg 槽位恰好让出写入所需的空间
T0+δ'     UPDATE 落进空槽 → 200 OK + 管理员 JWT

0.06 秒这个数字是调出来的:释放早了,应用自己的重连会抢走槽位;释放晚了,init 请求超时。窗口之内,一击命中,直接拿到管理员 JWT。

远程模式(纯攻击者视角,公网零特权,用于可利用性证明)。没有 iptables,只有 8788 的 CONNECT 隧道:建墙(约 340 条,实测同量级 345 条稳定)→ 用并发压力持续榨干应用连接池 → 在压力窗口内反复发射 init。远程模式没有本地模式的精确编排,守卫失败与写入落地之间的竞态靠概率逼近,所以需要 rounds——实测第 2 轮第 1 次命中。

复现同时完成了三件事:

  1. 证明:这不是理论漏洞,是端到端可利用的认证接管,公网零特权即可打出;
  2. 可逆:PoC 结束自动把密码恢复原状,全程只动 settings 表一行;
  3. 揪出一个此前没人注意的事实——这个要单独说。

PoC 开跑前,脚本会把库里现存的哈希备份到 /tmp/hash_before.txt。

06. 处置:先止血,再根治

处置分两步走,顺序很重要。

第一步(立刻):轮换 JWT 签名密钥,而不是只改密码。

很多人的第一反应是改密码,但在这个场景下不够。JWT 是无状态的:服务端不存 session,每个请求只验签名。密码改了,攻击者手里已经签发的旧 token 依然有效——他根本不需要再登录。想让存量 token 全部作废,唯一的办法是换签名密钥 /app/jwt.key。

轮换后验证:旧 token 请求 → 401;新登录 → 200。这一步做完,攻击者手里的会话凭证才算死透。

第二步(根治):内核层规则 A。

规则A

iptables -I DOCKER-USER 1 -p tcp --dport 8787 \\
  -m string --string \"POST /api/auth/init\" --algo bm \\
  -j REJECT --reject-with tcp-reset

逐参数拆开看这条规则为什么这样写:

片段 作用
-I DOCKER-USER 1 插到 DOCKER-USER 链首位。这条链是 Docker 网络转发的必经关卡,先于任何容器规则生效——宿主机上唯一\"全生效\"的控制点
--dport 8787 只盯 Web API 端口,不碰别的流量
-m string --string \"POST /api/auth/init\" --algo bm 用 Boyer-Moore 算法在报文载荷里找特征串。可行是因为平台走明文 HTTP;如果上了 TLS,内核字符串匹配就瞎了,那就要换应用层网关
--to 65535 只扫描报文前 64KB,防止深包检查被超长报文拖垮
-j REJECT --reject-with tcp-reset 用 TCP RST 立刻掐断,客户端马上收到失败,不会挂着重试占资源

为什么把止血点放在内核层?因为上游修复周期不可控,而规则 A 部署即刻全网生效、零业务侵入。部署后 PoC 复测 10 轮喷洒,全灭;/api/auth/status、/api/auth/login 等正常业务零误伤。

要诚实说明的是:规则 A 是止血补丁,不是治疗。真正的修复是上游把那段 err 分支改成 fail-close——本质上是一行代码的事。规则 A 是在等上游期间的保险丝。

最后记录一个处置期间观察到的现象:取证时发现约 26 个 DigitalOcean IP 同时挂着 30 条空闲长连接——每个 IP 恰好 2 条,分钟级轮换。这是一支分布式待命舰队:彩排和总攻可能只是先锋,大部队一直挂着随时可以回流。这个观察直接改变了处置的优先级判断——网段拉黑从\"可选\"变成\"必做\"。对面不是一次性兴趣,是编制化投入。


07. 复盘:四个教训

教训一:守卫的 fail-open 是认证系统的暗病。

\"查询报错\"和\"查询结果为否\"是两个世界,前者必须 fail-close。这个模式比想象中普遍——所有\"首次初始化 / 引导 / 重置\"类流程都是重灾区,因为这类代码天然写成\"读不到配置 = 还没初始化\"。值得每个开发者做一次自查:全文搜 if err != nil,看每一个分支后面是不是都跟着 return;再看所有 GetXxxStatus / XxxExists 命名的函数,异常时返回的是不是零值。

教训二:日志要扛得住攻击者盯着打。

logsink 在彩排阶段就被打死,正式攻击的应用层痕迹为零。日志通路和应用通路共享同一个数据库连接池,就是共享命运——攻击者打日志系统是免费送分题:打死了你变瞎,打不死也能耗掉你的存活预算。日志通路要有独立的存活预算、独立的存储,以及最容易被忽略的一条:日志断流本身要告警。

教训三:开放代理是内网穿透的免费高速公路。

8788 上一个无鉴权代理,把\"5432 不对公网开放\"这道防线的实际意义归零。CONNECT 隧道不挑目标,Docker 内网里的任何端口都裸奔。边界上的每一个端口都要回答同一个问题:它能不能被用来够到内网? 能,就要上代理三件套——鉴权、目标白名单、禁止内网网段。

教训四:updated\_at 是最便宜的入侵检测。

这次事件最早的告警信号不是任何安全产品,而是我自己的一次登录失败,和一行带时间戳的 UPDATE。如果攻击者更\"体贴\"一点——不锁死我,只偷看——发现可能要晚上几周。核心凭证表(密码哈希、API key、OAuth secret)的写时间戳,值得每天巡检 diff,一行 SQL 的成本,IDS 级别的价值。


08. 时间线(一图流)

10-09 11:00   攻击者首次试探(20 次 FATAL)
10-10 14:00   加码验证(114 次)
10-10 16:03   彩排开始:15 连接/秒 × 20 分钟 = 18625 次 FATAL
10-10 16:22   彩排结束,logsink 同时死亡,应用层日志从此沉默
10-11 11:29:12 总攻:45 次 FATAL 短促爆发,连接池被打穿后立刻泄压
10-11 11:29:15 密码哈希被写入(updated_at 铁证),init 响应返回管理员 JWT
10-12 15:29   发现登录异常,人工重置密码(后证实未真正生效)
10-13         取证定根因:/api/auth/init fail-open 0-day
              pageinspect 死元组恢复攻击者哈希,184 弱口令零命中
              可逆实验复现 gate 翻转;PoC 端到端复现 ×2(本地+远程)
10-14         jwt.key 轮换(存量 token 全灭)+ 内核层规则 A 上线
              PoC 复测 10 轮全灭,正常业务零误伤;发现 26 IP 待命舰队

09. 写在最后

干攻击这行久了,会有一种职业病:看谁都像漏洞,即使看自己写的东西,也像别人的靶场。

攻与防从来不是两个岗位,是同一双眼睛的两个方向。只防守的人看系统,看到的是“我已经拦了什么”;会攻击的人看系统,看到 的是“如果是我,我从哪进来”。前者修锁,后者修的才是门。

所以,每天出门撬别人锁的人,回家请记得摸摸自己的门。这不叫被害妄想,这叫职业素养。

环境为自有授权测试平台,PoC 不公开细节,方法论供防御参考。

ARTEX渗透平台密码被改引出的0DAY" 网络安全文章

ARTEX渗透平台密码被改引出的0DAY"

本文记录ARTEX渗透平台管理员密码被未知攻击者修改的应急响应全过程。通过数据库时间戳取证、日志分析排除传统入侵路径,最终定位到init接口在数据库异常时fail-open的认证绕过0-day漏洞。攻击链为:利用无鉴权HTTP代理建立内网隧道,通过半截连接打满PostgreSQL连接池,触发应用守卫读取失败,绕过初始化校验并写入新密码获取管理员JWT。文章提供完整攻击链分析、可复现实验与PoC参数,对自部署平台安全建设具有重要参考价值。
页面加载时投放了一个混淆的JS文件,导致多阶段VenomRAT感染" 网络安全文章

页面加载时投放了一个混淆的JS文件,导致多阶段VenomRAT感染"

该文分析了一起多阶段VenomRAT感染链:混淆JS投递后,在启动文件夹创建BAT实现持久化,进而通过PowerShell下载并执行嵌入两个.NETPE载荷的cim.png文件,最终使用RunPE技术将Venombin.exe注入PowerShell\\_ISE进程。C2配置经AES-128-CBC加密,包含HVNC、RDP访问、凭据窃取等功能,文章末尾附有完整IOC列表,可供威胁狩猎与应急响应参考。
你的WiFi正在「看见」你:98k星开源感知平台RuView拆解(穿墙识人+呼吸心率)" 网络安全文章

你的WiFi正在「看见」你:98k星开源感知平台RuView拆解(穿墙识人+呼吸心率)"

本文评测98k星开源WiFi感知平台RuView,其利用ESP32捕获CSI信号实现穿墙人员检测、非接触呼吸心率监测与跌倒识别,并原生接入五大智能家居生态。文章拆解其能力分层、工程生态与隐私威胁模型,指出感知即监控的风险,并给出信道隔离、语义数据分级授权等缓解清单,强调部署需遵守隐私法规并告知被感知人员。
10月16日某行业线上比武初赛wp" 网络安全文章

10月16日某行业线上比武初赛wp"

分析了手机和计算机镜像,提取了涉案团伙的URL、商品文案、利益分配、人员微信ID、访问记录、SSH会话、钱包地址、加密容器文件、订单数据、服务器配置、数据库账户信息等,并解析了相关证书和手机号码。总结了参赛体验,对出题厂商的题目难度提出质疑。
评论:0   参与:  0