Java反序列化基础篇-03-Java反射进阶
Drunkbaby Lv6

Java反射进阶

Java反序列化基础篇-03-Java反射进阶

0x01 前言

前文说到 Java 反射的一些基础知识,这篇讲进阶。

0x02 反射的进阶知识

1. 关于 java.lang.Runtime

  • 关于 Java 的 Runtime 类,有必要还是说一下。

为什么要用这个 Runtime 类?

我们知道 Java 当中很多的 CVE 漏洞,都与反序列化有关,反序列化也与 RCE 有关,而 Runtime 这个类正是用来命令执行的。

最主要的原因,Runtime 类中有 exec 方法,可以用来命令执行。

2. 设置 setAccessible(true)暴力访问权限

在一般情况下,我们使用反射机制不能对类的私有 private 字段进行操作,绕过私有权限的访问。
但一些特殊场景存在例外的时候,比如我们进行序列化操作的时候,需要去访问这些受限的私有字段,这时我们可以通过调用 AccessibleObject 上的 setAccessible() 方法来允许访问。

  • 这种方法与 getConstructor 配合使用

getMethod 类似,getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,

所以必须用参数列表类型才能唯一确定一个构造函数。
还是以弹计算器为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
package src.ReflectDemo;  

import java.lang.reflect.Constructor;

// 进阶使用反射
public class FinalReflectionCalc02 {
public static void main(String[] args) throws Exception{
Class c1 = Class.forName("java.lang.Runtime");
Constructor m = c1.getDeclaredConstructor();
m.setAccessible(true);
c1.getMethod("exec", String.class).invoke(m.newInstance(),"C:\\WINDOWS\\System32\\calc.exe");
}
}

3. forName 的两个重载方法的区别

对于 Class.forName() 方法,有两个重载方法。

1
2
forName(String className)
forName(String name, boolean initialize, ClassLoader loader)
  • 第一个参数表示类名
  • 第二个参数表示是否初始化
  • 第三个参数表示类加载器,即告诉Java虚拟机如何加载这个类,Java默认的ClassLoader就是根据类名来加载类, 这个类名是类完整路路径,如 java.lang.Runtime

因此,forName(className)等价于forName(className, true, currentLoader)

4. 各种代码块执行顺序

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

// 各种代码块执行顺序
public class FunctionSort {
public static void main(String[] args) throws Exception{
Test test = new Test();
}
static class Test{
{
System.out.println("1");
}
static {
System.out.println("2");
}
Test(){
System.out.println("3");
}
}
}

其实你运⾏⼀下就知道了,⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。

其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯,但在当前构造函数内容的前⾯。

所以说, forName 中的 initialize=true 其实就是告诉 Java 虚拟机是否执⾏”类初始化“。

那么,假设我们有如下函数,其中函数的参数name可控:

1
2
3
public void ref(String name) throws Exception {
Class.forName(name);
}

我们就可以编写⼀个恶意类,将恶意代码放置在 static {}中,从⽽进行恶意代码的执⾏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.Runtime;
import java.lang.Process;

