freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java安全之反射
2022-11-08 23:16:00
所属地 广东

前言

关于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)

image.png
image.png

第⼀个就是我们最常⻅的获取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) {
        }
    }
}

image.png

进阶

上面可以看到,我们是先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");  //实例化类并执行方法

image.png

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

image.png

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

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

我们在做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对应上了。

image.png

深入

两个问题:

  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();
    }
}

image.png

这儿可能有点疑惑了,怎么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了。

image.png

如果我们要使用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);
    }
}

image.png

那为什么在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");
    }
}

image.png

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

# web安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录