前言
之前写了一篇Java反射的文章,现在这篇算是一点小小的补充。
正文
首先先补充一下forName方法,该方法的作用是要求JVM查找并加载指定的类,也就是说JVM「会执行该类的静态代码段」。
forName有两个函数重载:
-
Class<?> forName(String name)
-
Class<?> forName(String name, 「boolean」 initialize, ClassLoader loader)
默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就是 ClassLoader 。ClassLoader是一个“加载器”,在「Java安全第一篇 | 反射看这一篇就够了」,这篇文章中简单写了一点,可以去翻一翻。关于初始化,这里有一个小问题,看下面的代码:
public class ClassTest {
public ClassTest() {
System.out.printf("Initial %snt", this.getClass());
}
{
System.out.printf("Empty block initial %snt", this.getClass());
}
static {
System.out.printf("Static initial %snt", ClassTest.class);
}
}
上述的三个“初始化”⽅法有什么区别,调⽤顺序是什么,在安全上有什么价值?我们运行一下,结果如下
我们发现⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。其中, static {} 就是在“类初始化”的时候调⽤的,所以,我们的恶意代码就可以写在static {} 中,为啥呢?「因为有些恶意利用,整个过程中Java并没有执行Class文件中的任何方法,只是使用累加器加载和实例化了该类而已。所以我们需要让代码在实例化的时候就会被执行」。因此我们这类会采用静态块,也就是static {}。比如测试代码如下:
public class Test {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"open ."};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
该模拟攻击代码比较简单,只是在mac电脑上打开文件管理器。
上文说到,我们可以通过forName
加载任意类,获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。而class.newInstance()
的作用就是调用这个类的无参构造函数
但是,这里又有了新的问题,就是我们有时候在写漏洞利用方法的时候,会发现使用 newInstance
总是不成功,例如,最常见的情况就是 java.lang.Runtime
,这个类在我们构造命令执行Payload的时候很常见,看如下代码:
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
这时候,我们运行,发现报错了,如下:
我们发现这里的报错提示,说的是 Runtime 类的构造方法是私有的。
那如何解决呢?这里我们可以通过 Runtime.getRuntime() 来获取到 Runtime 对象。我们将上面的payload进行修改,代码如下:
Class<?> cls = Class.forName("java.lang.Runtime");
Method exec = cls.getMethod("exec", String.class);
Method getRuntime = cls.getMethod("getRuntime");
Object invokerun = getRuntime.invoke(cls);
exec.invoke(invokerun,"/System/Applications/Calculator.app/Contents/MacOS/Calculator");
发现代码执行成功,弹出计算器。
这个问题解决了,那我们在思考一下,「如果使用的类构造函数即使是私有的,那么我们能调用吗?」
这里是有办法的,我们可以使用getDeclaredConstructor()来获取一个类的私有构造方法,现在我们将上面的代码进行如下修改
Class<?> cls = Class.forName("java.lang.Runtime");
Constructor exec = cls.getDeclaredConstructor();
exec.setAccessible(true);
cls.getMethod("exec", String.class).invoke(exec.newInstance(),"/System/Applications/Calculator.app/Contents/MacOS/Calculator");
在上面的代码中,exec.setAccessible(true);
是必须要有的。我们在获取到一个私有方法后,必须用setAccessible
修改它的作用域,否则仍然不能调用。
再来一个问题,「如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?」
这里就需要用到一个新的反射方法 getConstructor
,getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。获取到构造函数后,我们使用 newInstance 来执行。比如,我们常用的另一种执行命令的方式ProcessBuilder
。
ProcessBuilder有两个构造函数
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command) 也就是可变参数
第一种的构造函数案例代码如下
Class<?> cls = Class.forName("java.lang.ProcessBuilder");
Method start = cls.getMethod("start");
start.invoke(cls.getConstructor(List.class).newInstance(Arrays.asList("/System/Applications/Calculator.app/Contents/MacOS/Calculator")));
执行成功,弹出计算器
第二个可变参数的构造函数,我们如何利用反射执行它呢?
其实,对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价的(也就不能重载):
public void hello(String[] names) {}
public void hello(String...names) {}
所以,我们我们可以如下构造代码:
Class<?> cls = Class.forName("java.lang.ProcessBuilder");
Method start = cls.getMethod("start");
//调用newInstance的时候,因为这个函数本身接收的就是可变参数,我们传给ProcessBuilder的也是可变参数,二者叠加为一个二维数组。
start.invoke(cls.getConstructor(String[].class).newInstance(new String[][]{{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}}));
执行成功,弹出计算器
参考
Java安全漫谈- 02.反射篇(2)
Java安全漫谈- 03.反射篇(3)
原文始发于微信公众号(小艾搞安全):Java安全|和反射相关的一些小补充
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论