理解Java反序列化-ysoserial URLDNS

目前网上绝大部分 Java 反序列化漏洞文章都以 CommonsCollections 这条利用链作为教程,这条利用链其实特别复杂,用 CommonsCollections 作为 Java 反序列化入门教程未免太过硬核。

想要利用「不安全的反序列化」漏洞,就需要看被反序列化的类的 readObject() 是怎么写的。通常 Java 反序列化类中的 readObject() 方法很少会被重写,这也就是为什么「不安全的反序列化」利用链一般都只出现在通用 jar 包中。大部分反序列化的利用链较为复杂,甚至还需要多种 Java 特性来配合,比如反射、动态代理、JNDI注入等等,光了解这些特性就要花上不少时间。

这里通过 URLDNS 作为利用链来学习 Java 反序列化就会简单很多。

一、利用

使用 WebGoat 不安全的反序列化题目作为复现环境。

URLDNS 是 ysoserial 工具的一个模块,作用是发起一个 DNS 请求,该模块方便在于利用广,仅靠 JDK 默认的 java.util.Hashmapjava.net.URL 即可完成反序列化的过程。并且利用链较短,有助于理解 Java 反序列化,利用链太长容易迷茫在利用链中的技术细节中。

ysoserial 中 URLDNS 模块的使用方法如下:

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS "https://dns.example.com"

Burp Deserialization Scanner Exploiting 利用如下:

URLDNS-exploit

Burp Collaborator 得到如下返回,就这样非常简单的完成了一次反序列化的利用,利用反序列化发起了一次DNS请求。

burp-collaborator-result

二、调试前准备

ysoserial 项目地址: https://github.com/frohoff/ysoserial/ ,git clone 或直接下载下来导入到编译器中,我这里使用的编译器是 IntelliJ IDEA。

ysoserial 项目结构如下,在 payloads 目录中可以看到 ysoserial 中所有的可利用类。

ysoserial-payload

ysoserial 项目中存在 pom.xml 文件,表明这是一个由 maven 构建的项目。IntelliJ IDEA 会自动根据其中的配置下载依赖。

POM(Project Object Model,即项目对象模型)是 Maven 工程的基本工作单元,是一个 XML 文件,包含了 Maven 用于构建项目的项目信息及各种配置信息,比如项目依赖,项目开发人员,版本,插件,组织信息、项目授权、项目的 url 等等。我们通过 POM 文件来完成对 Maven 项目的管理,构建等操作。

如果 maven 依赖有问题,可以手工点击菜单里的 Files - Project Structure,然后配置 Libraries。

ysoserial-project-structure

先输入需导入的包名,等待搜索完成,再选择相应的版本下载即可。

download-library-from-maven-repository

pom.xml 文件中 maven-assembly-plugin 就是一个用来打包项目的插件,可以把依赖、类文件什么的都打包在一起。其中存在 <mainClass>,表明 ysoserial.GeneratePayload 就是主类。

maven-mainClass

根据 pom.xml 中的 <mainClass> 找到找到 GeneratePayload 类,main 函数前有个绿色的小箭头,点击这个小箭头可选择从此 Debug( 因为一个项目中可能存在多个 main 函数,所以通过这种方法快速找到运行时的主类)。

debug-GeneratePayload

此时就可以在我们需要的地方下断点,在 IntelliJ IDEA 中点击左侧栏即可设下断点,下图红色圆圈就是断点,调试过程中,程序运行到断点处就会停下。

URLDNS-hashmap

如果此时直接开始调试只能在 console 中看到输出 usage 页面,ysoserial 需要添加参数来运行指定的 payloads 模块。

所以需要修改默认参数。修改路径/方法如下。打开 IDEA 右上角的 Edit Configurations...

Edit-Configurations

然后在 Program arguments 中输入参数 URLDNS "<http://xxx.com>"

set-program-arguments

以上方法主要是调试整个程序,调试其他项目的思想大致如此。

ysoserial 可以不用单独调试,这里主要是为了记录方法/思想,以后遇到其他项目时就知道该怎么去调试了。

三、反序列化过程调试

3.1 介绍

ysoserial 有那么多 payloads,调试整个程序是非常冗余的。ysoserial 提供了单独调试 payloads 的方法,每个 payload 的 main 方法中都有调用 PayloadRunner ,可以 Debug payload 的 main 函数来进行单独调试。不设置运行参数时会默认使用 calc.exe 作为参数来执行。

