最近在看Phith0n师傅的知识星球的Java安全漫谈系列,记点东西吧
关于反射的基础知识可以看我之前的黑马程序员Java教程学习笔记(六)
Java的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。 这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。
对象通过反射可以获取类的全部成员(构造器、属性、方法[包括私有方法]),通过反射可以读取和修改编译后生成的class
文件
反射首先就是要获取Class
类对象,而获取 “类” 也就是java.lang.Class
对象有以下三种方式:
Class c1 = Class.forName("类全名");
Class c2 = 类名.class;
Class c3 = 对象.getClass();
其次在反射中对于获取类对象、方法、以及调用方法也很重要:
forName
方法
forName
方法有两个构造器
public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
第一个参数是类全名、第二个参数表示是否初始化、第三个参数就是ClassLoader
,而第一种构造器等同于Class.forName(className, true, currentLoader)
,也就是第二种方式的封装。
ClassLoader
就是加载器,Java默认的就是根据类全名来加载类,例如:java.lang.Runtime
第二个参数initialize
的“初始化”到底是什么阶段的初始化看下面这个例子就明白了
package com.mochu.src1;
public class TrainPrint {
// 实例代码块,创建对象调用构造器时执行,且在调用构造器之前执行
{
System.out.println("实例代码块被执行");
}
// 静态代码块,随着类的加载而加载,用于静态数据初始化
static {
System.out.printf("静态代码块被初始化");
}
// 无参构造器,创建对象实例化时调用
public TrainPrint() {
System.out.printf("无参构造器被执行");
}
}
package com.mochu.src1;
public class Test {
public static void main(String[] args) throws Exception {
ref(TrainPrint.class.getName());
}
public static void ref(String name) throws Exception {
Class.forName(name);
}
}
显而易见forName
当中的initialize=true
就是类的初始化阶段,在这个阶段静态代码块会随着类的加载而加载
而以上的三个“初始化”方法的顺序就是:静态代码块 -> 实例代码块 -> 无参构造器
代码块相关知识可以看我之前的:黑马程序员Java教程学习笔记(四)
那么我们可以往静态代码块中添加恶意代码,然后通过forName()
触发
package com.mochu.src1;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
public class EvilClass {
static {
try{
InputStream is = Runtime.getRuntime().exec("whoami").getInputStream();
byte[] buffer = new byte[1024];
int readSize = 0;
ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
while ((readSize = is.read(buffer)) != -1) {
infoStream.write(buffer, 0, readSize);
}
System.out.println(infoStream.toString());
}catch (Exception e) {
// do noting
}
}
}
package com.mochu.src1;
public class Test {
public static void main(String[] args) throws Exception {
ref(EvilClass.class.getName());
}
public static void ref(String name) throws Exception {
Class.forName(name);
}
}
这样利用反射就可以加载任意类,并且内部类也是可以通过反射获取到成员信息,普通类C1
编写内部类C2
,在编译之后会生成两个class
文件,可以通过forName("C1$C2");
加载这个内部类。
newInstance()
方法
注:JDK9开始,弃用了newInstance()
而使用getDeclaredConstructor().newInstance()
进行替换
newInstance()
方法的作用就是调用类的无参构造器得到一个对象,不过需要注意的是有时候newInstance()
会失败,可能有以下原因:
- 使用的类没有无参构造器
- 使用的类的构造器是私有的
比如:
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "whoami");
会报错
原因是Runtime
类的构造器是私有的
这种将构造器私有的操作,常见于一些设计模式,例如:“单例模式”
单例模式可以看我之前的:黑马程序员Java教程学习笔记(四)
Runtime
类就是 “懒汉单例模式” :构造器私有,静态变量存储对象,提供返回单例对象的方法
正确的方法应该是调用getRuntime()
方法来获取Runtime
对象
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc");
getMethod()
方法
public Method getMethod(String name, Class<?>... parameterTypes)
getMethod
的作用是通过反射获取一个类的某个public
方法,第一个参数是方法名,第二个参数是参数类型且支持可变参数
invoke()
方法
public Object invoke(Object obj, Object... args)
invoke()
方法的作用是调用方法执行,它的第一个参数是:
- 如果调用的方法是普通方法,那么第一个参数是类对象
- 如果调用的方法是静态方法,那么第一个参数是类
可以这么理解:
正常执行方法是:[对象/类名].[方法名](参数一, 参数二, 参数三...)
反射中执行方法:[方法名].invoke([对象/类名], 参数一, 参数二, 参数三...)
所以可以将命令执行的Payload分解一下:
Class clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Method execMethod = clazz.getMethod("exec", String.class);
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc");
getConstructor()
方法
public Constructor<T> getConstructor(Class<?>... parameterTypes)
返回一个构造器对象(只能返回public
的),接收的参数是构造器的参数列表,所以必须确定参数列表类型,获取到构造器之后,使用newInstance
来执行命令。
比如,使用另一种命令执行的方式ProcessBuilder
ProcessBuilder
有两个构造器:
ProcessBuilder(String... command)
ProcessBuilder(List<String> command)
通过getMethod("start")
获取到start方法,然后invoke
执行,invoke
的第一个参数就是ProcessBuilder Object
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc")));
如果使用ProcessBuilder (List<String> command)
这个构造器,就需要设计到可变参数,可变参数内部本质就是一个数组
可变参数可以看我之前的:黑马程序员Java教程学习笔记(五)
public void hello(String[] names) {}
public void hello(String...names) {}
那么对于反射来说,如果要获取的目标构造器中包含了可变参数,其实默认它是数组就行了。
所以,我们将字符串数组的类String[].class
传给getConstructor
,获取ProcessBuilder
的第二种构造器,在调用newInstance
的时候,因为这个构造器本身接收的是一个可变参数,我们传给ProcessBuilder
的也是一个可变参数,二者叠加为一个二维数组。
Class clazz1 = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"notepad"}}));
那么如果一个方法或者构造器是私有的,如何执行它?
这就需要使用getDeclared
系列的方法进行反射了,具体可以看我之前的黑马程序员Java教程学习笔记(六)中关于反射的相关知识
我们拿之前Runtime
类来测试,Runtime
类的构造器是私有的,使用getDeclaredConstructor
(方法存在就能拿到)获取Runtime
类的私有构造器,不过需要使用setAccessible(true)
来打开权限(暴力反射)
不过这种暴力反射破坏封装性
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class);
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(constructor.newInstance(), "calc");