初学java安全 3.24

一直对java安全处于一个简单了解但是一直迫切学习的阶段

希望可以写一篇博客来督促自己深入学习java安全的知识

java反射

java反射机制是指在java代码执行过程中,对于任意一个类,可以获取这个类的属性与方法;对于任意一个对象,可以获取、修改这个对象的属性值,调用对象的方法。这种动态获取信息和调用方法的技术就是java中的反射机制。

class对象

参考了

1
2
https://zhuanlan.zhihu.com/p/456898438

class类是一个类,而class对象是它的对象.

用文章中的一张图片来理解

class类用来描述类的元数据,每个类都有一个与之关联的class对象,这些在面向对象中都有涉及

每个类都有且仅有一个class对象。通过类和对象都能获取到类的class对象,获取到class对象有下面三种方式。

1.Student.class //通过类名.class的方式获取class对象

2.stu1.getClass() //通过类对象.getClass()的方式获取class对象

3.Class.forName(“com.test.Student”) //通过全限定名的方式获取class对象

Java反射的主要作用是通过class对象来对类的属性和方法进行获取和调用

简单写一段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
25
26
package com.test;

class Student {
int age;
String name;
public Student(){
this.age=10;
this.name="xiaoming";
}
public void sayHello(){
System.out.println("my name is"+this.name+", my age is"+this.age);
}
}
public class Test1 {
public static void main(String[] args) throws Exception{
Student stu1=new Student();
stu1.sayHello();
//通过反射来生成类对象
String class_name="com.test.Student";
String function_name="sayHello";
Student stu2=(Student)Class.forName(class_name).newInstance();
Class.forName(class_name).getMethod(function_name).invoke(stu2);
}
}


记录一下里面的语法

1
Student stu2=(Student)Class.forName(class_name).newInstance();

Class.forName(class_name) 用于根据类的完全限定名(包括包名)加载一个类,并返回该类的 Class 对象。newInstance() 是 Class 类中的一个方法,用来动态地创建类的实例。它相当于使用 new 关键字来创建对象,但 newInstance() 是在运行时动态调用的。newInstance() 已被废弃,建议使用 getConstructor().newInstance() 来创建对象(这点是因为 newInstance() 不能处理参数化构造函数)。但在旧版 Java 中,它仍然有效。(文章写的是newInstance(),所以在这里提及一下)

1
2
Class.forName(class_name).getMethod(function_name).invoke(stu2);

getMethod(function_name) 是 Class 类的一个方法,它返回类中与指定名称和参数匹配的 公共方法。注意,getMethod() 只能获取 公共方法,如果方法是私有的或受保护的,则需要使用 getDeclaredMethod()。invoke(stu2) 是 Method 类的一个方法,它用于调用由 getMethod() 或 getDeclaredMethod() 返回的 Method 对象所表示的方法。

反射机制看起来更加复杂,但是反射的主要作用是动态,调用的类名称和方法都是通过变量传入的,另外,上文提到过的反射机制的一个重要特征是可以调用类的私有方法(包括protected和private)。如下图所示,这种看似违背继承机制的特性却是反射里面极为重要的概念,也是后续很多java反序列化利用链依赖反射机制的重要原因。

到此,我们就先简单总结一下反射的优势:

1、反射是对动态类和方法的调用

2、反射可以调用和修改类的私有属性和方法

java反射机制一般有下面三种用途:字段获取和修改、方法获取和访问、构造函数获取和使用。

Java的反射机制提供了获取属性的方式,同时支持对私有的属性进行获取。一种最典型的获取属性的方式如下图所示:

这里因为age默认是私有访问权限需要使用getDeclaredField()

这样就可以通过反射获取到字段的值。

如果字段是static修饰的,那么在获取字段和修改字段时,可以使用null代替具体的对象stu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.Field;

class Student {
int age;
private static String name;
public Student(){
this.age=10;
this.name="xiaoming";
}
public void sayHello(){
System.out.println("my name is"+this.name+", my age is"+this.age);
}
}
public class Test1 {
public static void main(String[] args) throws Exception {
Student stu1 = new Student();
Class class1=stu1.getClass();
Field field =class1.getDeclaredField("name");
field.setAccessible(true);
field.set(null,"lisi");
System.out.println(field.get(null));

}
}

同样是讲解一下语法,方便回顾

1
2
Field field = class1.getDeclaredField("name");
field.setAccessible(true);

通过反射拿到 name 字段的 Field 对象。因为 name 是 private 的,所以要设置为 setAccessible(true) 以便绕过 Java 的访问控制。

1
field.set(null,"lisi")

field.set(Object obj, Object value) 是用来给字段赋值的。
因为 namestatic 静态字段,不依赖任何对象,所以这里传入的是 null
相当于执行了:Student.name = "lisi";

1
System.out.println(field.get(null));

通过反射读取 static 字段 name 的值(同样传入 null 表示静态字段),打印结果是:

字段基本操作

