目录
前言:
完成 SQL 注入、RCE 漏洞环境的编写后,接下来的任务是想办法构造一个 java 反序列化的漏洞环境,并用代码实现,运行测试。
对于反序列化漏洞而言,使用 php 语言来编写漏洞复现的环境更为容易一些,但是我们的项目环境是使用 java 语言编写的,所以我们需要想办法构造 java 反序列化漏洞。同时由于 java 反序列化漏洞理解起来较难,在学习这个漏洞的原理及利用方式等基础内容时,就花费了很长的时间。所以这篇博客主要记录一下 java 反序列化漏洞的基本知识,包括漏洞简介、危害、利用方式以及如何防范等。
1、java 反序列化简介
1.1、简介
1.1.1、什么是序列化与反序列化?
Java 序列化是指把 Java 对象转换为字节序列的过程;
Java 反序列化是指把字节序列恢复为 Java 对象的过程;
1.1.2、为什么要用序列化与反序列化?
在 为什么要用序列化与反序列化 之前我们先了解一下对象序列化的两种用途:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 在网络上传送对象的字节序列。
我们可以想想如果没有序列化之前,又是怎样一种情景呢?
举例:
Web 服务器中的 Session 会话对象,当有10万用户并发访问,就有可能出现10万个 Session 对象,显然这种情况内存可能是吃不消的。
于是 Web 容器就会把一些 Session 先序列化,让他们离开内存空间,序列化到硬盘中,当需要调用时,再把保存在硬盘中的对象还原到内存中。
我们知道,当两个进程进行远程通信时,彼此可以发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。
同样的序列化与反序列化则实现了 进程通信间的对象传送,发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
初步总结:Java 序列化和反序列化,其一,实现了数据的持久化,通过序列化可以把数据永久的保存在硬盘上;其二,利用序列化实现远程通信,即在网络上传递对象的字节序列。
1.1.3、什么是 java 反序列化漏洞
Java 反序列化漏洞是与 java 相关的漏洞中最常见的一种,也是网络安全工作者关注的重点。在cve中搜索关键字serialized共有174条记录,其中83条与java有关;搜索deserialized共有20条记录,其中10条与java有关。这些出现反序列化漏洞的框架和组件包括的大名鼎鼎的spring,其中还有许多Apache开源项目中的基础组件。例如Apache Commons Collections。 这些基础组件大量被其他框架或组件引用,一旦出现漏洞就会引起大面积的网络安全事故,后果非常严重。
需要序列化的对象必须实现@serializable接口。需要注意的是,如果被序列化或反序列化的类中存在writeObject()|readObject()方法,则在进行序列化|反序列化之前就会调用该方法。这通常是引起反序列化漏洞的一个重要特性。
如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。
下面通过一段简单的代码认识一下java的序列化与反序列化:
首先定义一个User类用于序列化:
public class User implements Serializable{
private int age;
private String username;
private String password;
User(){
this.age = 10;
this.username = "test";
this.password = "test";
}
//在序列化之前被调用
private void writeObject(ObjectOutputStream os) throws IOException {
os.defaultWriteObject();
System.out.println("readObject is running!");
}
//在反序列化之后被调用
private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException {
is.defaultReadObject();
System.out.println("writeObject is running!");
}
@Override
public String toString() {
return "User{" + "age=" + age + ", username='" + username + '\'' + ", password='" + password + '\'' + '}';
}
}
然后进行序列化|反序列化操作:
public static void main(String args[]) throws IOException, ClassNotFoundException {
User user = new User(); //将序列化对象存储在serialize_data中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serialize_data"));
System.out.println("serialize");
oos.writeObject(user);//序列化
oos.close(); //存储在serialize_data中的对象反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serialize_data"));
System.out.println("deserialize");
User userDeserialize = (User)ois.readObject();//反序列化
System.out.println(userDeserialize.toString());
ois.close();
}
//输出结果 /* serialize readObject is running! deserialize writeObject is running! User{age=10, username='test', password='test'} */
可以看出,自定义的readObject|writeObject方法确实在序列化反与反序列化的过程中被调用了。
1.2、危害
java 反序列化漏洞危害较大,可造成远程代码执行、获取 shell 等高危操作。
1.3、利用
-
漏洞触发场景
在java编写的web应用与web服务器间java通常会发送大量的序列化对象例如以下场景:
1)HTTP请求中的参数,cookies以及Parameters。
2)RMI协议,被广泛使用的RMI协议完全基于序列化
3)JMX 同样用于处理序列化对象
4)自定义协议 用来接收与发送原始的java对象
-
漏洞挖掘
(1)确定反序列化输入点
首先应找出readObject方法调用,在找到之后进行下一步的注入操作。
一般可以通过以下方法进行查找:
1)源码审计:寻找可以利用的“靶点”,即确定调用反序列化函数readObject的调用地点。
2)对该应用进行网络行为抓包,寻找序列化数据,如wireshark,tcpdump等 注: java序列化的数据一般会以标记(ac ed 00 05)开头,base64编码后的特征为rO0AB。
(2)再考察应用的Class Path中是否包含Apache Commons Collections库
(3)生成反序列化的payload
(4)提交我们的payload数据
1.4、防范
每一名Java程序员都应当掌握防范反序列化漏洞的编程技巧、以及如何降低危险库对应用造成的危害。
- 对于危险基础类的调用
下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller,之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。
- 通过Hook resolveClass来校验反序列化的类
在使用readObject()反序列化时首先会调用resolveClass方法读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。
- 使用ValidatingObjectInputStream来校验反序列化的类
使用Apache Commons IO Serialization包中的ValidatingObjectInputStream类的accept方法来实现反序列化类白/黑名单控制,具体可参考ValidatingObjectInputStream介绍;
- 使用contrast-rO0防御反序列化攻击
contrast-rO0是一个轻量级的agent程序,通过通过重写ObjectInputStream来防御反序列化漏洞攻击。使用其中的SafeObjectInputStream类来实现反序列化类白/黑名单控制,示例代码如下:
SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true);
in.addToWhitelist(SerialObject.class);
in.readObject();
- 使用ObjectInputFilter来校验反序列化的类
Java 9包含了支持序列化数据过滤的新特性,开发人员也可以继承java.io.ObjectInputFilter类重写checkInput方法实现自定义的过滤器,,并使用ObjectInputStream对象的setObjectInputFilter设置过滤器来实现反序列化类白/黑名单控制
- 禁止JVM执行外部命令Runtime.exec
通过扩展SecurityManager
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包含的类。
下一篇博客将记录 java 反序列化漏洞环境的搭建思路、部分重要代码以及运行测试等内容。
参考文章
https://www.cnblogs.com/Fluorescence-tjy/p/11222052.html
https://www.cnblogs.com/niceyoo/p/10596657.html
https://www.jianshu.com/p/1c2e8aa874d0
https://www.sohu.com/a/233533386_354899
https://safe.it168.com/a2015/1113/1777/000001777302.shtml
https://www.cnblogs.com/ssooking/p/5875215.html