Java安全之反射

一颗小胡椒2022-11-22 10:36:15

前言

关于Java安全,反序列化漏洞一直是一个热门话题,而反序列化漏洞⼜可以从反射开始说起。通过反射,对象可以通过反射获取他的类,类可以通过反射拿到所有⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上动态特性。

入门

有以下三种方法获取⼀个“类”,也就是java.lang.Class对象:

//1、通过对象调用 getClass() 方法来获取,通常应用在:比如你传过来一个 Object类型的对象,而我不知道你具体是什么类,用这种方法
Person p1 = new Person();
Class c1 = p1.getClass();
//2、直接通过类名.class 的方式得到,该方法最为安全可靠,程序性能更高这说明任何一个类都有一个隐含的静态成员变量 class
Class c2 = Person.class;
//3、通过 Class 对象的 forName() 静态方法来获取,用的最多,但可能抛ClassNotFoundException异常
Class c3 = Class.forName("com.ys.reflex.Person");

在安全研究里边,我们常见的各种payload几乎都是使用Class.forName方法来获取类

forName有两个函数重载:

  • Class forName(String name)
  • Class forName(String name, boolean initialize, ClassLoader loader)

第⼀个就是我们最常⻅的获取class的⽅式,通过类名来获取,其实可以理解为第⼆种⽅式的⼀个封装。

Class.forName(className)
// 等于
Class.forName(className, true, currentLoader)

ClassLoader 是什么呢?它就是⼀个“加载器”,告诉Java虚拟机如何加载这个类。这是另外的一个知识点。Java默认的ClassLoader就是根据类名来加载类,这个类名是类完整路径,如java.lang.Runtime

关于第二个参数initialize,我们可以将这个“初始化”理解为类的初始化,我们执行下面这行代码,JVM会做什么呢?

Person p = new Person("zhangsan",20);
  1. 因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。 2
  2. 执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
  3. 在堆内存中开辟空间,分配内存地址。
  4. 在堆内存中建立对象的特有属性。并进行默认初始化
  5. 对属性进行显示初始化。
  6. 对对象进行构造代码块初始化。
  7. 对对象进行对应的构造函数初始化。
  8. 将内存地址付给栈内存中的p变量

可以分为三类初始化:static代码块的初始化,其他初始化,构造函数初始化,其中构造函数初始化会涉及到super类的构造函数初始化,这里不细讲了。

需要注意的是使用class.forName()会对类的静态代码块进行初始化(不会初始化类的构造函数),

那么我们就可以编写⼀个恶意类,将恶意代码放置在static {}中,从⽽执⾏

import java.lang.Runtime;
import java.lang.Process;
public class command {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String commands = "calc.exe";
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
        }
    }
}

进阶

上面可以看到,我们是先import java.lang.Runtime,然后再使用;正常情况下,除了jdk内置类,如果我们想拿到一个类,需要先import才能使用。而使用forName就不需要,这样我们可以加载任意类进行攻击。

获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法

在反射类库中,用于实例化对象的方法有两个。

  • Class.newInstance():这个方法只需要提供一个class实例就可以实例化对象,如这个方法不支持任何入参,底层是依赖无参数的构造器Constructor进行实例化的。
  • Constructor.newInstance(Object...init args):这个方法需要提供java.lang.reflect.Constructor<T>实例和一个可变参数数组进行对象的实例化,这个方法除了可以传入构造参数之外,还有一个好处就是可以通过抑制修饰符访问权限检查,也就是私有的构造器也可以用于实例化对象。

我们在构造payload的时候,实例化不成功的原因有以下两个:

  1. 使用的类没有无参构造函数,因为newInstance()底层是依赖无参数的构造器实现的,没有无参构造函数,怎么可能实例化成功
  2. 使用的类构造函数是私有的,我们可以使用Constructor.newInstance(Object...initargs)来实例化

我们来分析下面这个payload:

Class cls = Class.forName("java.lang.Runtime");
Method execMethod = cls.getMethod("exec", String.class);
Method getRuntimeMethod = cls.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(cls);
execMethod.invoke(runtime, "calc.exe");

首先获取java.lang.Runtime类,接下来获取这个类的exec方法,接下来又获取getRuntime方法,下面两部可能看着有点疑惑了,先来看invoke是干嘛的

invoke 的作用是执行方法,它的第一个参数是:
  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类
  • static修饰的静态方法会随着类的定义而被分配和装载入内存中
  • 普通方法只有在对象创建时,在对象的内存中才有这个方法的代码段
这也就是为什么invoke方法参数不同的原因所在。

常规的方法执行是: Person per = Person.eat(1,2,3)

反射则是 eat.invoke(Persno,1,2,3)

有以下代码段:

Class cls = Class.forName("java.lang.Runtime");  //获取类
Method execMethod =cls.getMethod("exec",String.class); //获取方法
execMethod.invoke(cls.newInstance(), "calc.exe");  //实例化类并执行方法

