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()
执行命令:
package com.Anchor;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class evilClass = Class.forName("java.lang.Runtime"); // 得到Runtime的class对象
Method method = evilClass.getMethod("exec", String.class); // 从中获取exec方法的method实例
Method runtimeMethod = evilClass.getMethod("getRuntime"); //从中获取getRuntime方法的method实例
Object evil = runtimeMethod.invoke(evilClass); //调用getRuntime方法从而获得一个Runtime类对象(这里invoke传入的参数也可以是null)
method.invoke(evil, "calc.exe"); // 调用exec方法,运行exec命令,打开一个计算机
}
}
看到这个过程,你可能会感觉到疑惑,为什么非要调用getRuntime()
方法获得Runtime
对象,而不考虑直接通过其中的evilClass
的newInstance()
来直接获得Runtime
对象?
确实,上面这么做是显得有些麻烦,但是实践证明,通过newInstance()
方法是不可行的,比如我们改成下面这行代码:
package com.Anchor;
public class Main {
public static void main(String[] args) throws Exception {
Class evilClass = Class.forName("java.lang.Runtime"); // 得到Runtime的class对象
evilClass.getMethod("exec", String.class).invoke(evilClass.newInstance(), "calc.exe");
//newInstance()实例化出一个Runtime对象,调用其中的exec方法
}
}
这样是变简洁了,但是当运行这段代码时会产生一个报错:
报错中有private
这个字眼,我们看看rt.jar
包下的java.lang.Runtime
类的源码,可以发现其中的无参构造函数是私有方法,很明显,newInstance是不能调用这样的私有无参构造函数的,这就是为什么运行后会报错。
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
类为例尝试调用其中的构造函数,一共是三个调用函数,分别是无参、只有一个参数以及有两个参数的构造函数,其中只有一个参数的构造函数是私有的。
package com.Anchor;
public class User {
private String name;
private Integer age;
public User() {
}
private User(String name) {
this.name = name;
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println(this.getName());
}
public Integer getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
}
测试的Main函数如下:
package com.Anchor;
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
Class userClass = Class.forName("com.Anchor.User");
User user0 = (User) userClass.newInstance(); //通过Class对象的newInstance方法调用无参构造函数
Constructor cs1 = userClass.getConstructor(); //通过getConstructor获得无参构造函数的Constructor对象
User user1 = (User) cs1.newInstance(); //第一种是newInstance
System.out.println("Class对象的newInstance方法:\t" + user0);
System.out.println("getConstructor[no parameters]:\t" + user1);
System.out.println("--------------------------------------------");
// 得到只有一个参数的构造函数的Constructor对象,使用getDeclaredConstructor方法,可以获得私有方法的Constructor对象
Constructor cs2 = userClass.getDeclaredConstructor(String.class);
cs2.setAccessible(true); //由于是private必须设置可访问
User user2 = (User) cs2.newInstance("anch0r");
System.out.println("getDeclaredConstructor[one parameter]:\t" + user2);
System.out.println("--------------------------------------------");
Constructor cs3 = userClass.getConstructor(String.class, Integer.class); //得到有两个参数的构造函数的Constructor对象
User user3 = (User) cs3.newInstance("anch0r2", 100);
System.out.println("getConstructor[two parameters]:\t" + user3);
}
}
最后运行结果为:
Class对象的newInstance方法: User [name=null, age=null]
getConstructor[no parameters]: User [name=null, age=null]
--------------------------------------------
getDeclaredConstructor[one parameter]: User [name=anch0r, age=null]
--------------------------------------------
getConstructor[two parameters]: User [name=anch0r2, age=100]
接下来以两个例子来说明Constructor类和相关的一些方法引入的意义。
设置setAccessible(true)绕过访问权限
你也许注意到上面这个例子中的一个方法:setAccessible(true)
。
在程序中,调用这一方法,可以取消java语言访问控制的检查能力。我们通过getDeclaredConstructor
获得一个私有构造函数的Method对象后,可以用setAccessible(true)
来实现绕过私有权限的访问控制。而从上文我们知道,就是因为java.lang.Runtime
类中的无参构造函数是私有的我们才没法通过Class对象的newInstance
方法来实例化对象,现在我们有了一个可以绕过private
的方式了,那么就来利用setAccessible(true)
改造之前的命令执行代码:
package com.Anchor;
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
Class evilClass = Class.forName("java.lang.Runtime");//老规矩,获取一个class对象
Constructor cs = evilClass.getDeclaredConstructor(); //利用getDeclaredConstructor获取私有的无参构造函数的Constructor对象
cs.setAccessible(true); // 取消java的访问控制的检查机制,绕过访问控制
evilClass.getMethod("exec", String.class).invoke(cs.newInstance(), "calc.exe");
// 通过无参构造函数的Constructor对象实例化,再借助getMethod获取exec的Method类,最后调用exec函数。
}
}
运行后成功弹出计算器:
调用java.lang.ProcessBuilder
不同类型的构造函数实现命令执行
在构造命令执行的payload时,除了Runtime
类,ProcessBuilder
类也是比较常用的,一般使用反射来获取其构造函数,然后调用其中的start()
方法来执行命令。
我们先通过查看源码研究一下这个类的的构造函数:
可以看到该类构造函数有两个重载方式。
第一个重载
第一个重载需要传入的参数类型是列表类型List
:
public ProcessBuilder(List<String> command) {
if (command == null)
throw new NullPointerException();
this.command = command;
}
我们在使用getConstructor
获得Constructor对象时,需要将传参类型修改为List.class
,所以我们能够立刻写出下面这个payload:
Class evilClass = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)evilClass.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();
这里用Arrays.asList
方法将字符串"calc.exe"
转为了列表类型再传入到Constructor类的newInstance方法中,从而执行构造函数public ProcessBuilder(List<String> command)
。
这里虽然执行成功,但是有个缺点,使用了强制类型转换(也就是第二行前面的(ProcessBuilder)
语法),然而在一般情况下,表达式上下文是没有这种语法的。我们仍然需要用java反射
改进这一步,避免使用强制类型转换,如下面这行代码:
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
了,就不需要再进行强制类型转换了。
运行成功弹出计算器:
第二个重载
第二个重载需要传入的参数类型比较特殊,是可变长的参数
public ProcessBuilder(String... command) {
this.command = new ArrayList<>(command.length);
for (String arg : command)
this.command.add(arg);
}b
java可变长参数:
java一般使用以使用
...
这样的语法来表示这个函数的参数个数是可变的,在编译时java会将可变长参数编译成一个数组类型,可以说下面这两种形式是等价的:public void hello(String[] names) {} public void hello(String...names) {}
对于hello这个函数,我们就可以传入一个字符串数组类型的数据,比如
String[] names = {"hello", "world"};
如果我们要实现对这第二个构造函数的重载,就要给它传入可变长参数。我们可以直接将可变长参数当成数组,也就是说将字符串数组类型的class对象:String[].class
传给 getConstructor作为参数类型 ,从而获取 ProcessBuilder
类这第二种重载的构造函数。
具体代码如下:
Class evilClass = Class.forName("java.lang.ProcessBuilder");
evilClass.getMethod("start").invoke(evilClass.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
运行后成功弹出计算器:
0x03 小结
这一篇主要是解决上一篇的一些疑惑,同时在引入了Constructor
类概念后加入了一些新的玩法。
参考文章: