PHP代码审计之WEB安全系列基础文章(十二)之XXE篇

【如需转载,请详细表明来源,请勿设置原创】

嗨,大家好,我是闪石星曜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 &#x25; 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实体,将实体中的内容解析并输出到前端页面

image-20220807181728222

4.2 无回显XXE(bind XXE)

无回显XXE则是返回包没有任何回显内容,我们无法在响应包中得到我们想要的数据。

同样的测试代码这里却没有回显

image-20220807182307608

这种情况下我们如何判断该处是否存在XXE漏洞呢?

其实我们可以引用外部参数实体,通过DNSLOG回显来判断此处是否存在XXE,然后在进行下一步的利用。

image-20220807200101166

image-20220807200041079

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(TRUEFALSE,例如 LIBXML_NOBLANKS(1))进行设 置。
ns 可选。规定命名空间前缀或 URI
is_prefix 可选。规定一个布尔值。如果 ns 是前缀则为 TRUE,如果 nsURI 则为 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 // <<<XX为PHP定界符格式,在其中的内容会被原样输出
<note>
<to>PHP</to>
<from>is best</from>
<heading>in the</heading>
<body>world!</body>
</note>
XML; //定界符结尾

$xml=simplexml_load_string($note);
//var_dump($XML);
echo $xml->getName() . "<br>";

foreach($xml->children() as $key=>$value)
{
echo $key . ": " . $value . "<br>";
}
?>

image-20220808135547000

不仅可以引用内部实体,重要的是可以引用外部实体。

代码示例:

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也不会进行读取。

image-20220808170045646

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(TRUEFALSE,例如 LIBXML_NOBLANKS(1))进行设 置。
ns 可选。规定命名空间前缀或 URI
is_prefix 可选。规定一个布尔值。如果 ns 是前缀则为 TRUE,如果 nsURI 则为 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>";
}
?>

image-20220811174812578

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协议读取文件。

image-20220812101303799

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();
?>

image-20220812110637007

5.XXE漏洞

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

安装完成界面如下

image-20220812144238980

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

image-20220812145948579

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

image-20220812145849320

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

image-20220812150906447

image-20220812161239861

6.XXE代码审计总结

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

PHP代码审计之WEB安全系列基础文章(十二)之XXE篇
http://example.com/2022/10/10/PHP代码审计之WEB安全系列基础文章(十二)之XXE篇/
作者
Power7089
发布于
2022年10月10日
许可协议