按理说,应该弹计算器啊,为什么报错了呢?看报错提示,不能获取一个被“privite”修饰符修饰的类。

原来构造方法是私有的,那肯定是实例化不了的,为什么构造方法要搞成私有的,不想让人用?

这其中就涉及到一个常见的设计模式--->工厂模式,具体是什么,就不说了。举例

我们在做Web开发的时候,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连 接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来 获取:

public class DBC {
     private static DBC instance = new DBC();
     public static DBC getInstance() {
       return instance;
    }
     private DBC() {
       // 建立连接的代码... 
    }
}

只有只有类初始化的时候会执行一次构造函数,后面只能通过getInstance获取这个对象,避免建立多个数据库连接。

Runtime类就是单例模式,我们只能通过Runtime.getRuntime()来获取到Runtime对 象。我们将上述Payload进行修改即可正常执行命令了

public class Main {
    public static void main(String[] args) throws Exception {
        Class cls = Class.forName("java.lang.Runtime");  //获取类
        Method execMethod =cls.getMethod("exec",String.class); //获取exec方法
        Method getRuntimeMethod = cls.getMethod("getRuntime");   //获取getRuntime方法
        Object runtime = getRuntimeMethod.invoke(cls);  //执行getRuntime方法来获取Runtime类
        execMethod.invoke(runtime, "calc.exe");  //执行方法exec方法
    }
}

这样就和一开始的payload对应上了。

深入

两个问题:

  1. 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
  2. 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

第一个问题:我们上节在开始就说过用于实例化对象的方法有两个,我们只说了第一种,而第二种方法就可以解决这节第一个问题。

首先我们需要通过反射方法getConstructor()获得获得一个Constructor对象,

和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数,听着有点绕,看以下例子:

Class cl=Class.forName(Person);
//获取到Person(String name,int age) 构造函数
Constructor con=cl.getConstructor(String.class,int.class);
 //通过构造器对象 newInstance 方法对对象进行初始化,使用有参数构造函数
Object obj=con.newInstance("神奇的我",12);

我们常用的另一种命令执行的方法ProcessBuilder.start(),我们使用反射来获取其构造函数,然后调用start()来执行命令:

public class Main {
    public static void main(String[] args) throws Exception {
        Class cls = Class.forName("java.lang.ProcessBuilder");  //获取类
        Constructor con = cls.getConstructor(List.class);          // 获取构造器
        ProcessBuilder process = (ProcessBuilder) con.newInstance(Arrays.asList("calc.exe")); //通过构造器实例化对象
        process.start();
    }
}

这儿可能有点疑惑了,怎么start就直接弹计算器了?

ProcessBuilder有两个构造函数:

public ProcessBuilder(List<String> command) 
 public ProcessBuilder(String... command)

我们用的是第一个形式的,所以在getConstructor的时候传入的是List.class

所以接下来需要用数组的形式传入calc.exec参数

这里需要注意,我们通过Constructor实例化对象返回的是一个Object对象,我这里是u强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们不能直接执行命令,仍需利用反射来完成这一步,payload如下:

public class Main {
    public static void main(String[] args) throws Exception {
        Class cls = Class.forName("java.lang.ProcessBuilder");  //获取类
        Constructor con = cls.getConstructor(List.class);          // 获取构造器
        Method startMethod = cls.getMethod("start");   //获取start方法
        Object probulid = con.newInstance(Arrays.asList("calc.exe")); //通过构造器实例化对象
        startMethod.invoke(probulid);
    }
}

通过getMethod("start")获取到start方法,然后invoke执行,invoke的参数就是ProcessBuilder Object了。

如果我们要使用public ProcessBuilder(String... command)这个构造函数,具体的paayload该如何构造呢?

这又涉及到Java里的可变长参数(avarargs)了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用...这样的语法来表示“这个函数的参数个数是可变的”。 对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,下面这两种写法在底层是等价的:

public void hello(String[] names) {}
public void hello(String...names) {}

也由此,如果我们有一个数组,想传给say函数,只需直接传即可

String[] names = {"hello", "world"};
hello(names);

对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。 所以,我们将字符串数组的类String[].class传给getConstructor,获取ProcessBuilder的第二种构造函数:

Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class)

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

public class Main {
    public static void main(String[] args) throws Exception {
        Class cls = Class.forName("java.lang.ProcessBuilder");
        Constructor con = cls.getConstructor(String[].class);
        Method startMethod = cls.getMethod("start");
        Object probuild =con.newInstance(new String[][]{{"calc.exe"}});
        startMethod.invoke(probuild);
    }
}

那为什么在newInstance时传入的是一个二维数组呢?

这是因为在newInstance函数本身接收的是一个可变长参数,我们传给ProcessBuilder也是一个可变长参数,二者叠加为一个二维数组。这儿比较绕,得转过弯来

如果一个方法或构造方法是私有方法,我们是否能执行它呢?

