0x00 前言

Java反序列化基础篇的第二篇文章。

参考教程:
白日梦组长的视频 Java反序列化漏洞专题

Java反序列化基础篇-02-Java反射与URLDNS链分析 | Drunkbaby’s Blog

0x01 回顾

攻击思路

在上篇文章,我们大致了解了Java反序列化的攻击思路

  1. 首先要具有Serializable接口,可以被序列化喝反序列化

  2. 入口类(最好重写readObject方法)应该满足三个条件:

    • readObject方法接收的参数类型宽泛,比如Object类型
    • readObject方法可以调用常见的函数,比如Object类下的hashCode方法
    • 该入口类最好jdk自带
  3. 找到了入口类,还要找到调用链( gadget chain

  4. 最后要找到执行类(sink)用来攻击

谈起反射

在上文,我们遇到了一个问题,给出题干:

image-20241106112920588

不难看到在反序列化之前执行了hashmap.put方法,跟进该方法:

image-20241106113020668

对比hashmapreadObject方法的一段代码:

image-20241106113229017

二者一模一样的代码,对URLDNS有很大的影响:

在序列化hashmap对象之前,hash(key)方法会执行一遍,也就是执行一遍URL对象的hashCode方法:发送DNS请求并设置URL对象的hashCode属性不为-1——到反序列化执行readObject方法时没法发送DNS请求。

所以我们希望在序列化前设置URL对象的hashCode属性不为-1,要序列化时再设置该属性为-1,这就需要用到反射了。

0x03 反射

Java 安全可以从反序列化说起,而反序列化可以从反射说起。

反射概述

两种语言类型

Java本身是一种静态语言,在编译时就要进行类型检查,这通常要求我们显示声明变量的类型。静态语言的优势在于其运行性能较好,减少运行时的错误。劣势在于开发周期更长。

与之相反,PHP则属于动态语言,具有很多灵活的语言特性。PHP在运行时进行类型检查,变量的类型在运行时确定,通常不要求显式声明变量类型。动态语言的优势在于语法更加灵活,适合快速开发和原型涉及。劣势在于可能要面临更多的运行时错误

大家可以对比一下,PHP有很多的语法漏洞可以攻击,而Java的语法漏洞则非常少。

为何需要反射

每一种机制的产生都有其目的性或者说作用,Java的反射机制的作用即是:让Java具有动态性

这是因为编程语言不能完全分裂成静态语言和动态语言,现实的复杂性决定了一个好的编程语言最好要具备上述两种语言类型的优点。

正射 · 反射

欲知逆,先知正。正射与反射的关键区别在于二者对类和对象的访问和操作方式

  • 正射在编译时确定方法调用或属性访问的具体类型,比如直接通过类创建一个对象后使用该类的方法,代码逻辑如下:

image-20241106144501375

  • 反射则是指在运行时动态获取类的信息(如方法、字段等)并进行操作,先看看如下代码:

image-20241106144753122

反射操作需要用到Class对象,但是Class对象只有在运行时才能被Java虚拟机(JVM)创建,也就是说整个反射机制都依赖于程序运行阶段而非之前的编译阶段。

那么这里的Class类型和对象是什么呢?理解反射的第一步就是理解Class是什么。

Class

读者初写Java程序时不难发现Java程序在编译时会生成**.class**文件,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
30
31
32
33
34
35
36
37
38
39
40
41
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.pax.UnserializeBlog;

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

public class Person implements Serializable {
public transient String name;
private transient int age;
public static int id;

public static void staticAction() {
}

public Person() {
}

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

public String toString() {
return "com.pax.UnserializeOne.Person{name=" + this.name + '\'' + ", age=" + this.age + '}';
}

public void action(String act) {
System.out.println(act);
}

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}

Person.class就是Class类型的对象,当 JVM 加载一个类时,它会创建一个 Class 对象来表示这个类,这个 Class 对象包含了关于类的信息,包括类名、方法、字段、构造函数等。

所以反射就是操作Class对象

机制组成

反射机制相关操作一般位于java.lang.reflect包中,而java反射机制组成需要重点注意以下的类或包:

类/包 解释
java.lang.Class 类对象
java.lang.reflect.Constructor 类的构造器对象
java.lang.reflect.Field 类的属性对象
java.lang.reflect.Method 类的方法对象

0x04 使用

相关方法

反射机制允许我们了解并使用某个类,相关操作有:

方法 作用
forName 通过类名称获取该类
newInstance 实例化类对象
getMethod 获取该类的方法
invoke 执行该类的方法

具体流程

(1)首先要获得Class对象

对于常规的类,也就是构造器是公有的类,可以直接实例化类对象:

image-20241106190903762

但是我们尝试直接实例化Class对象,会报错:

image-20241106191126849

跟进Class的构造器:

image-20241106191235177

如上所说,如果构造器是私有的,则不能直接实例化类对象。

下面介绍几种获得Class对象的方法:

第一种:利用已实例化对象的getClass()方法

需要一个已经实例化的对象,通过该类的getClass方法获得Class对象。利用方式如下:

image-20241106191912511

这里多说两点:

image-20241106192119195

  • 该方法返回值类型是Class<?>,也就是返回一个Class对象
  • 每个类都继承Object类,Object类定义了getClass方法,这意味着每个类都有该方法

第二种:使用类的 .class 方法

如果已经加载了某个类,只想获取它的java.lang.Class 对象,那么就直接拿它的 class 属性即可。利用方式如下:

image-20241106192604885

第三种:Class.forName(String className):动态加载类

如果已知某个类的类名,可以通过Classforname方法来获取。利用方式如下:

image-20241106193555064

三种方法的使用

用一段代码来展示利用上述的三种方法获取Class对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LocalClassTest {
public static void main(String[] args) throws Exception {
// 第一种:利用已实例化对象的getClass()方法
Person person = new Person("one", 123);
Class c1 = person.getClass();
System.out.println(c1.getName());

// 第二种:使用类的 .class 方法
Class c2 = Person.class;
System.out.println(c2.getName());

// Class.forName(String className):动态加载类
Class c3 = Class.forName("com.pax.UnserializeBlog.Person");
System.out.println(c3.getName());
}
}

image-20241106194450163

(2)获得成员变量 Field

获取成员变量Field位于 java.lang.reflect.Field 包中,注意下面四种方法:

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

image-20241106195212753

  • 代码如下:

image-20241106195958857

  • 输出如下:

image-20241106200030493

(3)获取成员方法 Method

获取成员变量Field位于 java.lang.reflect.Method 包中,与获取Field类似,有四种方法:

方法 作用
Method getMethod(String name, 类<?>... parameterTypes) 返回该类所声明的public方法
Method[] getMethods() 获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法
Method getDeclaredMethod(String name, 类<?>... parameterTypes) 返回该类所声明的所有方法
Method[] getDeclaredMethods() 获取该类中的所有方法

不再演示,请读者自行复现。

(4)获取构造函数 Constructor

获取成员变量Field位于 java.lang.reflect.Constructor 包中,仍然是四种方法:

方法 作用
Constructor<> getConstructor(类<?>... parameterTypes) 匹配和参数配型相符的public构造函数
Constructor<?>[] getConstructors() 只返回public构造函数
Constructor<> getDeclaredConstructor(类<?>... parameterTypes) 匹配和参数配型相符的构造函数
Constructor<?>[] getDeclaredConstructors() 返回所有构造函数

不再演示,请读者自行复现。

0x05 进阶

反射创建对象

反射创建对象,也称之为:反射之后实例化对象,过程如下:

image-20241106203039662

newInstance 方法在 Java 中用于通过反射创建一个类的实例。它是 java.lang.Class 类中的一个方法,允许你在不知道具体类的情况下动态地创建对象。

使用private修饰的方法和修改private修饰的成员变量

使用private修饰的方法需要先设置权限

image-20241106201539822

修改private修饰的成员变量也同理:

image-20241106201958770

这里讲讲invoke方法,invoke 方法位于 java.lang.reflect.Method 类中,用于执行某个的对象的目标方法,一般会和 getMethod 方法配合进行调用。用法如下:

1
public Object invoke(Object obj, Object... args)

第一个参数为类的实例也就是对象,第二个参数为一个数组,接受目标方法的参数。

但是invoke方法的第一个参数并不是固定的:

  • 如果目标方法是普通方法,第一个参数就是类对象;

  • 如果目标方法是静态方法,第一个参数就是类;

该回归到上篇文章所讲的URLDNS了,将它完善。

0x06 完善

回顾思路

完善URLDNS,在于先使URL对象的hashCode属性设置为非-1,再在hashmap.put方法之后,进行序列化之前重新设置hashCode属性为-1。源码如下:

image-20241106112920588

如何解决

先把hashCode属性设置成非-1,暂且设置成1:

image-20241106204601145

但是这样还不够,因为hashCode属性没法自动回归成-1,所以我们还要再主动设置成-1:

image-20241106205141508

如此URLDNS链便完善了!

0x07 内置

利用Java内置的java.lang.Runtime包实现弹出计算器,也就是getshell:(不能直接实例化对象,因为 java.lang.Runtime 是私有的)

image-20241106210341540

他人更精简的代码:

1
2
Class c1 = Class.forName("java.lang.Runtime");  
c1.getMethod("exec", String.class).invoke(c1.getMethod("getRuntime").invoke(c1), "C:\\WINDOWS\\System32\\calc.exe");

0x08 结语

我也没想到,下一篇竟然还是反射,不过会更加进阶。