Java安全-反射

Java安全中很重要的一个模块就是Java的反序列化漏洞,而要进行Java反序列化漏洞的学习,就需要先了解反射的一些知识,在之前的一篇文章中,已经大概介绍了反射的各种用法,这里就不再详细介绍了,需要了解的可以移步博客首页的《Java基础知识》这篇文章进行查看。反射是大多数语言里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性

Java的反射机制,为Java提供了动态特性,那么什么是动态特性呢,简单的来说,就是在通过外部文件配置,在不修改源码的情况下,来控制程序,例如php中的一句话木马的执行就是一个动态特性

在Java安全中,各种和反射有关的payload都会用到如下方法

  • 获取类的方法: forName
  • 实例化类对象的方法: newInstance
  • 获取函数的方法: getMethod
  • 执行函数的方法: invoke

获取class对象,也就是类,一般有三种方法

1、类的.class属性

第一种就是最简单明了的方式,我们可以通过类名的属性class获取,如果你已经加载了了某个类,只是想获取到它的java.lang.Class 对象,那么就直接拿它的class 属性即可

1
Class c1=ReflectDemo.class;

2、实例化对象的getClass()方法

第二种我们可以先实例化一个对象,之后在调用getClass()方法,如果上下文中存在某个类的实例 ,那么我们可以直接通过该方法获取他的类

1
2
ReflectDemo demo2= new ReflectDemo();
Class c2 = demo2.getClass();

3、Class.forName(String className):动态加载类

第三种则是调用Class类中的forName方法,如果你知道这个类的名称,就可以使用forname来获取

1
Class c3 = Class.forName("java.lang.Runtime");

forName

这里重点讲一下forName这个获取类的方法,

forName有两个函数重载:

  • forName(String name)
  • forName(String name, **boolean** initialize, ClassLoader loader)

一般情况下我们用的就是第一个forName的重载,initialize参数表示是否“初始化”,默认值为true,即需要“初始化”,第三个参数先不细说

由于其默认参数为true,所以在使用forName来获取一个Class对象的时候,会自动”初始化“改对象,但这个“初始化”,指的并不是调用这个类的构造函数,所以在“初始化”的时候,构造函数并不会调用,那这个“初始化”是什么意思呢,可以理解为类的初始化

看一下以下代码

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

public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}

public static void main(String[] args) {
new TrainPrint();
}
}

执行结果如下图所示

可以很清楚的看出来,stasic{}是第一个被调用的,然后是{},最后才是构造函数

其中, static {} 就是在“类初始化”的时候调用的,而{} 中的代码会放在构造函数的super() 后面,但在当前构造函数内容的前面。
所以说, forName 中的initialize=true 其实就是告诉Java虚拟机是否执行”类初始化“

举一个P神的文章中的例子

1
2
3
public void ref(String name) throws Exception {
Class.forName(name);//根据name参数获取一个类
}

编写如下恶意类,把恶意执行代码放到static中,就会优先执行,然后填入上面那个可利用的函数中,从而执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}
}
}

简单的利用

在正常情况下,比如说我们要执行一些系统命令,例如调出系统的计算器,需要如下代码

1
2
3
4
5
6
7
8
package reflect;
import java.lang.Runtime;
public class test_RunTime {
public static void main(String[] args) throws Exception {
Runtime r=Runtime.getRuntime();
r.exec("calc.exe");
}
}

可以发现,在正常调用的情况下,我们需要先import Runtime这个包,然后再进行调用,但如果我们拿到的程序没有引入这个包,我们该怎么调用呢,这个时候forName这个方法就非常的有用了,它不需要import就能进行任意类的加载,这对我们攻击者非常有利

在使用forName方法获取到这个类之后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方

class.newInstance() 的作用就是调用这个类的无参构造函数,不过,我们有时候在写漏洞利用方法的时候,会发现使用newInstance 总是不成功,这时候原因可能是:

  1. 你使用的类没有无参构造函数
  2. 你使用的类构造函数是私有的

第一个原因好理解,因为没有无参构造函数,所以自然也调用不了,所以会报错,那第二个是什么意思呢

这就涉及到一个很常见的设计模式:“单例模式”,简单的来说就是只允许进行一次实例化(或者理解为初始化),比如用户要连接一个数据库,作为开发者自然想要一个用户只能跟数据库建立一条连接,以减少带宽和性能浪费,所以在进行后端编写的时候,就可以将数据库连接使用的类的构造函数作为私有类,然后写一个静态的方法获取数据库的连接

1
2
3
4
5
6
7
8
9
public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

这样就只会在类初始化的时候进行一次连接,后面只能通过getInstance 获取这个对象,避免建立多个数据库连接

然后我们再来看看java.lang.Runtime这个类的反射命令执行

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

执行发现报错

根据报错信息就可以看出是由于刚才我们提到的原因,Runtime这个类的构造方法是私有的,所以反射无法进行调用

改成如下形式比较容易看懂

在这里我们也能发现反射和传统方法的区别了,传统方法是对象.方法(),反射中呢,是方法.invoke(对象)

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");//根据java.lang.Runtime获取class
Method execMethod = clazz.getMethod("exec", String.class);//从clazz中获取exec方法
Method getRuntimeMethod = clazz.getMethod("getRuntime");//从clazz中获取getRuntime方法
Object runtime = getRuntimeMethod.invoke(clazz);//执行getRuntime方法,实例化出一个runtime类
execMethod.invoke(runtime, "calc.exe");//通过runtime类执行exec方法,参数为calc.exe