Java安全-反射篇

最近在看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");

在这里插入图片描述

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末初mochu7

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值