JAVA反射机制

一、Java反射机制

Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而动态操作类或对象属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。

为什么需要JAVA反射?

举个例子:我们在某个程序中,需要使用实现Animal接口的Dog类,让它叫两声,叫这个动作在接口中就定义了;如果有一天,我们想Cat叫,我们是不是还要修改代码,去new一个Cat,然后把Dog换成Cat呢?然而在现实中,有很多代码的源代码你都拿不到,都修改不了;或者说,你把产品交给客户,还希望他去修改代码?

不如这样:

我们在编写代码的时候,只写一个动物会叫这个功能,我们利用反射创建一个类,然而反射需要的参数是一个字符串,我们将字符串存入配置文件中,这样,我们修改配置文件即可动态产生不一样的代码了,而用户,通过修改配置文件就可以了,他不需要怎么写代码(一般也不会知道)。

使用情景:

情景一:有的类是我们在编写程序的时候无法使用new一个对象来实例化对象的。

例如:

  • 调用来自网络的二进制.class文件,而没有其.java代码
  • 注解-注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。

情景二:动态加载(可以最大限度的体现Java的灵活性,并降低类的耦合度:多态)

有的类可以在用到时再动态加载到jvm中,这样可以减少jvm的启动时间,同时更重要的是动态的加载需要的对象(多态)。例如:

  • 动态代理-在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代方式。这时,就需要反射技术来实现了。

Java反射用白话文来说就是:在编译完后(固定)的还能动态操作Java对象(动态加载)

获取类对象代码实现方式

1
2
3
4
String className     = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

二、ClassLoader类加载

参考文章:

为什么要自己实现一个类加载器?

2.1 类加载器的种类

Java有三种类加载器(面试会问)

  1. 根类加载器(bootstrap class loader)
  2. 扩展类加载器(extension class loader)
  3. 应用类加载器(applicaion class loader)

关系如下图所示:

在这里插入图片描述

双亲委派机制:

  • 定义:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试去加载。这个机制就叫双亲委派机制

双亲委派机制的实现:

  1. 首先,检查请求的类是否已经被加载过了
  2. 未加载,则请求父类加载器去加载对应路径下的类,
  3. 如果加载不到,才由下面的子类依次去加载。

例如:

  • Java.lang.String->根加载器->扩展加载器->本地加载器

2.2 用户自己实现类加载器

1、为什么要实现自己的ClassLoader

  • 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义中class类文件的加载,我们需要自己的ClassLoader
  • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
  • 可以定义类的实现机制,实现类的热部署,如OSGI中的bundle模块就是通过实现自己的ClassLoader实现的。

2、加载class文件

在实现自己的类加载器之前,先来看一下双亲委派机制的代码实现逻辑;因为我们实现的ClassLoader都是继承于java.lang.ClassLoader类,父加载器都是AppClassLoader,所以在上层逻辑中依旧要保证该模型,所以一般不覆盖loadClass函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
//检查指定类是否被当前类加载器加载过
Class c = findLoadedClass(name);
if( c == null ){//如果没被加载过,委派给父加载器加载
try{
if( parent != null )
c = parent.loadClass(name,resolve);
else
c = findBootstrapClassOrNull(name);
}catch ( ClassNotFoundException e ){
//如果父加载器无法加载
}
if( c == null ){//父类不能加载,由当前的类加载器加载
c = findClass(name);
}
}
if( resolve ){//如果要求立即链接,那么加载完类直接链接
resolveClass();
}
//将加载过这个类对象直接返回
return c;
}

从上面的代码中,我们可以看到在父加载器不能完成加载任务时,会调用findClass(name)函数,这个就是我们自己实现的ClassLoader的查找类文件的规则,所以在继承后,我们只需要覆盖findClass()这个函数,实现我们本地加载器中的查找逻辑,而且还不会破坏双亲委派模型。

3、加载资源文件(URL)

我们有时会用Class.getResource():URL来获取相应的资源文件。如果仅仅使用上面的ClassLoader是找不到这个资源的,相应的返回值为null

Class.getResource()的源代码:

1
2
3
4
5
6
7
8
9
10
public java.net.URL getResource(String name) {
name = resolveName(name);//解析资源
ClassLoader cl = getClassLoader();//获取到当前类的classLoader
if (cl==null) {//如果为空,那么利用系统类加载器加载
// A system class.
return ClassLoader.getSystemResource(name);
}
//如果获取到classLoader,利用指定的classLoader加载资源
return cl.getResource(name);
}

