Diggid's Blog

Java反序列化漏洞基础 - 反射&反序列化

字数统计: 5.4k阅读时长: 22 min
2021/02/22 Share

前言

最近想研究一下Java反序列化漏洞相关的内容,作为学习Java安全的开篇,学习Java反序列化需要一些前置知识,其中就包括Java反射和Java反序列化漏洞的利用基础。理解好这些前置知识,对于后面java反序列化的pop链以及各种Gadget分析有很大的帮助。文章内容解释的比较通俗,因为笔者的Java水平也不高,所以尽量说简单些。这篇文章参阅了很多师傅的理解和总结,感谢师傅们!

Java 反射

什么是Java 反射

简单来说,Java反射就是

Java反射机制允许运行中的Java程序获取自身的信息,操作类和对象的内部属性。

一开始看这句话可能不太理解,但我们重点关注一下:操作类和对象的内部属性。这句话基本上概括了Java反射的功能,因此再通俗一点解释Java 反射:

  • Java 反射是指在程序运行时,对于任何一个类,我们能获取到这个类的所有属性和方法,对于类的实例(对象),我们能调用该对象的任何一个属性和方法。
  • 类的实例对象能过通过反射机制获取到它的类,类又能通过反射得到类中的属性和方法,获取的属性可以赋值,获取到的方法可以调用

Java反射是Java作为静态语言的一种动态特性。

补充一下:

动态语言:Smalltalk,Ruby,Python,PHP,Lua,Perl,Groovy 等

静态语言:C/C++,Java,C# 等

实现Java反射的相关类

上面解释了Java反射的基本概念和功能。对于java来说,实现这些功能主要依靠java.lang.reflect包和java.lang.class类

java.lang.reflect.*

官方文档对这个包的解释是

Provides classes and interfaces for obtaining reflective information about classes and objects. Reflection allows programmatic access to information about the fields, methods and constructors of loaded classes, and the use of reflected fields, methods, and constructors to operate on their underlying counterparts, within encapsulation and security restrictions.

简单翻译一下就是

提供用于获取有关类和对象的反射信息的类和接口。反射允许以编程方式访问有关加载类的字段、方法和构造函数的信息,并允许在封装和安全限制内使用反射字段、方法和构造函数对其底层对应项进行操作。

而在这个包中有许多类,这里我们重点关注MethodConstructor类,具体的作用在后文会说明

image-20210222135930453

java.lang.Class

java.lang.Class类是反射的核心类,这个类继承了java.lang.Object类,实现了Serialize等接口。为什么说他是反射的核心呢?因为该类的许多方法能够实现反射机制,也就是获取类或对象的属性或方法等相关的反射信息。

image-20210222142220911

注意到Class<T>和下面对T的注解,这里是java泛型的一种表示,也就是说这个Class类实例化后是一个具体的类型,可能是Class<String>Class<Class>等,如果在实例化前不能确定类型则需要用Class<?>表示,在实例化后会赋予实际的类型。

这里说一下笔者对这个类的理解:

Class类是一个类,那么肯定有实例对象,而其他一般类(如java.lang.String或自定义的类),这些类经过某类名.class后会得到类本身,这个类本身(某类名.class)是一个对象,这个对象就是Class类的某类实例(如上面解释的Class<String>)。这里有点绕,简单点来说就是某类名.class是某个类从类到Class类实例的转变,得到了某个类的Class类实例,就可以像一般类的对象一样获取类中的属性和方法了,也就是获取Class类中的方法来进行进一步的反射调用。

得到了某类的Class类实例,那么再调用一次getclass()(某类.class==某类实例.getclass()),将会得到Class类的Class类实例,也就是Class<Class>,那么Class<非Class>Class<Class>有什么区别呢?不都是xxx类的Class类实例吗?等到后文说明获取类中的属性或方法的时候就知道了

这里说到了Class类的一些方法,这些方法在后文中会详细解释。

Java 反射能做什么

在上文说到,Java反射依赖于java.lang.reflect.*和java.lang.Class,其中有很多方法为反射机制提供了便利。Java反射除了能获取当前对象或类所包含的属性和方法外,反射机制还能开发各种通用框架。

很多框架都是通过XML文件来进行配置的( 例如 struts.xml , spring-*.xml 等 ) , 即所谓的框架核心配置文件 . 为了确保框架的通用性 , 程序运行时需要根据配置文件中对应的内容加载不同的类或对象 , 调用不同的方法 , 这也依赖于 Java 反射机制 .

