Java反序列化漏洞系列-1
Java 反序列化漏洞系列-1
1 序列化与反序列化基础
序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。
1.1 相关方法
ObjectOutputStream
类的 writeObject()
方法可以实现序列化。按 Java 的标准约定是给文件一个 .ser
扩展名。
ObjectInputStream
类的 readObject()
方法用于反序列化。
1.2 序列化前提
实现 java.io.Serializable
接口才可被反序列化,而且所有属性必须是可序列化的(用 transient
关键字修饰的属性除外,不参与序列化过程)
1.3 漏洞成因
序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。
反序列化 payload 生成工具:https://github.com/frohoff/ysoserial/
2 漏洞基本原理
2.1 序列化
序列化后的数据开头包含两字节的魔术数字:ACED
。接下来是两字节的版本号 0005
的数据。此外还包含了类名、成员变量的类型和个数等。
序列化的数据流以魔术数字和版本号开头,这个值是在调用 ObjectOutputStream
序列化时,由 writeStreamHeader
方法写入:
|
|
2.2 反序列化
Java程序中类 ObjectInputStream
的 readObject
方法用来将数据流反序列化为对象。
readObject()
方法在反序列化漏洞中它起到了关键作用。如果 readObject()
方法被重写,反序列化该类时调用便是重写后的 readObject()
方法。如果该方法书写不当的话就有可能引发恶意代码的执行。
如:
|
|
但是,实际中反序列化漏洞的构造比较复杂,而且需要借助 Java 的一些特性,如 Java 的反射。
3 Java 反射
3.1 Java 反射定义
对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 java 语言的反射机制。
反射是⼤多数语⾔⾥都存在的特性,对象可以通过反射获取它的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以直接调用。总之,通过反射,可以将 Java 这种静态语⾔附加上动态特性。
Java 语言虽然不像 PHP 那样存在许多灵活的动态特性,但是通过反射,可以达到一定的效果,如下面这段代码,在传入参数值不确定的情况下,该函数的具体作用是未知的。
|
|
在 Java 中定义的一个类本身也是一个对象,即 java.lang.Class
类的实例,这个实例称为类对象
- 类对象表示正在运行的 Java 应用程序中的类和接口
- 类对象没有公共构造方法,由 Java 虚拟机自动构造
- 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法
要得到类的方法和属性,首先就要得到该类对象
3.2 获取类对象
假设现在有一个 Person 类:
|
|
要获取该类对象一般有三种方法:
class.forName("com.geekby.Person")
Person.class
new Person().getClass()
最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象。
3.3 利用类对象创建对象
与直接 new
创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象。
|
|
方法 | 说明 |
---|---|
getConstructor(Class… parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
getConstructors() | 获得该类的所有公有构造方法 |
getDeclaredConstructor(Class… parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() | 获得该类所有构造方法 |
3.4 利用反射调用方法
|
|
方法 | 说明 |
---|---|
getMethod(String name, Class… parameterTypes) | 获得该类某个公有的方法 |
getMethods() | 获得该类所有公有的方法 |
getDeclaredMethod(String name, Class… parameterTypes) | 获得该类某个方法 |
getDeclaredMethods() | 获得该类所有方法 |
3.5 通过反射访问属性
|
|
方法 | 说明 |
---|---|
getField(String name) | 获得某个公有的属性对象 |
getFields() | 获得所有公有的属性对象 |
getDeclaredField(String name) | 获得某个属性对 |
getDeclaredFields() | 获得所有属性对象 |
3.6 利用反射执行代码
|
|
以上代码中,利用了 Java 的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提。
invoke 的作用是执行方法,它的第一个参数是:
-
如果该方法为普通方法,那么第一个参数是类对象
-
如果该方法为静态方法,那么第一个参数是类或 null
此外,另一种常用的执行命令的方式 ProcessBuilder
,通过反射来获取其构造函数,然后调用 start()
来执行命令:
|
|
查看文档可知:ProcessBuilder
有两个构造函数:
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
上面通过反射的调用方式使用了第一种形式的构造函数。
但是,上述的 Payload 用到了 Java 里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。因此,仍需利用反射来执行 start
方法。
|
|
上述的第二种构造函数如何调用呢?
对于可变长参数,Java 在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的:
|
|
因此,对于反射来说,如果目标函数里包含可变长参数,传入数组即可。
|
|
在调用 newInstance
的时候,因为该函数本身接收的是一个可变长参数:
传给 ProcessBuilder
的也是一个可变长参数,二者叠加为一个二维数组,所以整个 Payload 如下:
|
|
3.7 反序列化漏洞与反射
在安全研究中,使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。比如,上下文中如果只有 Integer 类型的数字,如何获取到可以执行命令的 Runtime 类:
比如可以这样(伪代码):1.getClass().forName("java.lang.Runtime")
4 DNSURL gadget 分析
4.1 调用链
|
|
payload:
|
|
4.2 分析
首先查看 HashMap 的 ReadObject 方法
339
行:在调用 putVal 方法之前会调用 hash 方法,查看其源代码:
899 - 903
行:如果 key == null
,hashcode 赋值为 0。key 存在的话,则调用 key 的 hashcode 方法。
在本 gadget 中,key 为 URL 对象。接着,跟进 URL 的 hashCode 方法。
URL 类的 hashCode 很简单。如果 hashcode 不为 -1,则返回 hashcode。在序列化构造 payload 的时候,需要设置 hashcode 为 -1 的原因,就是防止进入到 hashcode
方法中,进而发送 DNS 请求,影响判断。
当 hashcode==-1
,调用 handler 的 hashCode 方法。该类的定义在 URL 的构造函数中,主要是根据 scheme 去决定用什么类做 handler。在这里是 URLStreamHandler 类,跟进 URLStreamHandler 的 hashcode 方法。
在第 359 行,调用 getHostAddress
获取域名对应的 IP。
DNSURL 链便是利用该处,来触发 DNSLog 发送请求。