通过上面的代码发现Class.getResource()是通过委派给ClassLoader的getResource()实现的,所以我们来看ClassLoader对于资源文件的获取的具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);//这里
}
return url;
}

综上

我们在创建自己的ClassLoader时只需要**覆写findClass(name)和findResource()**即可

实现加载自定义路径下的class文件

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package TestClassLoader;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader extends ClassLoader{
private String classpath;

public MyClassLoader(String classpath){
this.classpath = classpath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getClassFile(name);
byte[] classByte = null;
try {
classByte = getClassBytes(fileName);
}catch (IOException e){
e.printStackTrace();
}
// 利用自身加载器
Class realClass = defineClass(null, classByte, 0, classByte.length);
if (realClass != null) {
System.out.println("由我加载");
return realClass;
}
// 委派给父类加载器
System.out.println("父类加载器加载");
return super.findClass(name);
}
/***
* 获取当前操作系统下的类文件合法路径
* @param name
* @return 合法的路径文件名
*/
private String getClassFile(String name) {
StringBuilder sb = new StringBuilder(classpath);
sb.append("/")
.append(name.replace('.','/'))
.append(".class");
return sb.toString();
}
/***
* 获取指定类文件的字节数组
* @param name
* @return 类文件的字节数组
* @throws IOException
*/
private byte [] getClassBytes ( String name ) throws IOException{
FileInputStream fileInput = new FileInputStream(name);
FileChannel channel = fileInput.getChannel();
ByteArrayOutputStream output = new ByteArrayOutputStream();
WritableByteChannel byteChannel = Channels.newChannel(output);
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int flag;
while ((flag = channel.read(buffer)) != -1) {
if (flag == 0) break;
//将buffer写入byteChannel
buffer.flip();
byteChannel.write(buffer);
buffer.clear();
}
}catch ( IOException e ){
System.out.println("can't read!");
throw e;
}
fileInput.close();
channel.close();
byteChannel.close();
return output.toByteArray();
}

public static void main(String[] args) {
MyClassLoader myClassLoader = new MyClassLoader("F:\\代码审计区\\MyDemo\\Test\\target\\classes\\org\\test");
try {
myClassLoader.loadClass("java.io.InputStream"); //会有父类加载器直接加载掉,不会经过自定义的LoadClass函数
myClassLoader.loadClass("One");
Class Main = myClassLoader.loadClass("Mainx");
}catch ( ClassNotFoundException e ){
e.printStackTrace();
}
}
}

运行结果:

image-20240624101338367

热部署和加密解密的ClassLoader实现,大同小异。只是findClass的逻辑发生改变而已

根据以上实现的类加载器基础上,对加载class后的调用的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("F:\\代码审计区\\MyDemo\\JavaEEServlet\\src\\TestClassLoader");
myClassLoader.loadClass("java.io.InputStream"); //会有父类加载器直接加载掉,不会经过自定义的LoadClass函数
//myClassLoader.loadClass("One");
Class Main = myClassLoader.loadClass("Main");
// 加载class文件后如何使用
Constructor constructor = Main.getConstructor();
Object obj = constructor.newInstance(); //实例化对象
Method method = Main.getMethod("slefsayhello"); //不传参
Method method_1 = Main.getMethod("sayhello",String.class); //传参
method.invoke(obj);
method_1.invoke(obj, "OkMain"); //对应传入参数
}

运行结果:
image-20240624104648810

Mian.class的Java测试代码如下所示:

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


import java.io.IOException;

public class Main {
public void sayhello(String name){
System.out.println("Hi!Hello "+name);
}
public void slefsayhello() throws IOException {
System.out.println("Hi! Hello My Dog!");
// 恶意代码
Runtime.getRuntime().exec("calc");
}

}

三、Java动态代理

所谓静态代理,顾名思义,当确定代理对象和被代理对象后,就无法再去代理另一个对象。

动态代理与静态代理的区别在于,通过动态代理可以实现多个需求。动态代理其实是通过实现接口的方式来实现代理,具体来说,动态代理是通过Proxy类创建代理对象,然后将接口方法“代理”给InvocationHandler接口完成的。

举个现实中的例子:

静态代理就相当于:每多个房东就需要一个中介,这显然不符合生活认知(对于租客来说,如果是用静态代理模式,每当想要换一个房东,那就必须再换一个中介,在开发中,如果有多个中介代码量就更大了)

动态代理就相当于:不管是多一个还是少一个,始终只需要一个中介。

动态代理基础知识:

  • 动态代理的角色和静态代理一样,需要一个实体类,一个代理类,一个驱动器。
  • 动态代理的代理类是动态生成的,静态代理的代理类是我们提前写好的。

