Java安全-反射
Java安全-反射
Java安全中很重要的一个模块就是Java的反序列化漏洞,而要进行Java反序列化漏洞的学习,就需要先了解反射的一些知识,在之前的一篇文章中,已经大概介绍了反射的各种用法,这里就不再详细介绍了,需要了解的可以移步博客首页的《Java基础知识》这篇文章进行查看。反射是大多数语言里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性
Java的反射机制,为Java提供了动态特性,那么什么是动态特性呢,简单的来说,就是在通过外部文件配置,在不修改源码的情况下,来控制程序,例如php中的一句话木马的执行就是一个动态特性
在Java安全中,各种和反射有关的payload都会用到如下方法
- 获取类的方法:
forName
- 实例化类对象的方法:
newInstance
- 获取函数的方法:
getMethod
- 执行函数的方法:
invoke
获取class对象,也就是类,一般有三种方法
1、类的.class属性
第一种就是最简单明了的方式,我们可以通过类名的属性class获取,如果你已经加载了了某个类,只是想获取到它的java.lang.Class 对象,那么就直接拿它的class 属性即可
1 | Class c1=ReflectDemo.class; |
2、实例化对象的getClass()方法
第二种我们可以先实例化一个对象,之后在调用getClass()方法,如果上下文中存在某个类的实例 ,那么我们可以直接通过该方法获取他的类
1 | ReflectDemo demo2= new ReflectDemo(); |
3、Class.forName(String className):动态加载类
第三种则是调用Class类中的forName方法,如果你知道这个类的名称,就可以使用forname来获取
1 | Class c3 = Class.forName("java.lang.Runtime"); |
forName
这里重点讲一下forName这个获取类的方法,
forName有两个函数重载:
forName(String name)
forName(String name, **boolean** initialize, ClassLoader loader)
一般情况下我们用的就是第一个forName的重载,initialize
参数表示是否“初始化”,默认值为true,即需要“初始化”,第三个参数先不细说
由于其默认参数为true,所以在使用forName来获取一个Class对象的时候,会自动”初始化“改对象,但这个“初始化”,指的并不是调用这个类的构造函数,所以在“初始化”的时候,构造函数并不会调用,那这个“初始化”是什么意思呢,可以理解为类的初始化
看一下以下代码
1 | package reflect; |
执行结果如下图所示
可以很清楚的看出来,stasic{}
是第一个被调用的,然后是{}
,最后才是构造函数
其中, static {}
就是在“类初始化”的时候调用的,而{} 中的代码会放在构造函数的super() 后面,但在当前构造函数内容的前面。
所以说, forName
中的initialize=true
其实就是告诉Java虚拟机是否执行”类初始化“
举一个P神的文章中的例子
1 | public void ref(String name) throws Exception { |
编写如下恶意类,把恶意执行代码放到static中,就会优先执行,然后填入上面那个可利用的函数中,从而执行命令
1 | import java.lang.Runtime; |
简单的利用
在正常情况下,比如说我们要执行一些系统命令,例如调出系统的计算器,需要如下代码
1 | package reflect; |
可以发现,在正常调用的情况下,我们需要先import Runtime这个包,然后再进行调用,但如果我们拿到的程序没有引入这个包,我们该怎么调用呢,这个时候forName这个方法就非常的有用了,它不需要import就能进行任意类的加载,这对我们攻击者非常有利
在使用forName方法获取到这个类之后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方
法
class.newInstance() 的作用就是调用这个类的无参构造函数,不过,我们有时候在写漏洞利用方法的时候,会发现使用newInstance 总是不成功,这时候原因可能是:
- 你使用的类没有无参构造函数
- 你使用的类构造函数是私有的
第一个原因好理解,因为没有无参构造函数,所以自然也调用不了,所以会报错,那第二个是什么意思呢
这就涉及到一个很常见的设计模式:“单例模式”,简单的来说就是只允许进行一次实例化(或者理解为初始化),比如用户要连接一个数据库,作为开发者自然想要一个用户只能跟数据库建立一条连接,以减少带宽和性能浪费,所以在进行后端编写的时候,就可以将数据库连接使用的类的构造函数作为私有类,然后写一个静态的方法获取数据库的连接
1 | public class TrainDB { |
这样就只会在类初始化的时候进行一次连接,后面只能通过getInstance 获取这个对象,避免建立多个数据库连接
然后我们再来看看java.lang.Runtime
这个类的反射命令执行
1 | Class clazz = Class.forName("java.lang.Runtime"); |
执行发现报错
根据报错信息就可以看出是由于刚才我们提到的原因,Runtime这个类的构造方法是私有的,所以反射无法进行调用
改成如下形式比较容易看懂
在这里我们也能发现反射和传统方法的区别了,传统方法是对象.方法(),反射中呢,是方法.invoke(对象)
1 | Class clazz = Class.forName("java.lang.Runtime");//根据java.lang.Runtime获取class |