Java反序列化基础篇-03-Java反射进阶
0x00 前言
Java反序列化基础篇的第三篇文章。
Java反序列化基础篇-03-Java反射进阶 | Drunkbaby’s Blog
0x01 知识回顾
setAccessible(true)
setAccessible(),即暴力访问权限。当一个类的构造器、属性、方法被private修饰时,就需要设置
setAccessible(true)
,才能成功访问它们。这种方法可以和
getConstructor
配合使用,再回顾一下getConstructor
方法:
该方法可以拿到指定的Class
对象的构造器。
利用思路:(顺序并不绝对,看个人理解和习惯)
- 先得到
Class
对象 - 再从
Class
对象拿取exec
方法 - 实例化
Class
对象 - 利用
exec
方法
代码实现如下:
但是可以实现的更简单一点:
就其原理,还是前文所述,利用反射机制得以利用类的方法,该方法的实现需要类的实例,类的实例的实现需要构造器,所以就需要getConstructor
,本例访问的构造器需要权限,所以使用setAccessible(true)
,这就是二者配合实现exec
。
forName 的重载方法
Class.forName()
有两个重载方法,如下:
1 | forName(String className) |
- 第一个参数表示类名
- 第二个参数表示是否初始化(第一个重载方法里默认是
true
)- 第三个参数表示类加载器,即告诉Java虚拟机如何加载这个类,Java默认的ClassLoader就是根据类名来加载类, 这个类名是类完整路路径,如
java.lang.Runtime
因此,forName(className)
等价于forName(className, true, currentLoader)
这里要注意是currentLoader,
currentLoader
是指调用forName
方法的类的类加载器,而不是别的类加载器
各种代码块执行顺序
先看下面代码,注意各个代码块:
1 | package com.pax.UnserializeBlog; |
输出:
也就是说先调用static {}
块,再调用{}
块,最后调用当前构造函数内容,这是为什么呢?
static {}
块在类初始化的时候调用,{}
块在构造函数的super()
后但在当前构造函数内容的前⾯调用,最后才是调用当前构造函数内容。
拓展
forName
中的 initialize=true
意味着目标类将会初始化,先看如下代码:(参数name
可控)
1 | public void ref(String name) throws Exception { |
那我们只要把恶意类写在目标类(name
)的static {}
块即可在目标类初始化时调用恶意代码了。
0x02 Java命令执行的三种反射
Java反序列化的三步就是:入口类、链子和命令执行的方法。下面讲讲Java命令执行的三种方法:
(1)Runtime
使用:
思路:
将执行结果的输出流存储在自定义的数组,再把数组的数据读出来。下面是更详细的解释:
- 先调用 getRuntime() 返回一个 Runtime 对象,然后调用 Runtime 对象的 exec 的方法。
- 调用 Runtime 对象的 exec 的方法会返回 Process 对象,调用 Process 对象的 getInputStream() 方法。
- 调用 Process 对象的 getInputStream() 方法,此时,子进程已经执行了 whoami 命令作为子进程的输出,将这一段输出作为输入流传入 inputStream
- OK,我们的第一行就是用来执行命令的,但是我们执行命令需要得到命令的结果,所以需要将结果存储到字节数组当中
这一段代码用来保存运行结果
JAVA
1
2
3
4
5
6
7
8
9 byte[] cache = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
/**
* readLen用于存储每次读取输入流的长度
*/
int readLen = 0;
while ((readLen = inputStream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}
(2)ProcessBuilder
与Runtime类唯一的区别如下:
(3)ProcessImpl
前面的Runtime
和ProcessBuilder
执行命令实际上还是调用了ProcessImpl这个类:
跟进ProcessImpl
类:
构造方法是private修饰的,所以不能直接调用该类。但是我们前面学过反射,所以操作如下:
1 | package com.pax.UnserializeBlog; |
0x03 Java反射修改static final修饰的字段
private
如果类成员变量被private
修饰,使用getDeclaredField
方法即可
static
如果static 单独出现的话,getDeclaredField
也是可以的
final
final字段能否修改,取决于字段是直接赋值还是间接赋值(编译时赋值还是运行时赋值)
直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。
直接赋值
写一个Man
类:
此时Man
类的name
和age
都是直接赋值,反射结果如下:
结果是没变的,说明反射修改字段失败
间接赋值
修改一下Man
类:
结果是成功的:
小结一下:
如果被final修饰的变量是间接赋值也就是在运行时赋值,就可以进行反射调用。
static + final
使用 static final 修饰符的 name 属性,并且是间接赋值,直接通过反射修改是不可以的。师傅们可以自行尝试,这里我们需要通过反射, 把 nameField 的 final 修饰符去掉,再赋值。
修改Man
类:
给出方法:
解析:
modifiers
是一个包含字段修饰符的内部字段(例如private
、final
等)通过
nameField.getClass().getDeclaredField("modifiers")
获取modifiers
字段。将其设置为可访问。
使用
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL)
来移除final
标志。此时
name
字段已经不再是final
,可以用nameField.set(man, new StringBuilder("manbaout"))
来修改name
字段的值。
简而言之,先获取modifiers
字段,对modifiers
字段进行final
修饰符去除,然后再设置新值。
0x04 结语
本篇的知识还是比较多的,好好消化吧,加油!