前言
Java描述的是一个世界
,程序运行开始时,这个世界
也开始运作,但世界
中的对象不是一成不变的,它的属性会随着程序的运行而改变。
但很多情况下,我们需要保存某一刻某个对象的信息,来进行一些操作。比如利用反序列化将程序运行的对象状态以二进制形式储存与文件系统中,然后可以在另一个程序中对序列化后的对象状态数据进行反序列化恢复对象。可以有效地实现多平台之间的通信、对象持久化存储。
一个类对象想要序列化成功,必须满足两个条件:
-
该类必须实现 java.io.Serlalizable 接口
-
该类的所有属性必须是可序列化的,如果一个属性是不可序列化的,则属性必须标明是短暂的。
出现的场景业务
-
用途: 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
-
在网络上传送对象的字节序列。
一般来说,服务器启动后,就不会再关闭了,但是如果逼不得已需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化将session信息保存起来放在硬盘,服务器重启后,又重新加载。这样就保证了用户信息不会丢失,实现永久化保存。
最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
例子: 淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个session,内存可能吃不消。这时就需要进行对象的活化、钝化,让其在闲置的时候离开内存,将信息保存至硬盘,等要用的时候,就重新加载进内存。
Java中序列化和反序列化的实现
-
序列化:ObjectOutputStream类 -> writeObject()
注:该方法对参数指定的obj文件进行序列化把字节序列写到一个目标输出流中,按照java标准是给文件一个ser
的扩展名
-
反序列化:ObjectInputStream类-> readObject()
注:该方法是从一个输入流中读取字节序列,再把他们反序列化成对象,将其返回
简单反序列化漏洞的Demo
java反序列化漏洞和php反序列化漏洞有着差不多的意思,php反序列化时会执行对应的魔术方法__wakeup(),而Java反序列化时会执行readObject()方法,所以如果readObject()方法被恶意构造的话,就有可能导致命令执行。
PS:有时也会使用readUnshared()方法来读取对象,readUnshared()不允许后续的readObject和readUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。
这是一个继承了Serializable接口的类
package UserTest;
import java.io.IOException;
import java.io.Serializable;
class User implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name){
this.name = name;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec("calc.exe");
}
}
重写了它的Object
方法,将Runtime加入进去
package UserTest;
import java.io.*;
public class test{
public static void main(String args[]) throws Exception{
//先创建对象
User user = new User();
//对象的属性其实设置不设置都可以
user.setName("hacked by ph0rse");
//把object对象储存为字节流的形式
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//将对象写入object文件
os.writeObject(user);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
User user1 = (User) ois.readObject();
System.out.println(user1.getName());
ois.close();
}
}
最终,在调用readObject方法的时候,是可以调用Runtime方法,去执行本地命令的
程序运行的过程可以简化为以下几点:
- 对象被序列化进object文件
- 从object中恢复文件
- 调用被恢复对象的readObject方法
- 重写的readObject方法中Runtime方法被执行
反序列化的起源
刚刚的Demo就是一个对反序列化没有进行安全审查的例子,实战中,这种智障情况一般不会出现。开发时产生的反序列化漏洞常见有以下几种情况:
-
重写ObjectInputStream对象的resolveClass方法中的检测可被绕过。
-
使用第三方的类进行黑名单控制。虽然Java的语言严谨性要比PHP强的多,但在大型应用中想要采用黑名单机制禁用掉所有危险的对象几乎是不可能的。因此,如果在审计过程中发现了采用黑名单进行过滤的代码,多半存在一两个‘漏网之鱼’可以利用。并且采取黑名单方式仅仅可能保证此刻的安全,若在后期添加了新的功能,就可能引入了新的漏洞利用方式。所以仅靠黑名单是无法保证序列化过程的安全的。
基础库中隐藏的反序列化漏洞
commons-fileupload 1.3.1
commons-io 2.4
commons-collections 3.1
commons-logging 1.2
commons-beanutils 1.9.2
org.slf4j:slf4j-api 1.7.21
com.mchange:mchange-commons-java 0.2.11
org.apache.commons:commons-collections 4.0
com.mchange:c3p0 0.9.5.2
org.beanshell:bsh 2.0b5
org.codehaus.groovy:groovy 2.3.9
org.springframework:spring-aop 4.1.4.RELEASE
有些反序列化防护软件便是通过禁用以下类的反序列化来实现保护
'org.apache.commons.collections.functors.InvokerTransformer',
'org.apache.commons.collections.functors.InstantiateTransformer',
'org.apache.commons.collections4.functors.InvokerTransformer',
'org.apache.commons.collections4.functors.InstantiateTransformer',
'org.codehaus.groovy.runtime.ConvertedClosure',
'org.codehaus.groovy.runtime.MethodClosure',
'org.springframework.beans.factory.ObjectFactory',
'xalan.internal.xsltc.trax.TemplatesImpl'
Pop Gadgets
POP Gadgets指的是在通过带入序列化数据,经过一系列调用的代码链,其中POP指的是Property-Oriented Programming,即面向属性编程,和逆向那边的ROP很相似,面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。两者的不同之处在于ROP更关注底层,而POP只关注对象与对象之间的调用关系。
Gadgets是小工具的意思,POP Gadgets即为面向属性编程的利用工具、利用链。
基本库中的反序列化触发机制较为复杂和底层,可以结合ysoserial源码中的exp来进行跟进分析。
发现Java反序列化漏洞
白盒检测
-
检索源码中对反序列化函数的调用来静态寻找反序列化的输入点
ObjectInputStream.readObject
ObjectInputStream.readUnshared
XMLDecoder.readObject
Yaml.load XStream.fromXML
ObjectMapper.readValue
JSON.parseObject
-
确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections等危险库
-
若不包含危险库,则查看一些涉及命令、代码执行的代码区域,防止程序员代码不严谨,导致bug
-
若包含危险库,则使用ysoserial进行攻击复现。
相关简单题目:Java反序列化漏洞从入门到关门 - FreeBuf网络安全行业门户
黑盒测试
在黑盒测试中并不清楚对方的代码架构,但仍然可以通过分析十六进制数据块,锁定某些存在漏洞的通用基础库(比如Apache Commons Collection)的调用地点,并进行数据替换,从而实现利用。
在实战过程中,我们可以通过抓包来检测请求中可能存在的序列化数据。
序列化数据通常以AC ED开始,之后的两个字节是版本号,版本号一般是00 05但在某些情况下可能是更高的数字。
为了理解反序列化数据样式,我们使用以下代码举例:
反序列化的修复
禁止JVM执行外部命令Runtime.exec
SecurityManager originalSecurityManager = System.getSecurityManager();
if (originalSecurityManager == null) {
// 创建自己的SecurityManager
SecurityManager sm = new SecurityManager() {
private void check(Permission perm) {
// 禁止exec
if (perm instanceof java.io.FilePermission) {
String actions = perm.getActions();
if (actions != null && actions.contains("execute")) {
throw new SecurityException("execute denied!");
}
}
// 禁止设置新的SecurityManager,保护自己
if (perm instanceof java.lang.RuntimePermission) {
String name = perm.getName();
if (name != null && name.contains("setSecurityManager")) {
throw new SecurityException("System.setSecurityManager denied!");
}
}
}
@Override
public void checkPermission(Permission perm) {
check(perm);
}
@Override
public void checkPermission(Permission perm, Object context) {
check(perm);
}
};
System.setSecurityManager(sm);
}
不建议使用黑名单
-
在反序列化时设置类的黑名单来防御反序列化漏洞利用及攻击,这个做法在源代码修复的时候并不是推荐的方法。
-
因为你不能保证能覆盖所有可能的类,而且有新的利用payload出来时也需要随之更新黑名单。
-
但有一种场景下可能黑名单是一个不错的选择。写代码的时候总会把一些经常用到的方法封装到公共类,这样其它工程中用到只需要导入jar包即可,此前已经见到很多提供反序列化操作的公共接口,使用第三方库反序列化接口就不好用白名单的方式来修复了。这个时候作为第三方库也不知道谁会调用接口,会反序列化什么类,所以这个时候可以使用黑名单的方式来禁止一些已知危险的类被反序列化,具体的黑名单类可参考contrast-rO0、ysoserial中paylaod包含的类。