一、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有三种类加载器(面试会问)
根类加载器(bootstrap class loader)
扩展类加载器(extension class loader)
应用类加载器(applicaion class loader)
关系如下图所示:
双亲委派机制:
定义:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器 中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试去加载。这个机制就叫双亲委派机制 。
双亲委派机制的实现:
首先,检查请求的类是否已经被加载过了
未加载,则请求父类加载器去加载对应路径下的类,
如果加载不到,才由下面的子类依次去加载。
例如:
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(); if (cl==null ) { return ClassLoader.getSystemResource(name); } 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); } private String getClassFile (String name) { StringBuilder sb = new StringBuilder (classpath); sb.append("/" ) .append(name.replace('.' ,'/' )) .append(".class" ); return sb.toString(); } 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.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" ); myClassLoader.loadClass("One" ); Class Main = myClassLoader.loadClass("Mainx" ); }catch ( ClassNotFoundException e ){ e.printStackTrace(); } } }
运行结果:
热部署和加密解密的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" ); Class Main = myClassLoader.loadClass("Main" ); 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" ); }
运行结果:
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.java
、XiaoMing.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(); } }
运行结果:
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中最为重要的是ClassPool
、CtClass
、CtMethod
以及CtField
这4个类。
ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
CtClass:表示一个类,这些CtClass对象可以从ClassPool获得
CtMethods:表示类中的方法
CtFields:表示类中的字段
Javassist使用流程:
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 { public ClassPool javassist_make_class () throws Exception { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("TestJavassist.Test" ); CtField id = new CtField (CtClass.intType, "id" , ctClass); id.setModifiers(Modifier.PUBLIC); 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); CtMethod ctNewMethod = CtNewMethod.make("public void calcDome(){java.lang.Runtime.getRuntime().exec(\"cmd.exe /c calc.exe\");}" , ctClass); ctClass.addMethod(ctNewMethod); CtMethod ctNewMethod1 = CtNewMethod.make("public void hello(){System.out.println(\"Hello World!\");}" , ctClass); ctClass.addMethod(ctNewMethod1); 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; } public void javassist_exec_class (ClassPool classPool) throws Exception { ClassLoader loader = new Loader (classPool); System.out.println("loading" ); 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 (); ClassPool classPool = test.javassist_make_class(); test.javassist_exec_class(classPool); } }
运行结果:
总结: Javassist
可能存在的漏洞点还是在于反射的利用、使用ClassPool
生成class文件的过程中,如果存在参数可控的话,也会造成代码注入的漏洞。
五、获取Class对象 Java反射操作的是java.lang.Class
对象,所以我们需要先想办法获取到Class对象,通常有以下几种获取方式:
类名.class
Class.forName(“java.lang.Runtime”)
classLoader.loadClass(“java.lang.Runtime”)
获取数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:
1 2 Class<?> doubleArray = Class.forName("[D" ); Class<?> cStringArray = Class.forName("[[Ljava.lang.String;" );
获取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 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 }; byte [] arr2 = new byte []{101 , 120 , 101 , 99 }; 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/