文章目录
- 1、 序列化与反序列化
- 2、序列化与反序列化案例
- 2.1、使用idea生成代码与serialVersionUID
- 2.2、实例化对象
- 2.3、序列化对象
- 2.4、反序列化
- 3、稍微深入serialVersionUID
- 综上小结,
- 4、transient 作⽤
- 5、反序列化漏洞
之前的文章,
php代码审计15.1之反序列化
php代码审计15.2之Session与反序列化
php代码审计15.3之phar伪协议与反序列化
1、 序列化与反序列化
序列化是让Java对象脱离Java运⾏环境的⼀种⼿段,
可以有效的实现多平台之间的通信、对象持久化存储。
序列化
把Java对象转换为字节序列的过程称为对象的序列化
反序列化
把字节序列恢复为Java对象的过程称为对象的反序列化。
2、序列化与反序列化案例
2.1、使用idea生成代码与serialVersionUID
在java中 序列化 必须实现 Serializable接⼝ 序列化保存的只是对象的状态,
并不包含⽅法和静态成员变量
新建⼀个user实现 Serializable接⼝
新建一个项目,
使用idea生成构造方法;代码--生成--构造函数--选择所有参数
其实可以直接右击,选择生成
类似的给所有的属性生成get/set方法
在生成一个toString方法,
toString方法将类当作字符串使用时,就会自动调用,
此时可以安装一个插件,直接搜索“GenerateSerialVersio”
之后就可以直接生成函数反序列化/序列号的uid
稍微补充下,
serialVersionUID是Java中的一个类属性,用于在序列化和反序列化过程中进行核验的一个版本号。
它是一个唯一的整数,用于标识类的版本信息。
如果两个类具有相同的serialVersionUID,则它们可以相互序列化和反序列化。
如果serialVersionUID不同,则会抛出NotSerializableException异常。
最终的完整代码,user.java
import java.io.Serializable;
public class user implements Serializable {
private static final long serialVersionUID = -2831602267920455150L;
private int age;
private String username;
private String password;
public user(int age, String username, String password) {
this.age = age;
this.username = username;
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "user{" +
"age=" + age +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
2.2、实例化对象
新建一个main,
实例化上边的user类,这个输出的本质就是调用的user类的toString方法,
main代码,
package com.example.demo2;
public class main {
public static void main(String[] args) {
user user = new user(20,"xbb","123456");
System.out.println(user);
}
}
2.3、序列化对象
main.java
package com.example.demo2;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class main {
public static void main(String[] args) throws Exception {
user user = new user(20,"xbb","123456");
// System.out.println(user);
serialize(user);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
user user = (user)ois.readObject();
System.out.println(user);
}
}
序列化后的文件是一个二进制文件,以txt的模式打开会乱码
以二进制的模式打开就好了,然后对比下设置serialVersionUID与不设置的区别,
红色的ser1.bin是设置的,紫色的是没有设置的(不手动设置会自动生成),
然后具体的区别也使用紫色的线划了出来,
还可以看到的是,其实序列化后保存的内容其实很有限,
2.4、反序列化
使用同一个serialVersionUID序列化的文件,然后反序列化是一切正常
但是假设反序列化读取的文件内的serialVersionUID与当前类的serialVersionUID不一致就会报错,
当我们注释掉类中手动设置的serialVersionUID在反序列化就一切正常
3、稍微深入serialVersionUID
开始说过,java的对象在序列化的时候,保存的内容有限。
很多内容不会保存,经过测试得到的结论是,序列化保存的内容如下:
● 类名
● 包名
● 父类名称(未测试,猜测存在)
● 变量名称、类型、值
其他的待测试,然后这个serialVersionUID的作用就是确认反序列化的类和代码是同一个类,
不是同一个类的话,就会报错。
当我们使用自定义的serialVersionUID时,
可以看到我们修改了toString方法,但是序列化没有保存,所以我们修改了方法,
在反序列化的时候也不会报错,
这还一个注意的点是,
又定义了一个新的变量“xbb”,但是序列化的内容是没有这个值的,理论是会报错的,
但是这里没有报错这里个人的猜测是因为没有校验的原因是serialVersionUID是我们自己指定的,
而具体变量的存在和值是根据serialVersionUID判断,所以没有报错,
我们使用系统自己生成的serialVersionUID来还原就会报错,
这也验证了我们上边的猜想,
又一个新的问题是,这个serialVersionUID的值,系统是根据什么生成的呢?
这里笔者去问了下AI,得到的回答是(下边括号里的内容是笔者测试得到的),
● 类名
● 包名
● 父类名称
● 字段名称和类型(没有值)
● 方法名称和参数类型
继续测试,既然我们可以通过指定serialVersionUID来欺骗java,
生成了一个新的变量和值没有报错,
一个场景是,
指定serialVersionUID来测试,代码在序列化的时候,
xbb变量是一个int类型的数字,内容为“111”,
我本地构造xbb的类型为字符串,内容也是“111”,会不会报错呢?
先模拟生成一个字符串的xbb,序列化保存文件的名称为“ser2.bin”
这里的背景是serialVersionUID是指定的,
是报错的,
这说明,我们通过指定serialVersionUID可以骗过系统检测,肯定是后续在还原的过程出错了,
通过二进制打开ser2.bin,猜测是这个“t..”是字符串的含义,
然后java程序在还原的时候发现序列化的内容与代码中变量类型不同导致的报错,
但是发现数字类型没有在bin文件中找到,是不是20太小了,来一个大的数尝试且不以“0”结尾,
但是结果还是没有在bin文件中找到,
还有就是xbb变量因为是int值,在序列化后的文件内也么有找到,
综上小结,
反序列化文件内的serialVersionUID必须与代码内的值一致
序列化仅仅会保存类的一些基本属性(类名,包名,变量类型和值;但是不会保存方法)
在指定serialVersionUID属性的情况下,可以在序列化的时候,少序列化一些变量,
无论是否指定serialVersionUID属性,都可以修改变量的值,但是都不能修改变量的类型
serialVersionUID系统自动生成的话,根据以下属性
● 类名
● 包名
● 父类名称
● 字段名称和类型(没有值)
● 方法名称和参数类型
4、transient 作⽤
Java中的transient关键字,transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象 的
序列化处理过程中会被忽略。因此,transient变量不会贯穿对象的序列化和反序列化,⽣命周期仅存于
调⽤者的内存中⽽不会写到磁盘⾥进⾏持久化。
比如这里,我们使用transient来修饰xbb变量,
反序列化后的代码就没有输出“333”
user.java
package com.example.demo2;
import java.io.Serializable;
public class user implements Serializable {
private static final long serialVersionUID = -2831602267920455150L;
private int age;
private String username;
private String password;
public user(int age, String username, String password) {
this.age = age;
this.username = username;
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
transient public int xbb = 333;
@Override
public String toString() {
return "user{" +
"age=" + age +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}' + xbb +"0000000666";
}
}
main.java
package com.example.demo2;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class main {
public static void main(String[] args) throws Exception {
user user = new user(2000566666,"xbb11","123456");
System.out.println(user);
unserialize();
//serialize(user);
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser4.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser4.bin"));
user user = (user)ois.readObject();
System.out.println(user);
}
}
5、反序列化漏洞
模拟重写 readObject 函数造成问题,
当你将一个对象序列化为字节流时,它的状态(即数据)被写入到一个输出流中。
而在反序列化过程中,readObject()方法的作用正是从一个源输入流中读取字节序列,
再把它们反序列化为一个对象,并将其返回,readObject()是可以重写的,可以定制反序列化的一些行为。
当作者重写了readObject函数,且在函数内存在一些危险的操作时,就可能会造成问题,
因为重写后的readObject函数就和php的魔术函数一样,会在反序列化的时候自动执行,
比如下边的代码,
开发本来的想法可能是弹出一个记事本,
但是恶意者可以先在本地序列化生成的时候,将cmd变量的值改为弹出计算器,
然后将本地序列化生成的文件让目标服务器执行,
就会弹出计算器,而非记事本
过程,
1、本地生成恶意序列化文件XX.bin
2、让目标服务器反序列化XX.bin
user.java
package com.example.demo2;
import java.io.Serializable;
public class user implements Serializable {
// private static final long serialVersionUID = -2831602267920455150L;
private int age;
private String username;
private String password;
public user(int age, String username, String password) {
this.age = age;
this.username = username;
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
transient public int xbb = 333;
@Override
public String toString() {
return "user{" +
"age=" + age +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}' + xbb +"0000000666";
}
public String cmd = "notepad";
//public String cmd = "calc";
//重写readObject()⽅法
private void readObject(java.io.ObjectInputStream in) throws Exception {
//执⾏默认的readObject()⽅法
in.defaultReadObject();
//执⾏打开计算器程序命令
Runtime.getRuntime().exec(cmd);
}
}
main.java
package com.example.demo2;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class main {
public static void main(String[] args) throws Exception {
user user = new user(2000566666,"xbb11","123456");
System.out.println(user);
//serialize(user);
unserialize();
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser4.bin"));
oos.writeObject(obj);
}
public static void unserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser4.bin"));
user user = (user)ois.readObject();
System.out.println(user);
}
}