3.1 JDK动态代理

JDK的动态代理需要了解两个类

核心:InvocationHandler调用处理程序类和Proxy代理类

1
public interface InvocationHandler

InvocationHandler是由代理实例的调用处理程序实现的接口,

每个代理实例都有一个关联的调用处理程序。

1
Object invoke(Object proxy,Method method,Object[] args);

当在代理实例上调用方法的时候,方法调用将被编码并分派到其调用处理程序的invoke()方法。

代理:Proxy

1
public class Proxy extends Object implements Serializable

3.2 实现动态代理

1、首先是我们的接口类:Say.java

1
2
3
4
5
package DtProcxy;

public interface Say {
public void say();
}

2、然后需要连个实体类去实现这个抽象类:XiaoHong.javaXiaoMing.java

1
2
3
4
5
6
7
8
package DtProcxy;

public class XiaoHong implements Say {
@Override
public void say() {
System.out.println("小红说她想要一个苹果");
}
}
1
2
3
4
5
6
7
8
package DtProcxy;

public class XiaoMing implements Say{
@Override
public void say() {
System.out.println("小明说他想要一座私人飞机!");
}
}

3、动态代理实现类:UserProxyInvocationHandler.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
27
package DtProcxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserProxyInvocationHandler implements InvocationHandler {
Object obj;

public UserProxyInvocationHandler(Object obj) {
this.obj = obj;
}

// 处理代理类实例,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 选择过滤要执行哪些方法
if (method.getName().equals("add") || method.getName().equals("delete")) {
System.out.println("有什么可以帮助你的");
method.invoke(obj, args);
System.out.println("当前方法执行结束...");
}else{
System.out.println("执行了意外之外的方法...");
method.invoke(obj, args);
}
return obj;
}
}

4、启动器:Client.java

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

import java.lang.reflect.Proxy;

public class Client {
public static void main(String[] args) {
XiaoHong xiaohong = new XiaoHong();
XiaoMing xiaoMing = new XiaoMing();

// 下面就是使用代理来执行代理替小明、小红说话的过程
UserProxyInvocationHandler xh = new UserProxyInvocationHandler(xiaohong);
UserProxyInvocationHandler xm = new UserProxyInvocationHandler(xiaoMing);
Say xiaoHoneSay = (Say) Proxy.newProxyInstance(Client.class.getClassLoader(),new Class[]{Say.class},xh);
Say xiaoMingSay = (Say) Proxy.newProxyInstance(Client.class.getClassLoader(),new Class[]{Say.class},xm);
xiaoHoneSay.say();
System.out.println("====================");
xiaoMingSay.say();
}
}

运行结果:

image-20240625123023462

3.3 在反序列化中动态代理的作用

假设存在一个能够漏洞利用的类B.f,比如Runtime.exec这种

我们将入口类定义为A,我们最理想的情况是A[O] -> O.f,那么我们将传进去的参数O替换为B即可。但是在实战情况下这种情况是极少的。

回到实战情况,比如我们的入口类A存在O.abc这个方法,也就是A[O] -> O.abc;而O呢,如果是一个动态代理类,O的invoke方法里存在.f的方法,便可以漏洞利用了,下面为展示整体思路:

1
2
3
4
A[O] -> O.abc
O[O2] invoke -> O2.f // 此时将 B 去替换 O2
最后 ---->
O[B] invoke -> B.f // 达到漏洞利用效果

动态代理在反序列化当中的利用和readObject是异曲同工的。

  • readObject方法在反序列化中会被主动执行
  • invoke方法在动态代理中会自动执行

四、Javassist动态编程

3.1 Javassist简介

动态编程是相对于静态编程而言的一种编程形式,对于静态编程而言,类型检查是在编译时完成,但是对于动态编程来说,类型检查是在运行时完成的。因此所谓动态编程就是绕过编译过程在运行时进行操作的技术

一般来说,在依赖关系需要动态确认或者需要在运行时动态插入代码环境中,需要使用动态编程。

Javassist中最为重要的是ClassPoolCtClassCtMethod以及CtField这4个类。

  • ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
  • CtClass:表示一个类,这些CtClass对象可以从ClassPool获得
  • CtMethods:表示类中的方法
  • CtFields:表示类中的字段

img

Javassist使用流程:

img

3.2 代码实现动态编程

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package TestJavassist;

import javassist.*;

import java.io.File;
import java.io.FileOutputStream;

public class TestDome {

//生成class字节码文件
public ClassPool javassist_make_class() throws Exception {
//获取默认类池
ClassPool classPool = ClassPool.getDefault();
//创建一个类ClassDemo
CtClass ctClass = classPool.makeClass("TestJavassist.Test");
//新建一个int类型名为id的成员变量
CtField id = new CtField(CtClass.intType, "id", ctClass);
//将id设置为public
id.setModifiers(Modifier.PUBLIC);
//将该id属性分配给Test
ctClass.addField(id);
//添加无参构造方法
CtConstructor ctConstructor = CtNewConstructor.make("public Test(){}", ctClass);
ctClass.addConstructor(ctConstructor);
//添加有参数构造方法
CtConstructor ctConstructor1 = CtNewConstructor.make("public Test(int ids){this.id=ids;}", ctClass);
ctClass.addConstructor(ctConstructor1);
//添加普通方法1
CtMethod ctNewMethod = CtNewMethod.make("public void calcDome(){java.lang.Runtime.getRuntime().exec(\"cmd.exe /c calc.exe\");}", ctClass);
ctClass.addMethod(ctNewMethod);
//添加普通方法2
CtMethod ctNewMethod1 = CtNewMethod.make("public void hello(){System.out.println(\"Hello World!\");}", ctClass);
ctClass.addMethod(ctNewMethod1);
//将class文件写入磁盘
//转换成字节流
byte[] bytes = ctClass.toBytecode();
//写入磁盘
File classpath = new File(new File(System.getProperty("user.dir"), "/src/TestJavassist"),"Test.class");
FileOutputStream fos = new FileOutputStream(classpath);
fos.write(bytes);
fos.close();

return classPool;
}
//执行class字节码文件
public void javassist_exec_class(ClassPool classPool) throws Exception {
// ClassPool classPool = ClassPool.getDefault();
//获取javassist的classloader
ClassLoader loader = new Loader(classPool);
System.out.println("loading");
//加载新的class文件
Class<?> clazz = loader.loadClass("TestJavassist.Test");
//反射调用函数
clazz.getMethod("calcDome").invoke(clazz.newInstance());
clazz.getDeclaredMethod("hello").invoke(clazz.newInstance());
}

public static void main(String[] args) throws Exception {
System.out.println("Hello");
TestDome test = new TestDome();
//生成class文件
ClassPool classPool = test.javassist_make_class();
//执行class文件
test.javassist_exec_class(classPool);
}
}

运行结果:

image-20240627102025004

总结:

Javassist可能存在的漏洞点还是在于反射的利用、使用ClassPool生成class文件的过程中,如果存在参数可控的话,也会造成代码注入的漏洞。

五、获取Class对象

Java反射操作的是java.lang.Class对象,所以我们需要先想办法获取到Class对象,通常有以下几种获取方式:

  1. 类名.class
  2. Class.forName(“java.lang.Runtime”)
  3. classLoader.loadClass(“java.lang.Runtime”)

获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:

1
2
Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class

获取Runtime类Class对象代码片段:

1
2
3
4
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象了,反射调用内部类的时候需要使用$来代替,如com.anbai.Test类有一个叫做Hello的内部类,那么调用的时候就应该将类名写成:com.anbai.Test$Hello

六、通过反射执行命令

普通反射执命令

1
2
3
4
Class runtime = Class.forName("java.lang.Runtime");
Method m1 = runtime.getMethod("getRuntime");
Method m2 = runtime.getMethod("exec", String.class);
m2.invoke(m1.invoke(runtime),"calc");

如果WAF拦截Runtime关键字的话,就转出字节数组的方式:

1
2
3
4
5
6
7
8
// java.lang.Runtime
byte[] arr = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
byte[] arr1 = new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}; // getRuntime
byte[] arr2 = new byte[]{101, 120, 101, 99}; // exec
Class runtime = Class.forName(new String(arr));
Method m1 = runtime.getMethod(new String(arr1));
Method m2 = runtime.getMethod(new String(arr2), String.class);
m2.invoke(m1.invoke(runtime),"calc");

七、参考资料

https://www.freebuf.com/articles/web/335236.html

https://www.cnblogs.com/yokan/p/16102570.html

https://kpa1on.github.io/2022/04/27/Java%E5%AE%89%E5%85%A8%E4%B9%8BJavassist%E5%8A%A8%E6%80%81%E7%BC%96%E7%A8%8B/#Javassist%E7%9A%84%E4%BD%BF%E7%94%A8

https://www.javasec.org/javase/CommandExecution/