【如需转载,请详细表明来源,请勿设置原创】
嗨,大家好,我是闪石星曜CyberSecurity创始人Power7089。
欢迎大家扫描下方二维码关注 “闪石星曜CyberSecurity” 公众号,这里专注分享渗透测试,Java代码审计,PHP代码审计等内容,都是非常干的干货哦。

今天为大家带来PHP代码审计基础系列文章第十二篇之XXE篇。
这是【炼石计划@PHP代码审计】知识星球第二阶段的原创基础系列文章,拿出部分课程分享给零基础的朋友学习。本系列原创基础文章涵盖了PHP代码审计中常见的十余种WEB漏洞,是匹夫老师精心的创作,欢迎关注我的公众号跟着一起学习。
【炼石计划@PHP代码审计】是一个系统化从入门到提升学习PHP代码审计的成长型知识星球。这里不仅注重夯实基础,更加专注实战进阶。强烈推荐加入我们,一起来实战提升PHP代码审计。

PHP代码审计之XXE(XML外部实体注入)
1.XXE原理
xxe其实也叫做XML外部实体化注入,该漏洞是由于站点某些功能处可以人为引入不安全的外部实体数据,在处理数据时引发的不安全问题。
那为什么会叫做外部实体注入而不叫内部实体注入呢?
其实是由于XXE的危害主要来自于dtd文件中引入的外部实体,例如可以通过一些伪协议:如file、ftp、data、phar等来达到我们想要的效果。
2.XML基础知识
我们在学习挖掘XXE漏洞时,首先得了解一下XML语法知识以及如何构造XML文档结构,这对于后面XXE漏洞利用是必要的一步。
2.1 XML文档实例
XML文档结构包括XML声明
、DTD文档类型定义
(可选)、文档元素
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!--XML声明,它定义 XML 的版本(1.0)和所使用的编码(UTF-8 : 万国码, 可显示各种语言)-->
<?xml version="1.0" encoding="UTF-8"?>
<!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)> <!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)> <!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)> <!--定义body元素为”#PCDATA”类型-->
]]]>
|
<note></note>
描述文档的根元素;中间的<to></to>...<body></body>
4 行描述根的 4 个子元素,这些元素都可以由用户自定义,格式有点像HTML、但是比HTML更加灵活。
1 2 3 4 5 6 7
| <!--文档元素--> <note> <to>I</to> <from>Like</from> <heading>PHP</heading> <body>PHP is the best in the world!</body> </note>
|
2.2 DTD(文档类型定义)
DTD的作用是用来规定文档元素类型的,DTD可以在XML文档内部申明,也可以引用外部DTD。
DTD代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)> <!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)> <!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)> <!--定义body元素为”#PCDATA”类型-->
]]]>
|
DTD分为内部申明和外部申明,我们所要利用的就是DTD中的外部申明。
内部申明代码示例:
内部申明格式:<!DOCTYPE 根元素 [元素申明]>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
<!--文档元素--> <note> <to>PHP is</to> <from>the best</from> <heading>in the</heading> <body> world!</body> </note>
|
外部申明代码示例:
外部申明格式:<!DOCTYPE 根元素 SYSTEM ”外部DTD文件“>
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note SYSTEM "http://IP/eval.dtd">
eval.dtd中的内容如上述DTD格式相同 <!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
|
构成DTD文件内容的叫做DTD实体,DTD实体分为内部实体、外部实体,内外部实体,又存在一般实体、参数实体。
一般实体的引用方式为:&实体名
参数实体的引用方式为:%实体名
注意:在定义参数实体时%与参数实体中间要有空格分割且参数实体只能在DTD内部申明和引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!DOCTYPE ceshi [
<!ENTITY to "PHP"> <!-- 内部一般实体 -->
<!ENTITY % from "is the best"> <!-- 内部参数实体 -->
<!ENTITY heading SYSTEM "http://ip/eval.dtd"> <!-- 外部一般实体 -->
<!ENTITY % body SYSTEM "file:///eval.dtd"> <!-- 外部参数实体 -->
%from; <!-- 引用参数实体 -->
]>
<ceshi>&heading;</ceshi> <!-- 引用一般实体 -->
|
参数实体可以嵌套进行使用,但是注意在里面的参数实体%要进行HTML实体编码。
1 2 3 4 5 6 7
| <!DOCTYPE ceshi [
<!ENTITY % body '<!ENTITY % content SYSTEM "http://ip/eval.dtd">'>
]>
<ceshi>&content;</ceshi>
|
4.XXE分类
4.1 有回显XXE
有回显XXE顾名思义就是在响应包中回显我们传入payload的结果建议大家做实验的时候使用PHP5.2、5.3、5.4因为在这些版本中libxml的版本还是2.7.X,在libxml版本大于2.9.1的时候PHP默认已经不解析外部实体了,如果是高版本PHP需要代码中libxml_disable_entity_loader(false)
开启。
这里我们构造DTD实体,将实体中的内容解析并输出到前端页面

