初探Java反序列化漏洞
最后更新时间:
文章总字数:
预计阅读时间:
Java反序列化
原生序列化与反序列化
Java补充知识
打印类的对象:会自动调用类的toString()
方法,默认是输出类名@哈希,如果希望输出我们想要看到的表现形式,需要重载toString()
方法
java的命令执行方法:千万注意java并不能够和python或者php一样直接运行终端,通常使用这样的代码来命令执行:
1 | public static void printResults(Process process) throws IOException { |
定义与具体操作
java的基本组成单位是类,为了让这些类在不同的项目之间方便地传输,必须将类序列化成字符串或者字节序列,例如在网页传输数据中常用的json和xml,然后用的时候再把类反序列化,本文讨论的是原生序列化与反序列化,即使用Java提供的方法来序列化与反序列化。
原生序列化和反序列化通过ObjectInputStream.readObject()
和ObjectOutputStream.writeObject()
方法实现。
此外值得注意的是,任何类如果想要序列化就必须实现java.io.Serializable接口,实际上该接口是一个空接口,唯一作用是给类做一个标记,让jre确定这个类可以序列化,只需要在类名后加implements java.io.Serializable
即可,并不需要其他操作:
1 | public class Person implements java.io.Serializable { |
也就是说,如果一个类没有该标记,就不用考虑对该类序列化和反序列化。
序列化:
1 | public static void serialize(Object obj) throws Exception{ |
反序列化:
1 | public static Object unserialize(String filename) throws Exception{ |
然后就可以像php一样直接调用这两个方法来序列化和反序列化。
漏洞成因与利用
java中支持在类中定义如下函数:
1 | private void writeObject(java.io.ObjectOutputStream out) |
这两个函数不是java.io.Serializable的接口函数,而是约定的函数,如果一个类实现了这两个函数,那么在序列化和反序列化的时候ObjectInputStream.readObject()和ObjectOutputStream.writeObject()会被主动调用。这也是反序列化漏洞产生的根本原因。
最简单的一种漏洞利用方式就是传入一个重写了readObject()
方法(注意重写的这个访问权限一定也要是private)的类的对象的字节序列,当程序反序列化该字节序列时就会自动执行readObject()
里的恶意方法,然而这种利用方式并不常见。
漏洞利用的可能形式有以下四点
- 入口类的readObject直接调用了危险方法
- 入口类参数中包含可控类,该类有危险方法,readObject时调用
- 入口类包含可控类,而该类又调用了其他有危险方法的类,readObject时调用,类似php的pop链,这里是gadgets,POP gadget可以存在于程序中的任何位置,唯一的要求是可以使用反序列化对象的属性来操纵代码,并且攻击者可以控制反序列化的数据
- 构造函数/静态代码块等类加载时隐式执行,或者动态代理中自动执行invoke
漏洞产生的特点
- 有入口类 source(重写readObject,参数类型宽泛)
- 调用链 gadget chain
- 执行类 sink (有危险的方法,rce,ssrf写文件等)
Java反射
反射是Java漏洞利用很重要的一环,万能的反射机制!(使用前记得将jdk版本调成1.8,太高版本的jdk反射受限)
定义和具体操作
定义:每加载一种class
,JVM就为其创建一个Class
类型的实例,并关联起来,通过Class来获得class的信息的行为叫反射。这个Class
实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序是无法创建Class
实例的。
所以,JVM持有的每个Class
实例都指向一个数据类型,一个Class
实例包含了该class
的所有完整信息,我们可以通过该实例获得或者修改class对象的属性或者方法。
具体操作:
首先要获得class的Class实例,有以下三种方法:
直接通过class的静态变量class获取
Class cls = String.class;
1
2
3
4
5
6
- 通过实例getClass()方法获取
- ```
String s = "Hello";
Class cls = s.getClass();使用class的完整类名+静态方法Class.forName()获取
```
Class cls = Class.forName(“java.lang.String”);1
2
3
4
5
6
7
8
如果获取到了一个`Class`实例,我们就可以通过该`Class`实例来创建对应类型的实例
```java
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
上述代码相当于new String()
。通过Class.newInstance()
可以创建类实例,它的局限是:只能调用public
的无参数构造方法。带参数的构造方法,或者非public
的构造方法都无法通过Class.newInstance()
被调用。解决方法是用Constructor
实例
获得了Class之后就可以获取object实例的一切信息
获取字段信息和修改信息
- Field getField(name):根据字段名获取某个public的field(只包括public包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(包括private不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
注意如果要获取私有的一定要用getDeclaredField,getField只能获取public!
一个Field
对象包含了一个字段的所有信息:
getName()
:返回字段名称,例如,"name"
;getType()
:返回字段类型,也是一个Class
实例,例如,String.class
;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义。先获取
Class
实例,再获取Field
实例,然后,用Field.get(Object)
获取指定实例的指定字段的值,如果要让问定义为private的字段,需要在访问前先加一句1
f.setAccessibla(true);
使用Field.set可以设置字段的值,第一个参数是Object实例,第二个参数是待修改的值
获取方法和调用方法
通过Class
实例获取所有Method
信息。Class
类提供了以下几个方法来获取Method
:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
一个Method
对象包含一个方法的所有信息:
getName()
:返回方法名称,例如:"getScore"
;getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
;getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义。
通过Method对象可以调用方法:
1 | public class Main { |
使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。
构建对象
为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且调用结果总是返回实例:
通过Class实例获取Constructor的方法如下:
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
通过Constructor
实例可以创建一个实例对象:newInstance(Object... parameters)
; 通过设置setAccessible(true)
来访问非public
构造方法
除了以上三种外,还可以通过getSuperclass()
获取父类getInterfaces()
获取接口
实操:DNS-URL利用链分析
URLDNS也就是 当HashMap的key传入为URL类型对象的话,这个URL对象就会执行URL类的hashCode()方法 ,从而触发DNS请求,通常用来探测是否存在反序列化漏洞。
阅读URL和HashMap源码得出链子流程:
1 | URL类-->hashCode()方法-->1.hashcode=!-1-->不执行DNS解析 |
由于一开始要先将URL类实例放入HashMap中,但是我们又不希望在放入的时候就DNS解析,于是要通过反射机制将hashcode改为不是-1的值。
为使反序列化自动调用readObject()时正常解析DNS,在将URL放入HashMap之后又要将hashcode改回-1,这样,如果发生反序列化就会自动DNS解析,即可判断存在反序列化漏洞。
1 | import java.io.FileInputStream; |
DNSLOG Platform (dig.pm)这个网站可以接收DNSLOG,判断链子是否调对。
JDK动态代理
定义和具体操作
Java标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface
的实例。
先定义了接口Hello
,但并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()
创建了一个Hello
接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。
1 | public class Main { |
在运行期动态创建一个interface
实例的方法如下:
- 定义一个
InvocationHandler
实例,它负责实现接口的方法调用(自动调用invoke方法); - 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
- 使用的
ClassLoader
,通常就是接口类的ClassLoader
; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler
实例。
- 使用的
- 将返回的
Object
强制转型为接口。
漏洞成因与利用
invoke涉及到了隐式函数调用(反序列化漏洞最喜欢看到的事),ClassLoader允许我们加载自己的类,攻击手法更加多样,具体链子和漏洞待进一步学习。
类的动态加载
定义和具体操作
类加载与反序列化:类加载的时候会自动执行代码,初始化会执行静态代码块,实例化会执行构造代码块和无参构造函数。
静态代码块:执行优先级高于非静态的初始化块,它会在类初始化的时候执行一次,执行完成便销毁,它仅能初始化类变量,即static修饰的数据成员。
静态代码块写法,
static{
}
对应的扩展下非静态代码块
非静态代码块(就是构造代码块):
执行的时候如果有静态初始化块,先执行静态初始化块再执行非静态初始化块,在每个对象生成时都会被执行一次,它可以初始化类的实例变量。非静态初始化块会在构造函数执行时,在构造函数主体代码执行之前被运行。
写法:
{
}
动态类加载方法:
- Class.forname
- ClassLoader.loadClass 不进行初始化
- ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader.loadClass->findClass(必须重写方法,如果使用系统默认的,加载过一次就不会再进行加载(双亲委派模型))->defineClass(从字节码中加载类,使得我们可以加载任意的类)
漏洞成因与利用
加载我们自己构造的恶意类,再通过任意类加载将其加载到服务器上:
- URLClassLoader file/http/jar
- ClassLoader.defineClass 字节码加载任意类,但是defineClass是private的
- Unsafe.defineClass 字节码,使用有诸多限制,public类不能直接生成,spring里面可以直接生成。
参考链接: