Java安全[反射(2)]

如何利用Runtime构造payload

Java安全[反射(2)]

第一篇讲到过,如果想要加载一个类,可以同forName进行加载,但是正常情况下我们一般用到的是import,所以forName就可以帮助攻击者加载任意类。

内部类

对于$,在很多源码里看到,类名包含$符号,比如在fastjioncheckAutoType时候就会先将 $替换为.

https://github.com/alibaba/fastjson/blob/fcc9c2a/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java#L1038

image-20230907120021390

可以看到类名的$被替换为.来解析,所以$起的作用实际就是查找内部类。

写个例子,在一个普通类My中,写一个内部类Your,然后编译看看output文件夹会生成什么

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

public class InsideClass {
public static void main(String[] args) {
}
}
class My{
class Your{

}
}

image-20230907122251594

可以看到,Your类对应生成了一个My$Your.classMy类对应生成了一个My.class

我们还可以试试加载这两个,看看是否有区别

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

public class InsideClass {
public static void main(String[] args) throws Exception {
Class<?> c1 = Class.forName("reflect2.My$Your");
System.out.println(c1.getName());
}
}
class My{
static {
System.out.println("My类被加载");
}
class Your{
static {
System.out.println("Your类被加载");
}
}
}

image-20230907124919435

发现初始化内部类时,外部类并没有被初始化,所以在一定程度上可以将它们当作两个无关类。

根据上面所说,Java会将$当作 . ,那如果直接把$换成 . 的话会怎么样

image-20230907130041368

发现运行报错,原因是Java编译器有自己的规则,$在它的规则中是外部类和内部类的分隔符,但是如果用 .来分割外部类和内部类就会让其分不清意图,从而报错,虽然其内部会将其当作 . ,但是前提还是$被当作内外部类分割符后处理。

getRuntime

class.newInstrance() 作用是调用这个类中的无参构造函数,但是经常直接在payload中调用newIntstrance时往往会报错,主要有两个原因

  1. 目标类没有无参构造函数
  2. 目标类的构造函数是私有的

最常遇到的情况下是,调用java.lang.Runtime

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

import java.lang.reflect.InvocationTargetException;

public class Instrance {
public static void main(String[] args) throw Exception {
Class<?> cls = Class.forName("java.lang.Runtime");
cls.getMethod("exec",String.class).invoke(cls.getDeclaredConstructor().newInstance(),"calc.exe");
}
}

发现报错中提示java.lang.Runtime是一个私有的类,是无法直接调用其中的方法

image-20230907161320123

继续跟进这个类的内容,

image-20230907161807768

Runtime确实为了安全考虑,将其的构造函数设置为私有,为了不让任何其他人实例化这个类,这里的话就无法通过newInstrance直接进行实例化Runtime,也就无法执行exec函数。

那这里就会有个问题,如果有类的构造函数是私有,那不是代表当用户想要使用这个类时,无法进行实例化,就无法使用,而正常业务中为什么会出现这种情况呢?

其实,这种情况叫做“单例模式”,是一种很常见的业务模式。

比如,网站的数据库连接,当连接成功后,就不需要每用一次就建立一次网站数据库连接,这样就会建立多个数据库连接,造成资源浪费。这样开发者在编写代码时就会将构造函数写出私有,并通过静态方法来获取这个函数。

在第一篇中提到过,初始化时,静态方法和静态变量只加载一次,而创建类对象时,构造函数则会每构造一个类对象就执行一次。

写个代码举个例子,

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

public class PrivateInstance {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("reflect2.TrainDB");
}
}
class TrainDB {
private static final TrainDB instance = new TrainDB();

public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
System.out.println("Done");
}
}

这样只有在初始化时才会执行一次静态变量,实例化TrainDB类,并执行构造函数,而后只能通过调用getInstance() ,才可以获得其实例,但是不会执行构造函数,这样也避免了多次建立实例。

image-20230907172109455

paload构造

回归正题,Runtime也是一样的,它也是单例模式,只能通过Runtime.getRuntime()获取Runtime的实例

image-20230907173650476

那么要构造payload就需要改一下,就不能用newIntstrance进行对Runtime的实例化,只有通过Runtime.getRuntime这个设定的静态方法获取Runtime的实例化后的对象。

1
2
3
4
5
6
7
8
9
package reflect2;
import java.lang.reflect.InvocationTargetException;

public class InstanceRuntime {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");
}
}

在这里,

1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

发现用了getMethod方法和invoke方法,

image-20230909110419443

getMethod

getMethod的作用就算通过反射获得一个类的某个特定的公有方法。其需要两个参数,第一个是方法名,第二个是方法所需参数的类型 [ 比如,字符串就算String.class] 。

但是在Java中支持类的重载,也就是可能存在多个相同的名字的但是参数列表或者类型不同的方法,所以只知道名字并不能直接确认函数。

所以在这里想调用exec方法时,就需要看看在Runtime中其重载列表,看看目标方法中所需的参数类型及其列表。

image-20230909105451350

这里可以用第四个重载类型,只要一个字符串,最简单。而前三个要字符串数组,也就是一个命令加上参数之类的。

所以就得到了通过以下代码获取Runtime.exec方法

1
getMethod("exec",String.class);

getMethod获得这个方法后,就需要执行这个方法,比如传入参数等等。

invoke

invoke的作用就是执行方法,它的第一个参数是:

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

  • 如果这个方法是一个静态的方法,那么第一个参数是类

    原因是普通方法需要类实例化后得到类对象,才可以调用该普通方法,所以需要传入类对象。

    而静态方法不用实例化类,就可以直接调用,所以传入类名即可。

其实转化一下就更加清楚了,

正常调用一个方法是 [1].method([2], [3], [4]...) ,而在反射里就是 method.invoke([1], [2], [3], [4]...) 。其中[1]是类或者类对象,而后[...]就是传入方法的参数。

paload分析

按上述的,分解一下payload

这里先初始化Runtime类,

然后获取Runtimeexec方法,

然后再获取RuntimegetRuntime方法,

然后执行getRuntime获取Runtime的实例化对象,这里invoke传入任何都可以,因为这里getRuntime方法是无参方法,所以不需要参数也行。

最后调用,exec方法,invoke第一个传入Runtime的实例化对象,第二传入执行的命令calc

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
((Method) execMethod).invoke(runtime, "calc");

image-20230909113747032

最后两个疑问,

  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?