反射的进阶知识

关于 java.lang.Runtime

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

为什么要用这个 Runtime 类?

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

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

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

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

这种方法与 getConstructor 配合使用
和 getMethod 类似,getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,

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

1
2
3
4
5
6
7
8
9
10
11
12
13
package urldns;

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");
}
}

forName 的两个重载方法的区别

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

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

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

各种代码块执行顺序

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

// 各种代码块执行顺序
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");
}
}
}

alt text

其实你运行一下就知道了,首先调用的是 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
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
}
}
}

Java 命令执行的三种方式

调用 Runtime 类进行命令执行

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

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);
}
}

大致思路:

  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);
}

ProcessBuilder

1
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
package urldns;

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);
}
}

使用 ProcessImpl

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

因为 ProcessImpl 是私有的方法
alt text

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
package urldns;

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);
}
}

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

private

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

PrivatePerson.java

1
2
3
4
5
6
7
8
9
package urldns;

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
13
14
15
16
17
18
package urldns;

import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class PrivateReflect {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException {
Class c = Class.forName("urldns.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);
}
}

alt text

static

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

StaticPerson.java

1
2
3
4
5
6
7
8
9
10
package urldns;

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
13
14
15
16
17
package urldns;

import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class StaticReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("urldns.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
11
12
package urldns;

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
18
19
20
21
22
package urldns;

import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class FinalStraightReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("urldns.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);
}
}

alt text

间接赋值

InDirectPerson.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package urldns;

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
20
21
22
23
24
package urldns;

import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class InDirectReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("urldns.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);
}
}

成功
alt text

static + final

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

StaticFinalPerson.java

1
2
3
4
5
6
7
8
9
10
package urldns;

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
18
19
20
21
22
23
package urldns;

import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class StaticFinalReflect {
public static void main(String[] args) throws Exception {
Class c = Class.forName("urldns.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);
}
}

alt text

参考https://drun1baby.top/2022/05/29/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-03-Java%E5%8F%8D%E5%B0%84%E8%BF%9B%E9%98%B6/