并且 PayloadRunner 会完成序列化到反序列化全部过程,就是从 payload 的生成到 readObject() 的读取。这样无需额外找反序列化环境来测试,方便快捷~

在 URLDNS 中点击 main 函数左侧的箭头进行 Debug

debug-URLDNS-main

单独调试 payload 同样需要添加参数,但无需指定模块,仅设置 DNSlog地址"http://xxx.com" 即可

set-program-arguments-URLDNS

URLDNS 中的 getObject() 方法中新建了一个 HashMap 类,并在 HashMap 中放置了一个 java.net.URL 类,url 是我们传入的参数。相比于其他 ysoserial 中的 payload,URLDNS 非常简单明了。

从中我们可以得知 URLDNS 序列化的是 HashMap 类,在之后反序列化的过程中,就能明确需要追踪的是 HashMap -> readObject()

3.2 反序列化过程

URLDNS 中 main 函数中用到 PayloadRunner.run(URLDNS.class, args)

PayloadRunner 在这里不是重点,就不单独介绍。PayloadRunner 代码分析可参考:http://1codelife.com/2017/09/17/Java-deserial-learning1/

final Object objAfter = Deserializer.deserialize(serialized) 表示调用反序列化过程,如下图断点处

debug-deserialize

Deserializer 类的 deserialize 方法处调用 objIn.readObject(),正式进入反序列化操作

debug-objectinputstream-readobject

在反序列化过程前有一些概念需要理解:

  1. 反序列化漏洞应该叫做「不安全的反序列化」漏洞,正常的反序列化是不存在危害的;
  2. 反序列化类必需实现 Serializable 接口,并重写 readObject() 方法;
  3. 未重写 readObject() 不存在「不安全的反序列化」漏洞;
  4. 即使重写 readObject() 也不一定就存在「不安全的反序列化」漏洞,还需要看重写的内容,是否可构造「有效利用链」;
  5. 序列化对象被反序列化时会调用序列化对象的readObject()方法。

第 5 点可能不太好理解,就以 URLDNS 来举例,上图断点处是 objIn.readObject()objInObjectInputStream对象,可以理解为执行的是 ObjectInputStream.readObject() 。此处 readObject() 的作用是读取传入的序列化对象。

此时我们传入一个 HashMap 的序列化对象,ObjectInputStream.readObjec() 会读取该序列化对象,进行反序列化操作,反序列化过程中会执行序列化对象的 readObject() 方法,也就是执行 HashMap.readObject() 方法。

两个 readObject() 都会执行,但两者的作用不同。利用链开始的 readObject() 是后者。

想要利用「不安全的反序列化」漏洞,就需要看序列化对象的 readObject() 是怎么写的。通常 Java 序列化类中的 readObject() 方法很少会被重写,这也就是为什么「不安全的反序列化」一般都只出现在通用 jar 包中。且每一个反序列化的利用链都非常复杂,甚至还需要多种 Java 特性来配合,比如反射、动态代理、JNDI 注入等等,光了解这些特性就要花上不少时间。

ObjectInputStream.readObject()HashMap.readObject() 过程中会经过多个 java.iosun.reflect 类中方法的调用 ,见下图调用栈,这是 Java 反序列化中最需要理解的一步

readobject-stack

从此开始反序列化利用链的分析,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);
}
}
}

咋一看 readObject() 方法并不会有特别明显的异常,其实重点在最后一行 putVal(hash(key), key, value, false, false)hash() 方法,计算 key 的 hash 值(在这下断点会方便后续快速调试)

debug-putVal

使用 IntelliJ IDEA 调试在这里会碰到问题,可能调试不会步进 hash 方法,而是直接步进 putVal 方法中,具体原因可看最下方附录。

步进 hash 方法,看到其中调用了 key 的 hashCode 方法

debug-hashCode

继续步进 hashCode,因为 key 是一个 java.net.URL 对象,就会跳到URL -> hashCode 方法

可以看到这里会判断hashCode != -1,这也是为什么构造 payload 时会将 hashCode 赋值为 -1

hashCode-not-equal-minus-one

此处 handlerURLStreamHandler 的一个对象,由 transient URLStreamHandler handler 创建。继续步进 hashCode 方法。

在 359 行看到 getHostAddress,步进该方法

debug-getHostAddress

这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其 IP 地址,在⽹络上就是⼀次 DNS 查询。