答案是可以。通过getDeclared系列方法

  • getMethod系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
  • getDeclaredMethod系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括
  • 私有的方法,但不能获取父类继承的方法

getDeclaredMethod的具体用法和getMethod类似,getDeclaredConstructor的具体体用法和getConstructor类似

前面我们说过Runtime这个类的构造函数是私有的,我们需要用Runtime.getRuntime()来 获取对象。其实现在我们也可以直接用getDeclaredConstructor来获取这个私有的构造方法来实例化对象,进而执行命令:

public class Main {
    public static void main(String[] args) throws Exception {
        Class cls = Class.forName("java.lang.Runtime");
        Constructor con = cls.getDeclaredConstructor();
        con.setAccessible(true);
        cls.getMethod("exec", String.class).invoke(con.newInstance(), "calc.exe");
    }
}

可见,这里使用了一个方法setAccessible,这个是必须的。我们在获取到一个私有方法后,必须用setAccessible修改它的作用域,否则仍然不能调用。

初始化构造函数
本作品采用《CC 协议》,转载必须注明作者和本文链接
Java安全之反射
2022-11-22 10:36:15
前言关于Java安全,反序列化漏洞一直是一个热门话题,而反序列化漏洞?可以从反射开始说起。通过反射,对象可以通过反射获取他的类,类可以通过反射拿到所有?附加上动态特性。入门有以下三种方法获取?个“类”,也就是java.lang.Class对象://1、通过对象调用 getClass() 方法来获取,通常应用在:比如你传过来一个 Object类型的对象,而我不知道你具体是什么类,用这种方法
应用场景wire作为依赖注入的代码生成工具,非常适合复杂对象的创建。而在大型项目中,拥有一个合适的依赖注入的框架将使得项目的开发与维护十分便捷。中最核心的两个概念就是Injector和Provider。这些方法接收所需依赖作为参数,创建组件并将其返回Injector?代码生成命令行在指定目录下执行?
使用java和redis实现一个简单的热搜功能,具备以下功能: 搜索栏展示当前登陆的个人用户的搜索历史记录,删除个人历史记录 用户在搜索栏输入某字符,则将该字符记录下来 以zset格式存储的redis中,记录该字符被搜索的个数以及当前的时间戳 (用了DFA算法,感兴趣的自己百度学习吧) 每当用户查询了已在redis存在了的字符时,则直接累加个数, 用来获取平台上最热查询的十条数据。(可以自己写接
动态函数PHP中支持一个功能叫 variable function ,变量函数的意思。//最终是system;当一个变量后边带括号,那他就被视作一个函数。编译器会解析出变量的值,然后会去找当前是否存在名为“system()”的函数并执行它。这里就不给实例了,很多免杀案例中都用到了这个特性。也是被疯狂查杀的特征。回调函数回调函数,简单来说就是一个函数不是由我直接调用,而是通过另一个函数去调用它。
Zimbra官方通报了一个RCE漏洞CVE-2022-27925,这是一个 ZIP压缩包解析导致路径穿越Getshell。
Kernel从0开始
2021-12-10 13:42:20
网上一大堆教编译内核的,但很多教程看得特别迷糊。第一次编译内核时,没设置好参数,直接把虚拟机编译炸开了。所以就想着能不能先做个一键获取内核源码和相关vmlinux以及bzImage的脚本,先试试题,后期再深入探究编译内核,加入debug符号,所以就有了这个一键脚本。
Activity漏洞挖掘详解
2021-10-18 16:22:12
2Activity漏洞初步介绍1.Activity基本介绍在学习Activity的漏洞挖掘之前,我们先对Activity的基本运行原理有一个初步的认识。
在2020年夏季,我们发现了一个未知的多模块C ++工具集,该工具集可用于可追溯到2018年的针对性强的工业间谍攻击。最初,我们对该恶意软件感兴趣的原因是其稀有性,该活动的明显针对性以及存在在代码,基础架构或TTP...
VMPWN的入门系列-2
2023-08-03 09:29:42
解释器是一种计算机程序,用于解释和执行源代码。与编译器不同,解释器不会将源代码转换为机器语言,而是直接执行源代码。即,这个程序接收一定的解释器语言,然后按照一定的规则对其进行解析,完成相应的功能,从本质上来看依然是一个虚拟机。总的来说,如果输入字符数小于0x10,string类的大概成员应该如下struct?
之前看chenx6大佬的博客学习了一下编写基础的LLVM Pass,但是那个有很明显的问题是,作者为了处理Function内部重复引用的多次解密的问题,特判了引用次数,如果存在多处对global string的引用是无法进行混淆的。但是实际的编程中很难不会引用多处字符串,所以那个只能混淆简单代码。之后学习了一下pluto-obfuscator项目,里面有一份GlobalEncryption.cpp,借此机会学习一下,顺便写一份New PassManager版本的。runOnModule首先获取Module的LLVMContext,获取所有的全局变量,添加到GVs中。
一颗小胡椒
暂无描述