Java安全之反射篇(2)

学习学习

Posted by lll-yz on November 18, 2021

反射篇(2)

在正常情况下,除了系统类,如果我们想拿到一个类,需要先import才能使用。而使用forName就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。

另外,我们经常在一些源码里看到,类名的部分包含$符号,比如fastjson在checkAutoType时候就会将$替换成.$的作用是查找内部类。

Java的普通类c1中支持编写内部类c2,而在编译的时候,会生成两个文件:c1.classc1$c2.class,我们可以把他们看作两个无关的类,通过Class.forName("c1$c2")即可加载这个内部类。

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

Class.newInstance()的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候在写漏洞利用方法的时候,会发现使用newInstance总是不成功,这时候原因可能是:

​ 1.你使用的类没有无参构造函数。

​ 2.你使用的类构造函数是私有的。

最最最最常见的情况就是java.lang.Runtime,这个类在我们构造命令执行payload的时候很常见,但我们不能直接这样来执行命令:

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

你会得到一个这样的错误:

I7SI8H.png

原因是Runtime类的构造方法是私有的。

有同学就比较好奇,为什么会有类的构造方法是私有的,难道他不想让用户使用这个类吗?这其实涉及到很常见的设计模式:”单例模式”。(有时候工厂模式也会写成类似)

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

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

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

我们用这个例子来看一下:

//privateC.java

public class privateC {
    private static privateC instance = new privateC();
    
    public static privateC getInstance(){
        return instance;
    }
    
    private privateC(){
        System.out.println("this is a private method");
    }
}

I7CWV0.png

这个例子,其实相当于在PrivateC内部实例化了,而外部仅仅是获取这个对象。

同理,Runtime也是单例模式,我们只能通过Runtime.getRuntime()来获取到Runtime对象。

I7PPsA.png

我们将上述payload进行修改即可正常执行命令了:

public class P {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //PrivateC pc = PrivateC.getInstance();
        Class clazz = Class.forName("java.lang.Runtime");
        clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
    }
}

I7FE8S.png

这里用到了getMethodinvoke方法。

getMethod的作用是通过反射获取一个类的某个特定的公有方法。而学过Java的同学应该清楚,Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用getMethod的时候,我们需要传给他你需要获取的函数的参数类型列表。

比如这里的Runtime.exec方法有6个重载:

I7AVmj.png

我们使用最简单的,也就是第一个,它只有一个参数,类型是String,所以我们使用getMethod("exec", String.class)来获取Runtime.exec方法。

invoke的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象。
  • 如果这个方法是一个静态方法,那么第一个参数是类。

这也比较好理解了,我们正常执行方法是[1].method([2], [3], [4...]),其实在反射里就是method.invoke([1], [2], [3], [4]...)

所以我们将上述命令执行的payload分解一下就是:

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

这个就比较好看懂了。

另一种方法

前文说过java.lang.Runtime类无参构造方法是private权限无法直接调用。

setAccessible通过反射修改方法的访问权限,强制可以访问

Constructor constructor = cls.getDeclaredConstructor();是获取类构造器方法的方法

public static void Method2() {
    try {
        //获取对象
        Class clazz = Class.forName("java.lang.Runtime");
        //获取构造方法
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);

        //实例化对象
        Object ob = constructor.newInstance();
        Method mt = clazz.getMethod("exec", String.class);

        mt.invoke(ob, "calc");

    } catch (NoSuchMethodException | ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

I7qgc6.png

反射之ProcessBuilder

通过看JDK文档,ProcessBuilder类有两个构造方法。

I7LDr8.png

如果分别使用反射构造方法获取实例化语句如下:

  • Class.forName("java.lang.ProcessBuilder").getDeclaredConstructor(List.class).newInstance(Arrays.asList("calc"))
  • Class.forName("java.lang.ProcessBuilder").getDeclaredConstructor(String.class).newInstance("calc")

方法一

方法一中,newInstance()实例化时需要执行命令参数一并进行。

public static void Method1() {
    try {
        //获取对象
        Class clazz = Class.forName("java.lang.ProcessBuilder");
        //实例化对象
        Object ob = clazz.getDeclaredConstructor(List.class).newInstance(Arrays.asList("calc"));
        //执行命令
        clazz.getMethod("start").invoke(ob, null);

    } catch (ClassNotFoundException | NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

IHuikQ.png

方法二

方法二使用第二种构造方法,可变长参数(String...表示参数长度不确定)。那么对于反射来说,如果要获取目标函数里包含的可变长参数,可直接视为数组。因此只需要将String[].class传给构造方法即可,但在调用newInstance()实例化方法时,不能直接传一个一维数组String[] {"calc"},而是应该传入一个二维数组String[][] calc。因为newInstance()函数本身接收的是一个可变长参数,我们传给ProcessBuilder的也是一个可变长参数,二者叠加由一维数组变成了二维数组。

public static void Method2() {
    try {
        Class clazz = Class.forName("java.lang.ProcessBuilder");

        String[][] clazz2 = new String[][] calc;
        Object ob = clazz.getConstructor(String[].class).newInstance(clazz2);

        clazz.getMethod("start").invoke(ob, null);

    } catch (ClassNotFoundException | NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
}

IHultJ.png

知识点补充

Java命令常见的两个类java.lang.Runtimejava.lang.ProcessBuilder,通常情况下使用java.lang.Runtime类。

Windows下执行命令的几种方式

1.Windows下调用程序:

Process proc = Runtime.getRuntime().exec("exefile");

2.Windows下调用系统命令:

String[] cmd = {"cmd", "/C", "copy exe1 exe2"};
Process proc = Runtime.getRuntime().exec(cmd);

3.Windows下调用系统命令并弹出命令行窗口:

String[] cmd = {"cmd", "/C", "start copy exe1 exe2"};
Process proc = Runtime.getRuntime().exec(cmd);

Linux下执行命令的几种方式

1.Linux下调用程序:

Process proc = Runtime.getRuntime().exec("./exefile");

2.Linux下调用系统命令:

String[] cmd = {"/bin/sh", "-c", "ln -s exe1 exe2"};
Process proc = Runtime.getRuntime().exec(cmd);

3.Linux下调用系统命令并弹出命令行窗口:

String[] cmd = {"/bin/sh", "-c", "xterm -e ln -s exe1 exe2"};
Process proc = Runtime.getRuntime().exec(cmd);