这里重点说明Java反射如何一步一步实现获取当前类所包含的属性或者方法并进行相关的调用:

  • 获取一个对象/类的class类实例

  • 通过class类实例获取类的成员变量/方法

  • 构造任意一个类的对象

  • 调用任意一个对象的方法

获取一个对象/类的class类实例

这里获取到的是Class<?>,也就是某类的class实例,有三种方法,三种的结果都是一样的Class<?>

1.obj.getClass()

2.Class.forName(className)

3.className.class

上代码:

image-20210222151253452

这里详细说一下Class.forName(className)这个静态方法。这个方法有两个重载

1.Class.forName( String className )

2.Class.forName( String className , Boolean initialize , ClassLoader loader )

  • String className : 类名
  • Boolean initialize : 是否进行类初始化,默认为初始化
  • ClassLoader loader : 加载器,默认使用当前的加载器,即currentLoader

什么是类初始化

类初始化能执行类中定义的静态代码块的内容,和类的实例化不同

image-20210222152453695

如果我们在某个类的静态代码块中存放恶意代码,如Runtime.getRuntime.exec('id');,那么便能够调用恶意代码

  • 存在如下代码场景
1
2
3
4
5
6
7
8
9
public class Evil1 {
public Evil1(String cls) throws ClassNotFoundException{
Class.forName(cls);
}

public static void main(String[] args) throws ClassNotFoundException{
Evil1 evil = new Evil1("Test3");
}
}

