0x00 前言
本篇包含以下元素:
Java的命令执行
Java的反射机制
Java的反序列化
利用Java反序列化与反射调用进行命令执行的demo
可配和代码食用:
https://github.com/chenlvtang/JavaUnserialization/tree/chenlvtang/Java-Reflection_RCE-Example
https://github.com/chenlvtang/JavaUnserialization/tree/chenlvtang/Java-Serialization-Example
0x01 java的命令执行
下面是一个Java执行系统命令的例子:
package com.edu;
public class ExecDemo {
public static void main(String[] args) throws Exception{
// Runtime.getRuntime().exec("calc.exe");
Runtime run = Runtime.getRuntime();
run.exec("calc.exe");
}
}
Windows下运行这段代码,如果成功弹出计算器,恭喜你,成功在Java中实现了命令执行。(当然,我们除了Runtime还有ProcessImpl可以执行命令,但是这对于入门者来说,可以日后专门再来讨论与对比)
0x01 java中的反射机制
下面是利用反射机制来调用上文中的命令执行的例子。
package com.edu;
import java.lang.reflect.Method;
public class ReflectExecDemo {
public static void main(String[] args) throws Exception{
//equal: Object run = Runtime.getRuntime();
Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
//得到方法exec, 其参数为String类型
Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
//equal: myruntime.exec("calc.exe");
exec.invoke(myRuntime, "calc.exe");
}
}
刚入门的你,可能会感觉到十分懵逼,但Java的一个好处就是看名字就能猜出个大概作用,所以下面就大概说一下以上所涉及方法的作用和小细节。
Class.forName
很明显就是根据名称获取到某个类,分为Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。
第一个参数为类名;第二个参数为是否初始化,默认为True,在类初始化时,会执行类中的静态代码;第三个参数指定类加载器(负责将其他类加载到JVM中的东东),默认为当前类。使用方法二时,缺省的参数当然就是会设置成默认值啦。
[这个forName的小知识点非常重要,请记牢,哈哈]
getMethod
获取方法,有两个参数,第二个参数可以缺省: getMethod(String name, Class<?>...parameterTypes)
,第一个参数代表的是方法的名字,第二个参数代表的是该方法参数的类型(三个点表示可变参数列表,接收0或多个参数),如果该方法没有参数,则getMethod的第二个参数可以缺省或者为传入null。
invoke
顾名思义,即“调用”,也有两个参数:invoke(Object obj, Object ... args)
。要与getMethod结合使用,一般是先用前者获取到一个Method类,再用invoke调用,举个上文的例子应该就便于理解invoke方法的参数意义了:
Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
//equal: myruntime.exec("calc.exe");
exec.invoke(myRuntime, "calc.exe");
可以看到,invoke的第一个参数即为调用该方法的(如上例的exec方法)类,第二个参数为传入该方法的参数值,同样也是一个可变参数列表。
0x02 java的反序列化
Java中的反序列化和PHP不同的是,在Java中一个类要能够被反序列化,则其必须实现了java.io.Serializable接口(该类必须实现java.io.Serializable 接口。该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明以transient注明。),并且它的序列化格式也非常得不同。下面是一个简单的序列化和反序列化的demo:
1.要被反序列化的类Chenlvtang.class
package com.edu;
import java.io.Serializable;
public class Chenlvtang implements Serializable{
private String name;
private String level;
public Chenlvtang(){
this.level = "noob";
this.name = "chenlvtang";
}
}
2.进行序列化和反序列化操作的SerializeDemo.class
package com.edu;
import java.io.*;
public class SerializDemo{
public static void main(String[] args) throws Exception{
Chenlvtang chenlvtang = new Chenlvtang();
//开始序列化
FileOutputStream file = new FileOutputStream("chenlvtang.bin");
ObjectOutputStream ser = new ObjectOutputStream(file);
ser.writeObject(chenlvtang);
ser.close();
//开始反序列化
FileInputStream file1 = new FileInputStream("chenlvtang.bin");
ObjectInputStream unser = new ObjectInputStream(file1);
Object result = unser.readObject();
unser.close();
System.out.println(result);
}
}
3.序列化文件
其中ACED
是魔术头,0005
是流协议版本,其他的就暂且不深入了解。
0x03 反序列化漏洞demo
可以看到上文中进行反序列操作的时候用了到了readObject
方法,并且是拿序列化内容进行调用,如果我们传入一个类,其中重写了readObject方法,那么在反序列化的时候是否会调用呢?下面重写chenlvtang.class, 并在里面重写readObject方法。
package com.edu;
import java.io.Serializable;
public class Chenlvtang implements Serializable{
private String name;
private String level;
public Chenlvtang(){
this.level = "noob";
this.name = "chenlvtang";
}
private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();//调用默认的方法
System.out.println("hihi");
}
}
运行结果:
可以看到,成功的调用了。那我们现在是不是可以想一下把其中的hihi换成上文中危险的命令执行呢?下面我们再次重写readObject方法
private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();//调用默认的方法
Runtime.getRuntime().exec("calc.exe");
}
运行结果:
成功执行了命令!Java中反序列化漏洞指的就是,在反序列化时,传入重写了readObject并能触发危险操作的构造链(由多个类依赖组成,连锁反应后,具有危害性),造成危害。