声明
本文属于OneTS安全团队成员mes9s0的原创文章,转载请声明出处!本文章仅用于学习交流使用,因利用此文信息而造成的任何直接或间接的后果及损失,均由使用者本人负责,OneTS安全团队及文章作者不为此承担任何责任。
对于Java反序列化漏洞来说,Java反射机制必须理解,本文专门用来说明究竟什么才是Java反射。
什么是Java反射
Java 反射机制允许运行中的Java程序获取自身的信息, 操作类和对象的内部属性。
Java 反射机制是指在程序运行时,对于任何一个类,都能知道这个类的所有属性和方法,对于任何一个实例对象 , 都能调用该对象的任何一个属性和方法。
Java中这种 " 动态获取信息 " 和 " 动态调用属性方法 " 的机制被称为 Java 反射机制。
实例对象可以通过反射机制获取它的类 , 类可以通过反射机制获取它的所有方法和属性 . 获取的属性可以设值 , 获取的方法可以调用。
在静态语言中 , 一般对象的类型都是在编译期就确定下来的 . 而通过 Java 反射机制 , 可以动态的创建对象并调用其方法和属性 。
Java反射的功能
正是因为 PHP 中存在多种动态特性 , 使得开发人员能通过很少的代码来实现非常多的功能。
比较典型的例子就是一句话木马 , 通过一行 <?php @eval($_POST[cmd]);能实现目录管理 , 命令执行 , 数据库连接 , 文件上传下载等多种多样的功能 。
但是 Java 本身是一门静态语言 , 无法像 PHP 那么灵活多变 。但是通过 Java 反射机制 , 可以为自身提供一些动态特性。
当我们在通过 IDE 写代码时 , 敲击点号" . " , 会出现当前对象或类所包含的属性和方法 。这里用到的就是 Java 反射机制。
而且 , 反射最重要的用途是开发各种通用框架 . 很多框架都是通过XML文件来进行配置的( 例如 struts.xml , spring-*.xml 等 ) , 即所谓的框架核心配置文件 。
为了确保框架的通用性 , 程序运行时需要根据配置文件中对应的内容加载不同的类或对象 , 调用不同的方法 , 这也依赖于 Java 反射机制 。
综上所述 , Java 反射机制的功能可分为如下几点 :
1、在程序运行时查找一个对象所属的类
2、在程序运行时查找任意一个类的成员变量和方法
3、在程序运行时构造任意一个类的对象
4、在程序运行时调用任意一个对象的方法
查找一个对象所属的类
如何获取一个类( java.lang.Class )呢?
总的而言有三种方法:
▪obj.getClass()
▪Class.forName(className)
▪className.class
具体的使用方法如下所示:
//查找对象所属的类
public class getClass {
public getClass(String name){
System.out.println(name);
}
public static void main(String[] args) throws ClassNotFoundException {
getClass gc1 = new getClass("mes9s0");
//已知上下文中存在某个类的实例对象名称
//可以调用obj.getClass()获取实例对象所属的类
System.out.println("通过obj.getClass()获得所属的类:" + gc1.getClass());
//已知某个类的名称
//可以调用Class.getClass("className")来获取类
System.out.println("通过Class.forName('className')获取类:" + Class.forName("getClass"));
//当已经加载了某个类
//可以通过className.class属性来获取类
System.out.println("通过className.class属性获取类:"+getClass.class);
}
}
针对不同的情况 , 可以用不同的方法来获取类 。
需要注意 : forName( ) 函数有两个重载 , 如下所示 :
Class.forName( String className )
Class.forName( String className , Boolean initialize , ClassLoader loader )
1、String className : 类名
2、Boolean initialize : 是否进行类初始化
3、ClassLoader loader : 加载器( 告诉 Java 虚拟机如何加载获取的类 , Java 默认根据类名( 即类的绝对路径 , 例如 java.lang.Runtime( ) )来加载类,Runtime类在Java安全从零到一(3)中讲过 )
其中 , 第一种方法是对第二种方法的封装 , 存在以下对应关系 :
Class.forName( String className ) == Class.forName( String className , True , currentLoader )
那么这个类初始化是指什么呢 ?
//类初始化
public class test1 {
{
System.out.println("构造代码块");
}
static {
System.out.println("静态代码块");
}
public test1() {
System.out.println("构造函数");
}
public void test1test() {
System.out.println("普通代码");
}
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cls = Class.forName("test1");//类初始化,初始化时会加载静态代码块
System.out.printf(String.valueOf(cls));
}
}
这个部分在我的Java安全从零到一(2)中讲过,可以详细翻看。
结果表明 , 在 Java 类初始化时 , 会执行静态代码块中的内容 。
那也就是说 , 如果我们能控制一个类 , 那么就可以通过在类中添加包含恶意代码的静态代码块。当类初始化时 , 默认会自动执行恶意代码. 如下所示 :
1、假设存在如下代码 :
public class vul {
public vul(String string) throws ClassNotFoundException {
Class.forName(string);
}
public static void main(String[] args) throws ClassNotFoundException {
vul v = new vul("test2");
//实例化 vul 类,调用构造方法 vul, vul()方法中通过 Class.forName(className) 方法来获取类,获取类时默认进行类初始化,调用静态代码块 static{}
}
}
此时 , 如果我们能控制 test2类 , 那就能执行任意代码 .
2、构造恶意的 test2 类:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class test2 {
static {
try {
//执行系统命令
Process p = java.lang.Runtime.getRuntime().exec("id");
//获取p的标准输入流作为输入字节流
InputStream is = p.getInputStream();
//字节流转化为字符流
InputStreamReader isr = new InputStreamReader(is);
//将字符流存入缓冲区
BufferedReader br = new BufferedReader(isr);
String line = null;
//逐一读取字符流中缓冲区的每一行
while ((line = br.readLine()) != null) {
System.out.printf(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个恶意类的内容是在该类静态代码块中 , 通过 java.lang.Runtime.getRuntime( ).exec( ) 执行系统命令 , 并将返回字节流转换为字符流 , 存入缓冲区后逐行读取并输出 。
3、当调用 vul 类时 , 会自动执行恶意代码 。
查找任意一个类的成员变量和方法
如何获取某一个类的所有方法呢?
总的来说有三种方法 :
▪className.getMethod(functionName , [parameterTypes.class])
▪className.getMethods()
▪className.getDeclaredMethods()
//查找一个类的方法
import java.lang.reflect.Method;
public class getMethod {
class methodClass {
public int add(int a, int b) {
return a + b;
}
}
public static void main(String[] args) throws NoSuchMethodException {
Class<?> cls = methodClass.class;//通过className.class获取类
//获取类方法的三种方式
Method method = cls.getMethod("add", int.class, int.class);
Method[] methods = cls.getMethods();//获取某个类的public方法
Method[] declareMethods = cls.getDeclaredMethods();//获取某个类的公共,保护,默认方法,不包括继承
//输出结果
//className.getMethod
System.out.println("getMethod获取方法" + method);
System.out.println("\ngetMethods获取的方法:");
for (Method m : methods) {
System.out.println(m);
}
System.out.println("\ngetDeclaredMethods获取的方法");
for (Method m : declareMethods) {
System.out.println(m);
}
}
}
getMethod( ) : 返回类中一个特定的方法。其中第一个参数为方法名称 , 后面的参数为方法的参数对应 Class 的对象。
getMethods( ) : 返回某个类的所有公用(public)方法 , 包括其继承类的公用方法。
getDeclaredMethods( ) : 返回某个类或接口声明的所有方法 , 包括公共、保护、默认(包)访问和私有方法 , 但不包括其继承类的方法。
补充:$是内部类的意思
构造任意一个类的对象
上文提到了可以通过三种方式来获取类 , 那么如果获取一个实例对象呢 ?
通过 className.newInstance() 构建一个实例对象。
我们都知道在类实例化时会调用构造函数 , 而构造函数又分为 " 有参构造函数 " 和 " 无参构造函数 " 。
然而 className.newInstance() 没有参数 , 只能调用无参构造函数(注意,该方法已经被弃用,新的在代码中) . 如果我们想要调用有参构造函数 , 就必须依赖于 Class 类的 getConstructor() 方法 。
通过 Class 类的 getConstructor() 方法 , 可以获取 Constructor 类的一个实例 , Constructor 类也存在一个 newInstance() 方法 , 不过该方法可以携带参数 . 用该方法来创建实例对象可以调用有参构造函数 。
//构造任意一个类的对象
import java.lang.reflect.InvocationTargetException;
public class newInstance {
//无参构造函数
public newInstance(){
System.out.println("这是无参构造函数");
}
//有参构造函数
public newInstance(String str){
System.out.println(str);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class<?> cls = Class.forName("newInstance");
System.out.println("通过className.newInstance()创建实例对象,默认调用无参构造函数,但是该方法已被弃用");
newInstance obj1 = (newInstance)cls.newInstance();
System.out.println("通过className.getConstrutor().newInstance()创建实例对象,可以添加参数调用有参构造函数");
newInstance obj2 = (newInstance)cls.getConstructor(String.class).newInstance("这里有参构造函数");
System.out.println("此处为新的无参构造函数调用");
newInstance obj3 = (newInstance)cls.getDeclaredConstructor().newInstance();
}
}
▪className.newInstance()
▪className.getConstructor( parameterType ).newInstance( parameterName )
▪className.getDeclaredConstructor().newInstance()
因此 , 我们可以通过 newInstance() 方法来构造任何一个类的对象。并且可以选择是调用其无参构造方法 , 还是有参的构造方法 。
调用任意一个实例对象的方法
有了实例对象 , 如何调用调用该对象的方法呢 ?
一般来说 , 可以通过 objectName.functionName() 这种格式来调用实例方法,举个例子 。
/调用任意一个实例对象的方法
import java.lang.reflect.InvocationTargetException;
public class invoke {
public String prt(String name) {
return name;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String name = "mes9s0";
Class<?> cls = Class.forName("invoke");
invoke ivk = (invoke) cls.getDeclaredConstructor().newInstance();
//通过objectName.functionName()来调用实例方法
String str1 = ivk.prt(name);
System.out.println(str1);
}
}
但在很多情况下 , 并不知道类名, 也就无法 new 出实例对象 , 更别提调用实例对象的方法了 。
当遇到这种情况时 , 就需要使用 Java 反射来调用实例对象的方法了 。
以下就是思路了:
➡不知道类怎么办 ?
我们可以通过 obj.getClass() , Class.forName(className) , className.class 来获取类.
➡不知道类有哪些方法怎么办 ?
可以通过 className.getMethod(functionName , [parameterTypes.class]) , className.getMethods() , className.getDeclaredMethods() 来获取类的方法.
➡不能 new 出实例对象怎么办 ?
我们可以通过 className.newInstance() , className.getConstructor().newInstance() 来构造实例对象 .
➡那如何调用实例对象的方法呢 ?
通过 invoke() 方法来调用任何一个实例对象的方法 !
看看定义,它是Method对象调用的:
我们把上面的代码改成invoke获得的
/调用任意一个实例对象的方法
import java.lang.reflect.InvocationTargetException;
public class invoke {
public String prt(String name) {
return name;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String name = "mes9s0";
Class<?> cls = Class.forName("invoke");
invoke ivk = (invoke) cls.getDeclaredConstructor().newInstance();
// String str1 = ivk.prt(name);
// System.out.println(str1);
Object ret = cls.getMethod("prt", String.class).invoke(ivk, name);
System.out.println(ret);
}
}
Method.invoke(obj , args[])
如上文所说的 , 通过Java反射机制来获取类 , 获取类的方法 , 构造实力对象 , 最终调用实例方法。
注 : 官方文档中提到了一些比较有意思的东西 , 需要注意 。
如果要调用的方法是静态的 , 则忽略 obj 参数。这个点其实比较好理解 , 我们知道Java中调用静态方法是无需创建实例对象的 , 所以这里可以省略 obj 参数 。
**如果要调用的方法的形参个数为 " 0 " , 那么 args[] 数组的长度可以为 " 0 " 或者 " null " 。
这个点其实也没啥说的 , args[] 数组本就是要调用方法的参数 , 既然目标方法没有参数 , 这里自然也就不用写 .
看了这么久,记得关注哦~