此时如果能控制Test3这个类,在类中的静态代码块中添加恶意代码即可实现任意代码执行

  • 执行任意代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Test3 {
static{
try {
Process p = Runtime.getRuntime().exec("whoami");
InputStream in = p.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while((line = br.readLine())!= null){
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}

执行结果如下:

image-20210222155615506

通过class类实例获取类的成员变量/方法

还是三种方法:

1.Class<T>.getMethod(functionName , [parameterTypes.class])

2.Class<T>.getMethods()

3.Class<T>.getDeclaredMethods()

1.这里详细看一下**getMethod()方法,因为后面构造反序列化payload时使用的比较多,要注意这个方法的参数和返回值**

image-20210222160232750

参数:

  • String name:要获取的方法名

  • Class<?>... parameterTypes:与所要获取的方法所匹配的参数,如有public int concat(int a, int b)这个方法,那么这里的参数应该是("concat", int.class, int.class),但是更常用的写法是new Class[]{int.class,int.class}Class<?>...相当于变长数组Class[]。如果方法无参数,则写为new Class[0]或者null

这里要注意一点,如果要获取的方法无参数,尽量不要不写getMethod()方法的第二个参数,因为在某些情况下是会抛出异常的,如innvoke.invoke()的情况下(等会介绍invoke()方法时会有说明),所以即使无参数,也要写null或者new Class[0]

返回值:

上述几个方法的返回值都是**java.lang.reflect.Method类的实例,后面可以作为调用者或者参数**在invoke方法中使用

2.对于className.getMethods()className.getDeclaredMethods()两个方法,区别就是

  • 前者返回某个类的所有公用(public)方法 , 包括其继承类的公用方法

  • 返回某个类或接口声明的所有方法 , 包括公共、保护、默认(包)访问和私有方法 , 但不包括其继承类的方法

构造任意一个类的对象

有两个方法:

  • Class<T>.newInstance()
  • Class<T>.getConstructor([Class<?>...]).newInstance(传入参数的值)
  • Class<T>.getDeclaredConstructor([Class<?>...]).newInstance(传入参数的值)

前者只能调用无参构造方法来创建对象,后者能调用无参或有参构造方法来创建对象,具体细节的区别可参考这篇文章

此外,这里的newIntsance方法和前面说到的forName方法一样,都会自动执行静态代码块中的内容,因此也可能成为一个漏洞利用的sink点。

image-20210222163308072

调用任意一个实例对象的方法

前面我们通过Class类实例获取到了一个类的方法并且构造出了这个类的实例对象,就差最后一步需要通过java.lang.reflect.Method类的invoke方法来调用实例对象的方法。具体看一下invoke方法的说明

image-20210222174610212

通过Method.invoke(Object obj, Object...args)的形式将反射获得的Method类实例应用于Class类实例,方法的参数为args。相当于obj.Method(args)但是这里还是要注意一下这个方法的参数和返回值,有利于理解反射的一些payload

参数:

  • Object obj:从中调用基础方法的对象。当调用的方法为静态方法时,该参数可以缺省或者为null(尽量写null)
  • Object...args:用于该方法的参数。当方法无参数时,该参数可以是new Object[0]或null(不可缺省)

返回值:

返回的是Object类的实例对象

注意:

前面在说getMethod()这个方法时,当获取的方法无参数时,说到了尽量要写null而不是省略,这里的invoke()方法也有同样的原则,当调用的方法为静态方法时,尽量写null而不是省略。因此,在编写payload时尽量保持getMethod()invoke()这两个方法的参数的完整性,以避免不必要的异常(后面有例子)。

Java 反射 to 代码执行

前面说完了Java 反射的内容,接下来要结合Java反射来分析一下Runtime.getRuntime().exec()这个常见的代码执行语句是怎么来的,并且知道如何用反射的写法来执行Runtime.getRuntime().exec()这个语句,因为在挖链的时候,基本上不可能会直接出现Runtime.getRuntime.exec(),都是通过多组反射来拼凑语句并完成最后的调用的

Runtime.getRuntime().exec()

我们还是按照Java反射的步骤一步一步来构造:

  1. 获取Runtime类
1
Class<?> cls = Class.forName("java.lang.Runtime")
  1. 获取Runtime类的exec方法
1
Method command = cls.getMethod("exec", new Class[]{String.class});
  1. 实例化Runtime类

如果是这么写cls.getConstructor().newInstance()那么会报错,查一下Runtime类的源码就会知道,Runtime类的构造方法是private修饰的,这里有两种解决办法

  • 获取并调用getRuntime()方法,这个方法return new Runtime()便可直接获取Runtime对象
  • 特殊情况中介绍的使用setAccessible(true)来开放访问权
  1. invoke调用exec方法,执行任意命令
1
Object inv = command.invoke(obj, new Object[]{"calc.exe"})

将上面的代码整合到一起,完整的代码如下(这里处理步骤3的方法是调用getRuntime()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class Test4 {

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException{
Class<?> cls = Class.forName("java.lang.Runtime");
Method exec = cls.getMethod("exec", String.class);
Method getRun = cls.getMethod("getRuntime", null);
Object obj = getRun.invoke(cls, new Class[0]);
Object inv = command.invoke(obj, "calc.exe");
}
}

执行带有回显的命令:

java要获取回显比较麻烦,尽量用反弹shell

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
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class Test5 {

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException{
Class<?> cls = Class.forName("java.lang.Runtime");
Constructor<?> cons = cls.getDeclaredConstructor();
cons.setAccessible(true);
Object obj = cons.newInstance();
Method exec = cls.getMethod("exec", String.class);
Process inv = (Process) exec.invoke(obj, new Object[]{"whoami"});

InputStream out = inv.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(out));
String line = null;
try {
while ((line = br.readLine()) != null){
System.out.println(line);
}
}catch (IOException e) {
e.printStackTrace();
}
}
}

java.lang.ProcessBuilder

如果要深究 exec()方法调用系统命令的具体细节,可以查看Runtime类的exec()方法的相应源码,这里直接给出结论:

exec()方法通过 ProcessBuilder 对象创建了一个执行命令的子进程来执行命令

因此我们想到可以直接通过 ProcessBuilder来执行系统命令

先看一下这个类的两个构造函数

image-20210222222633633

注意两种构造方法参数的不同。其他区别不深究

获取到该类的构造方法后,以构造方法规定的参数格式传入要执行的命令进行实例化时,之后调用**start()**方法即可执行命令

1.使用第一种构造方法

部分代码如下,其余部分和前面的差不多

1
2
3
4
Class<?> cls = Class.forName("java.lang.ProcessBuilder");
Method start = cls.getMethod("start", null);
Object obj = cls.getConstructor(List.class).newInstance(Arrays.asList("whoami"));
Process inv = (Process) start.invoke(obj, null);

由于第一种构造方法的参数形式是List<String>,所以我们使用Arrays.asList()方法转成List<String>

image-20210222225107024

2.使用第二种构造方法

1
2
3
4
Class<?> cls = Class.forName("java.lang.ProcessBuilder");
Method start = cls.getMethod("start", null);
Object obj = cls.getConstructor(String[].class).newInstance(new Object[]{new String[]{"whoami"}});
Process inv = (Process) start.invoke(obj, null);

至于newInstance()方法的参数为什么是new Object[]{new String[]{"whoami"}}

笔者的理解是这里涉及参数解封装的概念

image-20210222225451624

image-20210222230021567

newInstance()的参数要求Object...相当于Object[],因此我们传入new Object[]{xxx},而传入构造方法的参数是new Object[]{xxx}内的 xxx,ProcessBuilder的第二个构造方法的参数是String...所以我们传入new String[]{"whoami"},整合起来就是new Object[]{new String[]{"whoami"}}

javax.script.ScriptEngineManager

该类可以获取所有的JS引擎,然后在JS引擎中嵌入java的代码,可以解析java的代码。下面的代码用于获取所有的JS引擎名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf(
"Name: %s%n" + "Version: %s%n" + "Language name: %s%n" +
"Language version: %s%n" +
"Extensions: %s%n" +
"Mime types: %s%n" +
"Names: %s%n",
factory.getEngineName(),
factory.getEngineVersion(),
factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getExtensions(),
factory.getMimeTypes(),
factory.getNames()
);
}
}

可以得到一坨名称

1
Names: [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

通过javax.script.ScriptEngineManager().getEngineByName(<ScriptEngineName>)获取js引擎,然后再调用引擎的eval方法来执行代码,payload

1
new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s)")

几个特殊情况

当类的构造方法不可访问

有两种情况类的构造方法无法访问

  • 类的修饰符缺省

  • 构造方法由private、protected、default(缺省且非同包)修饰时

特别注意第一种,很容易遗漏。首先回顾一下我们从获取类到调用类中方法的流程便可以知道,如果类的构造方法不可访问,会影响两个流程:获取构造方法、通过构造方法实例化类对象,这两个流程其实是上文的构造任意一个类对象的细分。

1.获取构造方法

上面说过,getConstrucor()只能获取可访问的构造方法,而getDeclaredConstrucor()可以获取所有的构造方法

2.实例化

尽管我们通过getDeclaredConstrucor()获取到了不可访问的构造方法,但是其仍不能调用,需要一个方法来开放访问权

Constructor<T>.setAccessible(true);

代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import test.*;

class Test4 {
private Test4(){
System.out.println("Test4构造方法被调用");
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException{
Class<?> cls = Class.forName("Test4");
Constructor<?> cons = cls.getDeclaredConstructor();
cons.setAccessible(true);
Object obj = cons.newInstance();

}
}

invoke.invoke()

这个语句可能一开始看可能不对,假设有这样的场景

1
2
3
4
5
6
7
8
Class<?> cls = Runtime.class.getClass();
Class<?> runTime = Runtime.class;
Method getM = cls.getMethod("getMethod", new Class[]{String.class, Class[].class});
Object getRun = getM.invoke(runTime, new Object[]{"getRuntime", null});
Method invoke = getM.getClass().getMethod("invoke", new Class[]{Object.class, Object[].class});
Method command = runTime.getMethod("exec", String.class);
Object obj = invoke.invoke(getRun, new Object[]{null,null});
Process p = (Process) command.invoke(obj, "calc.exe");

这几行代码有点绕,如果理解了前面的内容,应该能够理解。简单分析一下

  • getM :(Method) java.lang.Class.getMethod()

  • getRun : (Object) java.lang.Runtime.getRuntime()

  • invoke : (Method) java.lang.Method.invoke()

重点关注这句

1
Object obj = invoke.invoke(getRun, new Object[]{null, null});

根据上面的简单分析,这行代码的意思是将invoke方法使用在java.lang.Runtime.getRuntime()这个Object上,而invoke方法的参数为null, null,那么便相当于:

1
getRun.invoke(null, null)

可以回顾一下前面的内容便可以得到下面的转换关系,方便理解invoke.invoke()

1
2
method.invoke(obj,new Object[]{args}) <=> obj.method(args)
上文的:exec.invoke(obj,new Object[]{"calc.exe"}) <=> obj.exec("calc.exe")

但是根据上文所说的invoke方法的第一个参数可省略的情况,是可以省略第一个参数的,也就是写成getRun.invoke(null),即

1
Object obj = invoke.invoke(getRun, new Object[]{null});

但结果是报错

image-20210222232629416

由于能力原因没能解决这个问题,如果有师傅知道这里的原因,或者有更好的解释,感谢赐教!

Java 反序列化

对于java 反序列化的概念和简单的实现这里不过多解释,可以自行google或者菜鸟教程学习一下

实现Java序列化和反序列化

类可反序列化的条件:

  • 实现了Serializable 接口 或 Externalizable 接口
  • 成员变量可反序列化,否则要用transicent(瞬态)声明
  • 在类中重写readObject()方法时实现了defaultReadObject()方法

Serializable & Externalizable

1.Serializable

一个类要想完成序列化操作,必须实现Serializable接口,该接口是标记接口。

官方建议在类中添加标记字段serialVersionUID属性,该属性用static final long修饰

常见的异常:

  • NotSerializableException:标识不可序列化对象的类
  • InvalidClassException:在反序列化过程中会根据这个版本号来判断序列化对象的发送者和接收着是否有与该序列化/反序列化过程兼容的类,通过serialVersionUID 属性来判断,如果不相同则会抛出该异常

2.Externalizable

该接口和Serializable接口的不同点:

  • 需手动编写writeExternal()readExternal()方法来替代 writeObject()readObject()方法

  • 主要用于序列化\反序列化对象中的某个属性

  • readExternal() 方法在反序列化时会调用默认构造函数,实现 Externalizable 接口的类必须要提供一个 Public 修饰的无参构造函数

Demo

写个小Demo来实现以下反序列化

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

public class Ser implements Serializable {
public String name;
public int age;
public transient String address;
public Ser(String name, int age, String address){
this.name = name;
this.age = age;
this.address = address;
}
private void readObject(ObjectInputStream os) throws IOException, ClassNotFoundException{
os.defaultReadObject();
}
public static void main(String[] args) throws IOException, ClassNotFoundException{
Ser obj = new Ser("diggid", 100, "hhh");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser"));
oos.writeObject(obj);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser"));
Ser obj2 = (Ser) ois.readObject();
ois.close();
System.out.println(obj2.name);
}
}

反序列化漏洞

漏洞成因

从上面那个Demo中我们知道了一个完整的序列化和反序列化流程,其中Ser类重写了readObject()方法并在其中实现了defaultObject()方法从而保证反序列化正常执行。

漏洞出现的原因是:在重写readObject()方法时,没有对其他代码进行限制,导致恶意代码也将会被反序列化执行。

1
2
3
4
private void readObject(ObjectInputStream os) throws IOException, ClassNotFoundException{
os.defaultReadObject();
Runtime.getRuntime().exec("calc.exe");
}

注意:readObject()方法的修饰符是private

结合反射

将反序列化漏洞和反射结合起来写一个Demo

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
import java.io.*;
import java.lang.reflect.*;

public class Ser implements Serializable {
static final long serialVersionUID = 1L;
public String name;
public int age;
public transient String address;
public Ser(String name, int age, String address){
this.name = name;
this.age = age;
this.address = address;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException{
ois.defaultReadObject();
//反射调用
Class<?> cls = Class.forName("java.lang.Runtime");
Method command = cls.getMethod("exec", String.class);
Method getRun = cls.getMethod("getRuntime", null);
Object obj = getRun.invoke(cls);
Object inv = command.invoke(obj, "calc.exe");
}

public static void main(String[] args) throws IOException, ClassNotFoundException{
Ser obj = new Ser("diggid", 100, "hhh");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser"));
oos.writeObject(obj);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser"));
Ser obj2 = (Ser) ois.readObject();
ois.close();
System.out.println(obj2.address);
}
}

参考

https://docs.oracle.com/en/java/javase/11/docs/api

https://www.lagou.com/lgeduarticle/94091.html

https://www.jianshu.com/p/95f349258afb

https://www.jianshu.com/p/a1438b476e82

https://www.guildhab.top/?p=6184

https://www.guildhab.top/?p=6337

CATALOG
  1. 1. 前言
  2. 2. Java 反射
    1. 2.1. 什么是Java 反射
    2. 2.2. 实现Java反射的相关类
      1. 2.2.1. java.lang.reflect.*
      2. 2.2.2. java.lang.Class
    3. 2.3. Java 反射能做什么
      1. 2.3.1. 获取一个对象/类的class类实例
      2. 2.3.2. 通过class类实例获取类的成员变量/方法
      3. 2.3.3. 构造任意一个类的对象
      4. 2.3.4. 调用任意一个实例对象的方法
    4. 2.4. Java 反射 to 代码执行
      1. 2.4.1. Runtime.getRuntime().exec()
      2. 2.4.2. java.lang.ProcessBuilder
      3. 2.4.3. javax.script.ScriptEngineManager
    5. 2.5. 几个特殊情况
      1. 2.5.1. 当类的构造方法不可访问
      2. 2.5.2. invoke.invoke()
  3. 3. Java 反序列化
    1. 3.1. 实现Java序列化和反序列化
      1. 3.1.1. Serializable & Externalizable
      2. 3.1.2. Demo
    2. 3.2. 反序列化漏洞
      1. 3.2.1. 漏洞成因
      2. 3.2.2. 结合反射
  4. 4. 参考