Java反射机制似乎没有提供获取父类私有字段的方法,那如何才能获取父类的私有属性呢?要获取父类的私有属性字段,需要首先获取父类的class对象,最简单的就是通过clazz.getSuperclass()来获取父类的class对象

跟前面类似直接放图

在获取类的属性字段时,有一个需要特殊注意的类型就是final修饰符。我们把name字段增加final修饰,然后通过反射的方式对name字段进行修改,相关代码如下图所示:

最后的两行代码,一个是通过反射获取到对象stu中保存的name字段的值,一个是通过sayHello方法调用来查看对象中保存的name值。我们会发现一个很奇怪的现象,这两个位置得到的name是不一样的,当final修饰一个数据域时,意义是声明该数据域是最终的,不可修改的。使用传统的方式对final修饰符的字段进行修改,代码运行不会报错,但是实际上却是进行的伪修改,真正的字段值并没有被修改。
1)下面的直接赋值方式不可修改

private final String name= "zhangsan";
2) 经过逻辑判断产生的变量赋值可以修改

private final String name = (null!=null?"zhangsan":"zhangsan");
3)new生成的对象赋值可以修改,如图10所示

private final StringBuilder name = new StringBuilder("zhangsan");
4)通过构造函数进行赋值的final字段可以修改,如图11所示
private final String name;

方法获取和访问

获取方法的函数和获取字段的函数很相似,在使用上也基本一致。这里需要强调的是getMethod方法的第二个参数,代表的是想要获取方法的参数类型。第二个参数需要传入的是参数对应的class对象,多个参数以数组的形式传入。

要使用通过反射获取的方法,需要使用invoke函数。关于invoke方法的使用如下:

invoke(Object obj, Object[] args)

执行通过反射获取的方法,obj代表要执行方法的对象,args代表方法的参数。

示例

如果方法是私有的 并且有多个参数,就应该使用下面的使用方式

一些语法详解:
Method method = class1.getDeclaredMethod("sayHello", new Class<?>[]{String.class, String.class});

getDeclaredMethod(“sayHello”, new Class<?>[]{String.class, String.class}) 用于获取 Student 类中名为 sayHello 的方法,并指定它的参数类型为 String 和 String

method.invoke(stu1, new Object[]{"111", "22"});

method.invoke(stu1, new Object[]{“111”, “22”}) 通过反射调用 sayHello 方法。
stu1 是调用该方法的对象(即方法的实例)。
new Object[]{“111”, “22”} 是方法参数的实际传递,数组中的元素对应 sayHello 方法的两个参数:String chrs 和 String name。
invoke() 方法会执行 sayHello(“111”, “22”),输出 “hello 11122”

剩下的static父类与上面大体类似

构造函数获取和使用

构造函数理论上也是一种方法,只是这是一种特殊的方法。我们很多情况下都需要通过反射获取构造函数,然后调用构造函数生成类的实例。可能某些小伙伴会问,生成类的实例不是直接new就可以了吗,为啥还要用反射这么复杂的调用方式来生成实例?答案是很多情况下,我们遇到的构造函数是private的或者是protected的,又或是我们本身想生成实例的类就是private的,这样我们就不能直接用new来生成实例了,只能通过反射来生成。

一个最简单的通过反射来获取构造函数并通过构造函数生成对象的代码如下:

需要注意的是,通过newInstance生成的对象永远都是Object类型,要实际调用对象的某个方法,需要进行向下强制类型转换。

java反射应用

这里只说反射的一个简单应用,通过反射来隐藏webshell关键字,绕过WAF检测。

最简单的一个java执行命令的方式如下

1
2
Runtime.getRuntime().exec("open /System/Applications/Calculator");

很多WAF在检测webshell的时候会基于关键字Runtime、getRuntime、exec这三个关键字来进行规则匹配。下面我们就通过反射来隐藏着三个关键字,如下图所示。我们把着三个关键字都写成了字符串变量,稍微做一下字符串拼接也就没有这三个关键字了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.test;

import java.lang.reflect.Method;

public class Shell {
public static void main(String[] args) throws Exception {
Class class1 =Class.forName("java.lang.Runtime");
Method method1=class1.getMethod("getRuntime");
Runtime obj=(Runtime) method1.invoke(null,new Object[]{});
Method method2=class1.getMethod("exec",String.class);
method2.invoke(obj,"open -a Calculator");
}

}

可以分成几步来看整个实现过程:
1) 由于Runtime类的getRuntime方法属于静态方法,所以我们这里不需要生成一个Runtime对象,直接传递null就可以调用getRuntime方法。

2) getRuntime()方法会返回一个Runtime类的对象,但是由于我们是通过反射的方式实现的,所以要进行一次强制类型转换。

3) 继续通过反射获取Runtime类的exec方法,传递一个String类型的参数,这个方法不是静态方法了,所以必须传入一个Runtime对象,也就是上一步返回的对象。

4) 隐藏关键字,执行命令。

通过相同的办法,可以隐藏其他冰蝎、哥斯拉等webshell的关键字,如果WAF不拦截反射相关的函数,那就可以通过反射来进行任意操作。