0x00 前言
发现关于java反射还有很多内容需要补充,就接着java反射机制进一步记录有关反射的内容。
0x01 关于newInstance()方法
通过上次的java反射学习,我们知道,通过调用获得来的class对象的newInstance()方法可以实例化一个对象,通过该实例化的对象,我们就可以成功调用对应类中的成员方法。
但调用newInstance()方法是有条件的,前提是这个类有无参构造方法,并且构造方法是公有的,因为newInstance()作用就是调用类的公有的无参构造方法,进而实例化出一个对象。如果一个类没有公有的无参构造函数,那么调用newInstance()就会不成功。
拿我这篇文章中的java反射机制中的 “0x03 java反射价值”中的例子来说明。当时我是利用反射机制先获取java.lang.Runtime(这是我们命令执行构造payload最常用的类)的class对象,然后是获得exec()和getRuntime()这两个成员方法,利用invoke方法先调用getRuntime()方法从而获得一个Runtime对象,然后才调用Runtime对象的exec()执行命令:
1 | package com.Anchor; |
看到这个过程,你可能会感觉到疑惑,为什么非要调用getRuntime()方法获得Runtime对象,而不考虑直接通过其中的evilClass的newInstance()来直接获得Runtime对象?
确实,上面这么做是显得有些麻烦,但是实践证明,通过newInstance()方法是不可行的,比如我们改成下面这行代码:
1 | package com.Anchor; |
这样是变简洁了,但是当运行这段代码时会产生一个报错:

报错中有private这个字眼,我们看看rt.jar包下的java.lang.Runtime类的源码,可以发现其中的无参构造函数是私有方法,很明显,newInstance是不能调用这样的私有无参构造函数的,这就是为什么运行后会报错。
1 | private Runtime() {} |

不太了解java的人这时也许又会产生疑问,为什么会有私有的无参构造方法?真是不想让用户使用这个类才设计的吗?
其实并不是,这涉及到常见的设计模式:“单例模式”。
单例模式,说通俗点,就是想要保证一个类仅有一个实例,并且提供一个访问它的全局访问点,主要解决的问题就是程序中对一个类频繁的创造和销毁,导致效率下降。单例模式思路就是将构造函数设置成私有,然后设置一个静态方法来获取该构造方法,这样就不会频繁调用构造函数建立对象了,只需要一次借助对应的静态方法来调用构造函数创建单例。而我之前成功调用exec函数的方法就是用的这个静态方法来实现的。
Runtime类很明显就是一个单例模式设计的类,它其中有一个静态函数getRuntime()就是用来获取Runtime对象的。(在上一张图中我们可以看到getRuntime()函数代码)所以,就有这样的思路,利用getMethod方法拿到getRuntime的Method对象,通过调用它从而获得Runtime对象。
0x02 Constructor类
看到上面的例子后,疑问又产生了,如果有这样一个类,它没有无参构造方法(或者是无参构造方法私有),也没有单例模式中的静态方法,那么怎么通过反射机制来实现实例化操作呢?
解决上面这个问题,这里得引入反射中一个大类,Constructor。在上一篇java反射中我只提及了Class类和Method类,当时以为Constructor类并不是很重要,就把它给遗漏了,没想到它在安全中还是有作用的。
Constructor类存在于反射包(java.lang.reflect)中,反映的是Class 对象所表示的类的构造方法。在反射中,一个类的Constructor对象是可以通过Class类中的成员方法获得的,相关的方法如下:
| 方法返回值 | 方法名称 | 方法说明 |
|---|---|---|
| static Class<?> | forName(String className) | 返回与带有给定字符串名的类或接口相关联的 Class 对象。 |
| Constructor | getConstructor(Class<?>… parameterTypes) | 返回指定参数类型、具有public访问权限的构造函数对象 |
| Constructor<?>[] | getConstructors() | 返回所有具有public访问权限的构造函数的Constructor对象数组 |
| Constructor | getDeclaredConstructor(Class<?>… parameterTypes) | 返回指定参数类型、所有声明的(包括private)构造函数对象 |
| Constructor<?>[] | getDeclaredConstructors() | 返回所有声明的(包括private)构造函数对象 |
| T | newInstance() | 调用无参构造器创建此 Class 对象所表示的类的一个新实例。 |
常用的是其中的getConstructor方法,和getMethod方法类似getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。通过getConstructor这些方法,我们不仅可以解决上面没有无参构造函数的问题,还可以任意调用自己需要的那个构造函数。
我们先以上次的User类为例尝试调用其中的构造函数,一共是三个调用函数,分别是无参、只有一个参数以及有两个参数的构造函数,其中只有一个参数的构造函数是私有的。
1 | package com.Anchor; |
测试的Main函数如下:
1 | package com.Anchor; |
最后运行结果为:
1 | Class对象的newInstance方法: User [name=null, age=null] |
接下来以两个例子来说明Constructor类和相关的一些方法引入的意义。
设置setAccessible(true)绕过访问权限
你也许注意到上面这个例子中的一个方法:setAccessible(true)。
在程序中,调用这一方法,可以取消java语言访问控制的检查能力。我们通过getDeclaredConstructor获得一个私有构造函数的Method对象后,可以用setAccessible(true)来实现绕过私有权限的访问控制。而从上文我们知道,就是因为java.lang.Runtime类中的无参构造函数是私有的我们才没法通过Class对象的newInstance方法来实例化对象,现在我们有了一个可以绕过private的方式了,那么就来利用setAccessible(true)改造之前的命令执行代码:
1 | package com.Anchor; |
运行后成功弹出计算器:

调用java.lang.ProcessBuilder不同类型的构造函数实现命令执行
在构造命令执行的payload时,除了Runtime类,ProcessBuilder类也是比较常用的,一般使用反射来获取其构造函数,然后调用其中的start() 方法来执行命令。
我们先通过查看源码研究一下这个类的的构造函数:

可以看到该类构造函数有两个重载方式。
第一个重载
第一个重载需要传入的参数类型是列表类型List:
1 | public ProcessBuilder(List<String> command) { |
我们在使用getConstructor获得Constructor对象时,需要将传参类型修改为List.class,所以我们能够立刻写出下面这个payload:
1 | Class evilClass = Class.forName("java.lang.ProcessBuilder"); |
这里用Arrays.asList方法将字符串"calc.exe"转为了列表类型再传入到Constructor类的newInstance方法中,从而执行构造函数public ProcessBuilder(List<String> command)。
这里虽然执行成功,但是有个缺点,使用了强制类型转换(也就是第二行前面的(ProcessBuilder)语法),然而在一般情况下,表达式上下文是没有这种语法的。我们仍然需要用java反射改进这一步,避免使用强制类型转换,如下面这行代码:
1 | Class evilClass = Class.forName("java.lang.ProcessBuilder"); evilClass.getMethod("start").invoke(evilClass.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))); |
这里通过getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object了,就不需要再进行强制类型转换了。
运行成功弹出计算器:

第二个重载
第二个重载需要传入的参数类型比较特殊,是可变长的参数
1 | public ProcessBuilder(String... command) { |
java可变长参数:
java一般使用以使用
...这样的语法来表示这个函数的参数个数是可变的,在编译时java会将可变长参数编译成一个数组类型,可以说下面这两种形式是等价的:
1
2
3 public void hello(String[] names) {}
public void hello(String...names) {}对于hello这个函数,我们就可以传入一个字符串数组类型的数据,比如
String[] names = {"hello", "world"};
如果我们要实现对这第二个构造函数的重载,就要给它传入可变长参数。我们可以直接将可变长参数当成数组,也就是说将字符串数组类型的class对象:String[].class 传给 getConstructor作为参数类型 ,从而获取 ProcessBuilder 类这第二种重载的构造函数。
具体代码如下:
1 | Class evilClass = Class.forName("java.lang.ProcessBuilder"); |
运行后成功弹出计算器:

0x03 小结
这一篇主要是解决上一篇的一些疑惑,同时在引入了Constructor类概念后加入了一些新的玩法。
参考文章: