bios000's Blog

《Java 安全一反射篇》笔记

2023/03/22

关于反射

这里建议先阅读[[java反射学习]]

作者将反射作为漫谈的第一部分内容

对象可以通过反射获取他的类,类可以通过反射拿到所有的方法(包括私有),拿到的方法可以调用,这就是动态特性–”一段代码,改变其中的变量,将会导致这段代码产生功能性的变化“

1
2
3
4
public void execute(String className,String methodName) thorw Exception{
class clazz = class.forName(class Name);
clazz.get(methodName).invoke(clazz.newInstance());
}

作者讲了三种通过类反射的执行payload的方法,这里就直接上代码

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
42
import java.util.Scanner;  

public class Test01 {
public static void main(String[] args) throws ClassNotFoundException {

Scanner sc = new Scanner(System.in);

// forName()
try {
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(
Class.forName("java.lang.Runtime")), new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
);

} catch (Exception e) {
e.printStackTrace();
}

// getClass()
try {
sc.getClass().forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(
Class.forName("java.lang.Runtime")), new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
);

} catch (Exception e) {
e.printStackTrace();
}

//.class
try {
Scanner.class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(
Class.forName("java.lang.Runtime")), new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
);


} catch (Exception e) {
e.printStackTrace();
}
}
}

看着挺长,这里建议弄清Class类[[java反射学习#Class 类的方法]]中getMethod()和[[java反射学习#Method 类方法]]中的invoke()就能其中的含义,主要就在于exec需要Runtime对象来执行
写个拆分的代码示例

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

public class ReflectTest05 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
// 执行命令
String[] cmd = new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};
// 获取方法
Method methodExec = Class.forName("java.lang.Runtime").getMethod("exec", String.class);
//反射生成java.lang.Runtime.getRuntime() 方法,实际运行时返回的是RunTime的实例化对象 -- getRuntime()对象
Method methodGetRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime");
//反射生成java.lang.Runtime.getRuntime类 -- Runtime Class clazzRunTime = Class.forName("java.lang.Runtime");
//执行命令
methodExec.invoke(methodGetRuntime.invoke(clazzRunTime), cmd);

//集合版
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke( Class.forName("java.lang.Runtime")
), cmd
);


}
}

这样写的目的是为了通过反射机制,动态地调用java.lang.Runtime类的exec方法,执行一个命令。cmd是一个字符串参数,表示要执行的命令。

具体来说,这样写的步骤如下:

  1. 用Class.forName(“java.lang.Runtime”)获取java.lang.Runtime类的Class对象¹³。
  2. 用Class对象的getMethod方法,获取Runtime类的exec方法和getRuntime方法的Method对象¹⁴。注意,exec方法需要指定参数类型为String.class,getRuntime方法不需要参数类型。
  3. 用getRuntime方法的Method对象的invoke方法,调用Runtime类的静态方法getRuntime,返回一个Runtime实例²⁴。注意,invoke方法需要传入一个null参数,表示不需要指定对象。
  4. 用exec方法的Method对象的invoke方法,调用Runtime实例的exec方法,执行cmd命令²⁴。注意,invoke方法需要传入两个参数:一个是Runtime实例,一个是cmd字符串。

这里我其实就已经产生疑问了,为啥要写那么长呢,我们明明已经学过[[java反射学习#Class 类的方法]]中的newInstance()方法了
直接写成下面这种方式不行吗

1
2
3
4

Class.forName("java.lang.Runtime").getMethod(
"exec",String.class).invoke(
Class.forName("java.lang.Runtime").newInstance(),"id");

结果作者在第二篇文章中给我解答了,class.newInstance()方法调用的是这个类的无参数构造方法,并且必须保证无参数类构造器是共有的
这里我觉得还是先说一下这个方法是干啥的 #newInstance

1
2
3
4
5
Class.newInstance()是一个反射方法,用于创建类的实例,他的作用相当于使用new关键字调用类的无参数构造器。但是它有一些限制和不足,比如

1. 他只能调用无参数构造器,不能传递参数
2. 他要求被调用的构造器是public类型的,不能调用私有的构造器
3. 它会抛出所有由被调用的构造器抛出的异常

由于java.lang.Runtime类是[[单例模式]],其构造方法是私有的,只能通过Runtime.getRuntime()来获取到Runtime对象

在这里ChatGPT给出了一个新的思路就是如何拿到私有类型构造器实例化,他使用了Constructor.newInstance()方法代替Class.newInstance()
Constructor.newInstance() 可以调用任意类型和参数个数的构造器(其实作者在第三篇也提到这个问题了)
具体代码示例如下

1
2
3
4
5
6
7
8
9
10
// 获取Runtime类的Class对象
Class<Runtime> runtimeClass =Class.forName("java.lang.Runtime");
// 获取Runtime类的私有构造器
Constructor<Runtime> runtimeConstructor = runtimeClass.getDeclaredConstructor();
// 设置构造器的可访问性为true
runtimeConstructor.setAccessible(true);
// 通过构造器创建一个Runtime类的实例对象
Runtime runtime = runtimeConstructor.newInstance();
// 通过反射调用exec方法
runtimeClass.getMethod("exec", String.class).invoke(runtime, "id");

这里还提到了一个invoke的使用 #invoke

1
2
3
4
invoke 的作用是执行方法,他的第一个参数是:

如果这个方法是普通方法,那么第一个参数就是类对象
如果这个方法是静态方法,那么第一个参数就是累

作者还提到了一种情况,就是如果我想要反射的类是没有无参数构造方法,怎么办,其实还是可以用到Constructor.getDeclaredConstructorConsturctor.getConstructor两个方法

这里作者是以另一种命令执行的方式进行演示的

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder")
((ProcessBuilder) claazz.getConsturcot(List.class).newInstance(Arrays.asList("calc.exe"))).start();

这里用到ProcessBuilder两个构造函数的第一个形式,但是你会发现这里用到了强转

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)

一般利用漏洞的时候是没有这种语法的所以就需要使用反射来完成这一步

1
2
3
4
5
Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke(
Class.forName("java.lang.ProcessBuilder").getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));


Class.forName("java.lang.ProcessBuilder").getMethod("start").invoke( Class.forName("java.lang.ProcessBuilder").getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));

然后根据一道题的wp去引出其中的作用,主要是绕过沙箱限制[[安全基础/代码审计/Java安全漫谈/反射/Code-Breaking Puzzles — javacon WriteUp - Ruilin]]

关于类初始化的一点小tips

作者首先讲了关于forName 一共有两个重载,其中一个是我们常用的

  • Class<?> forName(String name)
    而另一个是
  • Class<?> forName(String name, **boolean** initialize, ClassLoder loader)
    这里面第二个参数代表是否初始化,第三个参数就是ClassLoader

这里有两个tips需要搞清楚
第一个是关于.classforName() 的区别

1
使用`.class`来创建Class对象的引用时,不会自动初始化该Class对象,但是使用forName()回自动初始化该Class对象

第二个注意点就是弄清楚类的初始化的流程
这里作者举了一个例子:

1
2
3
4
5
6
7
8
9
10
11
public class Trainprint{
{
System.out.printf("Empty block initial &s\n",this.getclass()); //1
}
static{
System.out.printf("Static initial &s\n",TrainPrint.class); //2
}
public Trainprint(){
System.out.printf("Initial &s\n",this.getclass()); //3
}

类初始化执行的流程是,先执行static静态方法(2),再执行代码块代码(3),最后是构造函数(3)

所以根据该特性,我们如果可以控制反射的类名,就可以尝试编写一个恶意类,把恶意代码放在static()

CATALOG
  1. 1. 关于反射
  2. 2. 关于类初始化的一点小tips