public class TouchFile {
static {

try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

0x03 Java 命令执行的三种方式

还记得最开始讲的反序列化吗?反序列化当中我们需要入口类,需要链子,还需要一个命令执行的方法。

1. 调用 Runtime 类进行命令执行

先挂整个程序,再进行细细分析

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

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

// 使用 Runtime 类进行命令执行
public class RuntimeExec {
public static void main(String[] args) throws Exception {
InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream();
byte[] cache = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
while ((readLen = inputStream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}
// laptop-msg46jep\vanhurts

大致思路:

  1. 先调用 getRuntime() 返回一个 Runtime 对象,然后调用 Runtime 对象的 exec 的方法。

  2. 调用 Runtime 对象的 exec 的方法会返回 Process 对象,调用 Process 对象的 getInputStream() 方法。

  3. 调用 Process 对象的 getInputStream() 方法,此时,子进程已经执行了 whoami 命令作为子进程的输出,将这一段输出作为输入流传入 inputStream

  • OK,我们的第一行就是用来执行命令的,但是我们执行命令需要得到命令的结果,所以需要将结果存储到字节数组当中

这一段代码用来保存运行结果

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

  • InputStream inputStream = new ProcessBuilder("whoami)".start().getInputStream();

只是换了一种命令执行的方式,将内容读取出来的语句不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package src.CommandExec;  


import java.io.ByteArrayOutputStream;
import java.io.InputStream;

// 使用 ProcessBuilder 进行命令执行操作
public class ProcessBuilderExec {
public static void main(String[] args) throws Exception{
InputStream inputStream = new ProcessBuilder("ipconfig").start().getInputStream();
byte[] cache = new byte[1024];
int readLen = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((readLen = inputStream.read(cache)) != -1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}
//laptop-msg46jep\vanhurts

3. 使用 ProcessImpl

ProcessImpl 是更为底层的实现,RuntimeProcessBuilder 执行命令实际上也是调用了 ProcessImpl 这个类,对于 ProcessImpl 类我们不能直接调用,但是可以通过反射来间接调用 ProcessImpl 来达到执行命令的目的。

  • 因为 ProcessImpl 是私有的方法
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 src.CommandExec;  


import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Map;

// 使用 ProcessImpl 进行命令执行
public class ProcessImplExec {
public static void main(String[] args) throws Exception{
String[] cmds = new String[]{"whoami"};
Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class,
ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process e = (Process) method.invoke(null, cmds, null, ".", null, true);
InputStream inputStream = e.getInputStream();
byte[] cache = new byte[1024];
int readLen = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((readLen = inputStream.read(cache)) != -1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}
laptop-msg46jep\vanhurts

0x04 Java 反射修改 static final 修饰的字段

private

这个就不用说了,很简单,getDeclaredField 即可

PrivatePerson.java

1
2
3
4
5
6
7
8
  
public class PrivatePerson {
private StringBuilder name = new StringBuilder("Drunkbaby");

public void printName() {
System.out.println(name);
}
}

对应的反射代码 PrivateReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
public class PrivateReflect {  
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.PrivatePerson");
Object m = c.newInstance();
Method PrintMethod = c.getDeclaredMethod("printName");
PrintMethod.invoke(m);
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(m, new StringBuilder("Drunkbaby Too Silly"));
PrintMethod.invoke(m);
}
}

static

static 单独出现的话,getDeclaredField 也是可以的

StaticPerson.java

1
2
3
4
5
6
7
8
public class StaticPerson {  
private static StringBuilder name = new StringBuilder("Drunkbaby");

public void printInfo() {
System.out.println(name);

}
}

StaticReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticPerson");
Object m = c.newInstance();
Method nameMethod = c.getDeclaredMethod("printInfo");
nameMethod.invoke(m);
Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(m,new StringBuilder("Drunkbaby static Silly"));
nameMethod.invoke(m);
}
}

final

刚才使用反射成功修改了 private 修饰的变量, 那么如果是 final 修饰的变量那么还能否使用反射来进行修改呢?这时候就需要分情况了。

final 字段能否修改,有且取决于字段是直接赋值还是间接赋值(编译时赋值和运行时赋值的区别)。直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。

直接赋值

定义直接赋值的 final 修饰符属性

FinalStraightPerson.java

1
2
3
4
5
6
7
8
9
10
public class FinalStraightPerson {  

private final String name = "Drunkbaby";
public final int age = 20-2;

public void printInfo() {
System.out.println(name+" "+age);

}
}

如果我们直接用反射来修改值,是会报错的

FinalStraightReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FinalStraightReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.FinalStraightPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");
nameField.setAccessible(true);
ageField.setAccessible(true);
nameField.set(m,"Drunkbaby as Drun1baby");
ageField.set(m,"19");

printMethod.invoke(m);
}
}

间接赋值

InDirectPerson.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InDirectPerson {  
private final StringBuilder sex = new StringBuilder("male");
// 经过逻辑判断产生的变量赋值
public final int age = (null!=null?18:18);
// 通过构造函数进行赋值
private final String name;
public InDirectPerson(){
name = "Drunkbaby";
}

public void printInfo() {
System.out.println(name+" "+age+" "+sex);

}
}

InDirectReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InDirectReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.InDirectPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
Field ageField = c.getDeclaredField("age");
Field sexField = c.getDeclaredField("sex");
nameField.setAccessible(true);
ageField.setAccessible(true);
sexField.setAccessible(true);
nameField.set(m,"Drunkbaby Too Silly");
ageField.set(m,180);
sexField.set(m,new StringBuilder("female"));
printMethod.invoke(m);
}
}

成功

static + final

使用 static final 修饰符的 name 属性,并且是间接赋值,直接通过反射修改是不可以的。师傅们可以自行尝试,这里我们需要通过反射, 把 nameField 的 final 修饰符去掉,再赋值。

StaticFinalPerson.java

1
2
3
4
5
6
7
8
public class StaticFinalPerson {  
static final StringBuilder name = new StringBuilder("Drunkbaby");

public void printInfo() {
System.out.println(name);

}
}

StaticFinalReflect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StaticFinalReflect {  
public static void main(String[] args) throws Exception {
Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticFinalPerson");
Object m = c.newInstance();
Method printMethod = c.getDeclaredMethod("printInfo");
printMethod.invoke(m);

Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true);
Field nameModifyField = nameField.getClass().getDeclaredField("modifiers");
nameModifyField.setAccessible(true);
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
nameField.set(m,new StringBuilder("Drunkbaby Too Silly"));
nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
printMethod.invoke(m);
}
}

0x05 小结

说实话,感觉自己基础不太牢固吧,还是要努力,反射的东西感觉不多。
命令执行的方式给我一种非常灵活的感觉。

 评论