其实到这⾥反序列化的调试就可以结束了,后续是具体发起 DNS 查询的步骤。

我们也可再继续步进

debug-InetAddress-getByName

中间略过几步…

直接到关键步骤 nameService.lookupAllHostAddr(host),正是从此处发起 DNS 查询。

debug-InetAddress.getAllByName

下图是从 HashMap -> readObject() 开始到 DNS 请求执行的调用栈,这是 ysoserial payloads 中最简单的调用栈。

debug-URLDNS-stack

整个 URLDNS 的利用链可以简单概括成:

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

四、构造利用链

知道了反序列化的过程,就能据此构造利用链,利用链也叫做 “Gadget chains”。

要构造这个 Gadget ,只需要初始化⼀个 java.net.URL 对象,作为 key 放在 java.util.HashMap 中;然后,设置这个 URL 对象的 hashCode 为初始值 -1 ,这样反序列化时将会重新计算其 hashCode,才能触发到后⾯的 DNS 请求,否则不会调⽤ URL -> hashCode()

这里 hashCode 必须设置为 -1

hashCode-not-equal-minus-one

另外,URLDNS 为了防⽌在⽣成 Payload 的时候也执⾏了 URL 请求和 DNS 查询,所以重写了⼀ 个 SilentURLStreamHandler 类,但这不是必须的。

URLDNS 中的注释也存在相应的说明,可在 ysoserial 项目代码 https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/URLDNS.java 中查看。

五、对反序列化的理解

Java 反序列化漏洞核心就是执行被反序列化类的 readObject() 方法,readObject() 是整个利用链的起点。纵观整个反序列化的过程,最难的是利用链的构造。

一个系统存在反序列化漏洞需要满足以下条件:

  1. 需要可接收用户数据的ObjectInputStream -> readObject()
  2. 存在有效利用链的 lib/jar 包(一般不太可能根据程序自身的反序列化类构造一个利用链,除非白盒测试)

正常的反序列化类是没有危害的。只有当「被反序列化的类」重写了 readObject() 方法,且重写方法中存在「利用链」才属于不安全的。因为在「被反序列化的类」被反序列化时会自动执行 readObject() 方法,所以首先需要的是重写 readObject(),其次是重写的方法中存在「有效的利用链」。这里「利用链」有的是通过反射,有的是通过动态代理,有的是通过 JNDI 注入等等方法才导致最后的 RCE 。

「被反序列化的类」一般都是 lib/jar 包中的类。正常情况下,普通系统的开发在正常情况下很少会重写 readObject() 且存在「有效的利用链」(普通的开发都是功能型开发,很少用到反序列化,由于 Json 的普及使得极少情况下才需要手写序列化的类(结果Jackson和fastjson产生了大量的反序列化漏洞))。目前曝出来的大部分反序列化利用的都是 lib/jar 包中的类,这些类为了通用性就会写出一些特殊的 readObject() 方法,这也导致一些花式攻击手法的产生。

六、代码审计

搞清楚 Java 反序列化,以后代码审计就着重注意 ObjectInputStream -> readObject() 方法是否用户可控。

若用户可控,就再去找 lib 目录中是否存在 ysoserial payload 中已有的 jar 包,有的话就能直接认为该处存在反序列化漏洞。或者查看 Java 版本,若低于 Jdk7u21也能造成 RCE。

即使以上都没有,该处也算是一个风险点,说不定以后会有新的 Gadget 被发现。

七、附录

报错相关

URLDNS 参数需加上相应协议,如 http://https:// ,若不加具体协议,会产生如下错误:

ysoserial-URLDNS-error

亲测 ysoserial 不能在 Java 11 中使用,我的 Java 环境为 java 1.8.0_221。Java 1.8 还是稳~

IntelliJ IDEA 会默认步过 JDK 中的方法

方法1 使用 Force Step Into

可以使用 Force Step Into 强制步进,简单粗暴。

force-step-into

方法2 取消 Do not step into the classes

IntelliJ IDEA 默认在调试选项中关闭了对 JDK 源码的调试支持,需要在设置中取消勾选

Mac 中设置路径为 IntelliJ IDEA → Preferences... ,快捷键 ⌘ Cmd + ,

Windows 中设置路径为 File → Setting

打开设置界面后在 Build, Execution, Deployment → Debugger → Stepping → Do not step into the classes → 取消勾选 java.*javax.*

Do-step-into-the-classes

设置之后调试时不会自动跳过 java.*javax.* 类。