4.2 无回显XXE(bind XXE)
无回显XXE则是返回包没有任何回显内容,我们无法在响应包中得到我们想要的数据。
同样的测试代码这里却没有回显

这种情况下我们如何判断该处是否存在XXE漏洞呢?
其实我们可以引用外部参数实体,通过DNSLOG回显来判断此处是否存在XXE,然后在进行下一步的利用。


3.XXE相关函数
1
| 建议大家做实验的时候使用PHP5.2、5.3、5.4,因为在这些版本中libxml的版本还是2.7.X,而在libxml版本**大于2.9.1**的时候PHP默认已经不解析外部实体了,如果是高版本PHP需要手动添加`libxml_disable_entity_loader(false)`开启。
|
在PHP代码审计中常见的能够解析XML函数如下
simplexml_load_string()
1 2 3 4 5 6 7 8 9 10 11 12 13
| 定义和用法: 该函数转换形式良好的XML字符串为 SimpleXMLElement 对象。
语法: simplexml_load_string(data,classname,options,ns,is_prefix); data 必需。规定形式良好的 XML 字符串。 classname 可选。规定新对象的 class。 options 可选。规定附加的 Libxml 参数。通过指定选项为 1 或 0(TRUE 或 FALSE,例如 LIBXML_NOBLANKS(1))进行设 置。 ns 可选。规定命名空间前缀或 URI。 is_prefix 可选。规定一个布尔值。如果 ns 是前缀则为 TRUE,如果 ns 是 URI 则为 FALSE。默认是 FALSE。
返回值: 如果成功则返回SimpleXMLElement对象,如果失败则返回FALSE。
|
代码示例:
输出XML字符串中每个节点的元素名以及元素中的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php $note=<<<XML <note> <to>PHP</to> <from>is best</from> <heading>in the</heading> <body>world!</body> </note> XML; $xml=simplexml_load_string($note);
echo $xml->getName() . "<br>"; foreach($xml->children() as $key=>$value) { echo $key . ": " . $value . "<br>"; } ?>
|

不仅可以引用内部实体,重要的是可以引用外部实体。
代码示例:
1 2 3 4 5 6
| <?php $note=file_get_contents('php://input'); echo $note; $xml = simplexml_load_string($note); echo $xml; ?>
|
php://input
个可以访问请求的原始数据的只读流。当请求方式是post,并且Content-Type不等于”multipart/form-data”时,可以使用php://input来获取原始请求的数据。
个人理解php://input
类似于$_POST
,只是当Content-Type为multipart/form-data
时也就是上传文件时即使请求体中有数据php://input也不会进行读取。

simplexml_load_file()
该函数与simplexml_load_string()
函数的区别在于simplexml_load_file()
函数传入的是xml文件而simplexml_load_string()
传入的则是字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 定义和用法: 该函数转换指定的 XML 文件为 SimpleXMLElement 对象。
语法: simplexml_load_file(file,classname,options,ns,is_prefix); file 必需。规定 XML 文件路径。 classname 可选。规定新对象的 class。 options 可选。规定附加的 Libxml 参数。通过指定选项为 1 或 0(TRUE 或 FALSE,例如 LIBXML_NOBLANKS(1))进行设 置。 ns 可选。规定命名空间前缀或 URI。 is_prefix 可选。规定一个布尔值。如果 ns 是前缀则为 TRUE,如果 ns 是 URI 则为 FALSE。默认是 FALSE。
返回值: 如果成功则返回 SimpleXMLElement 对象,如果失败则返回 FALSE。
|
代码示例:
note.xml
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="ISO-8859-1"?> <note> <to>PHP</to> <from>is best</from> <heading>in the</heading> <body>world!</body> </note>
|
输出XML字符串中每个节点的元素名以及元素中的值
1 2 3 4 5 6 7 8 9 10 11
| <?php
$xml=simplexml_load_file("note.xml"); //var_dump($XML); echo $xml->getName() . "<br>";
foreach($xml->children() as $key=>$value) { echo $key . ": " . $value . "<br>"; } ?>
|

simplexml_import_dom()
1 2 3 4 5 6 7 8 9 10
| 定义和用法: 该函数从 DOM 节点返回 SimpleXMLElement 对象。
语法: simplexml_import_dom(node,classname); node 必需。规定 DOM 元素节点。 classname 可选。规定新对象的 class。
返回值: 如果成功则返回 SimpleXMLElement 对象,如果失败则返回 FALSE。
|
代码示例:
首先需要调用DOMDocument
类下的loadXML
方法来解析指定的 XML 文本串,然后在调用simplexml_import_dom()
函数将XML文本串转换为SimpleXMLElement
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$xml = simplexml_import_dom($dom);
echo $xml;
?>
|
解析外部实体,并通过file协议读取文件。

asXML()
1 2 3 4 5 6 7 8 9
| 定义和用法: 该函数格式化 XML(版本 1.0)中的 SimpleXML 对象的数据。
语法: asXML(filename); filename 可选。规定需要写入数据的文件的名称。
返回值: 如果成功则返回一个字符串,如果失败则返回 FALSE。如果指定了 filename 参数,成功则返回 TRUE,失败则返回 FALSE。
|
实例化一个SimpleXMLElement
类,然后调用类中的方法asXML()
将XML文本串转换为字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php $note=<<<XML <note> <to>PHP</to> <from>is best</from> <heading>in the</heading> <body>world!</body> </note> XML;
$xml=new SimpleXMLElement($note); echo $xml->asXML(); ?>
|

5.XXE漏洞
此次xxe漏洞就以XXE-Lab
靶场为例,只需要将源码放入phpstudy的www目录下访问即可,靶场源码会随文档一起打包下发。
安装完成界面如下

burp抓包发现,该登录方法通过xml格式来传递登录需要的数据

查看前端代码,在代码102行,自定义函数doLogin()
,接收两个参数并将参数以xml形式传递给doLogin.php
文件进行处理,在代码118行,通过填入的username
和password
值,通过后端返回的状态码在前端展示提示信息。

在代码第12行,使用php://input
接收POST数据,通过loadXML()
方法来解析我们传入的XML内容,最后通过simplexml_import_dom()
将内容转换为SimpleXMLElement
对象,上面传入到解析XML的过程中没有任何过滤已经造成了xxe,且代码32行会将我们输入的内容进行输出,从而我们可以判定此处xxe存在回显。


6.XXE代码审计总结
1
| 对于XXE来说我们首先要了解XML的语法以及基础结构,这样我们在发现XXE的时候可以轻松的构造可利用的payload,对于危险函数我们要了解各函数的用法以及他们之前的区别,这样对于大家在后续审计项目的时候会有很大的帮助。
|