从零开始的JAVA反序列化漏洞学习(⼀)
前⾔:
⼤概是决定复现JAVA的CVE,第⼀个拿cve-2016-4437试试,但是之前没接触过JAVA,在历经磨难安装好IDEA maven和依赖环境,跟着各位师傅的教程调试源代码发现⼤佬们的教程都是跟到 可以控制传⼊readObject()的反序列化就没了,再细查便是什么CC4,CC3.1之类看上去很深奥的东西。深感基础不⾜,从头开始学JAVA的各种机制,如有错误疏漏不⾜欢迎在评论中指出。
利⽤Java反射执⾏代码
Java的反序列化是离不开Java的反射机制的,反射机制离不开Object类和Class类,关于反射已经有很多⼤佬写过详解,这⾥只⽤两个例⼦来说明如何利⽤Java反射来执⾏代码
第⼀个,经典弹计算器
public class Hello {
public static void main(String[] args)throws Exception {
Hello helloTest =new Hello();
}
public void oneTest()throws Exception{
Object runtime=Class.forName("java.lang.Runtime")
.getMethod("getRuntime",new Class[]{})
.invoke(null);
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(runtime,"");
}
}
⾸先是⼏个关键的⽅法:
Class.forName()⽅法:
Class.forName是⼀个静态⽅法,可以⽤来加载类。该⽅法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和Class.forName(String className),参数String className为所需的类名,⽅法返回⼀个与给定的字符串名称相关联类或接⼝的Class对象。getMethod()⽅法与invoke⽅法:⼀般同时使⽤
Method getMethod(String parameterTypes),参数String name表⽰mothod的名称,Class parameterTypes表⽰method的参数类型的列表(参数顺序需按声明method时的参数列表排列),符合method名称和参数的method对象
零基础学java编程Object invoke(Object args),调⽤包装在当前Method对象中的⽅法,参数Object obj表⽰实例化后的对象,Object args表⽰⽅法调⽤的参数
再来分析例⼦中的第⼀部分代码:
Object runtime=Class.forName("java.lang.Runtime")
.getMethod("getRuntime",new Class[]{})
.invoke(null);
相当于调⽤了 Runtime类中的getRuntime⽅法并赋值给runtime,我们查看⼀下getRuntime⽅法的代码:
public class Runtime {
private static Runtime currentRuntime =new Runtime();
public static Runtime getRuntime(){
return currentRuntime;
}
}
因为是static⽅法,不依附于任何对象,所以在⽤.invoke()调⽤时可以没有 参数Object obj,返回的⼀个new Runtime对象。
再看第⼆部分
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(runtime,"");
相当于调⽤了 (“”),因为exec()⽅法不是static,所以invoke()⽅法需要⼀个Object obj参数,也就是第⼀部分返回的runtime。(PS. 个⼈觉得Java的反射机制有点模糊了 数据和程序 的边界,可以通过⽤户输⼊的字符串来调⽤没有预期的函数执⾏恶意命令。)
Transformer常见的恶意代码包装类
利⽤Transformer,我们可以构造⼀条可序列化的恶意的代码链
import Transformer;
import ChainedTransformer;
import ConstantTransformer;
import InvokerTransformer;
import ByteArrayOutputStream;
import IOException;
import ObjectOutputStream;
public class Hello {
public static void main(String[] args)throws Exception {
TransformerTest test =new TransformerTest();
test.runTest();
}
}
class TransformerTest {
public void runTest(){
Transformer[] transformers =new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"",}),
};
//ChainedTransformer transformerChain也可以是Transformer transformerChain
ChainedTransformer transformerChain =new ChainedTransformer(transformers);
//引爆点
try{
//serialize test
ByteArrayOutputStream out =new ByteArrayOutputStream();
ObjectOutputStream objOut =new ObjectOutputStream(out);
objOut.writeObject(transformerChain);
}catch(IOException e){
e.printStackTrace();
}
}
}
先是⼀个transformer数组⾥⾯添加了ConstantTransformer与InvokerTransformer,之后⽤该数组为参
数构造⼀个ChainedTransformer transformerChain对象(这⾥也可以是Transformer transformerChain对象,ChainedTransformer类implements了Transformer),调⽤它
的transform()⽅法
下断点调试下,可以看到ChainedTransformer的构造⽅法和transform()⽅法的代码为
public ChainedTransformer(Transformer[] transformers){
this.iTransformers = transformers;
}
public Object transform(Object object){
for(int i =0; i <this.iTransformers.length;++i){
object =this.iTransformers[i].transform(object);
}
return object;
}
也就是transform()会依次调⽤transformer数组中transformer的transform()⽅法
继续调试,第⼀个是ConstantTransformer的transform()
public ConstantTransformer(Object constantToReturn){
this.iConstant = constantToReturn;
}
//iConstant = class java.lang.Runtime
public Object transform(Object input){
return this.iConstant;
}
返回了class java.lang.Runtime,再进⼊InvokerTransformer的transform()
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args){
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input){
//input:class java.lang.Runtime
if(input ==null){
return null;
}else{
try{
/
/iMethodName:getMethod
//iParamTypes:Class[]{String.class,Class[].class}
//iArgs:Object[]{"getRuntime", new Class[0]}
Class cls = Class();
//调⽤getMethod来获取getMethod
Method method = Method(this.iMethodName,this.iParamTypes);
//调⽤invoke来执⾏getMethod来获取getRuntime⽅法
return method.invoke(input,this.iArgs);
}catch(NoSuchMethodException var5){
throw new FunctorException("InvokerTransformer: The method '"+this.iMethodName +"' on '"+ Class()+"' does not exist");
}catch(IllegalAccessException var6){
throw new FunctorException("InvokerTransformer: The method '"+this.iMethodName +"' on '"+ Class()+"' cannot be accessed");
}catch(InvocationTargetException var7){
throw new FunctorException("InvokerTransformer: The method '"+this.iMethodName +"' on '"+ Class()+"' threw an exception", var7);
}
}
}
⽤上⾯的反射基础知识可以知道,调⽤了Runtime的getMethod()⽅法(⽤⼀个与Runtime相关联的class对象来调⽤的),来查
getRuntime()⽅法,返回⼀个public static java.lang.Runtime java.Runtime(),(挺绕的,⽤getMethod来获取getMethod,再⽤invoke来执⾏getMethod来获取getRuntime)
也就是InvokerTransformer类的transform()⽅法将会以InvokerTransformer(⽅法名称, 参数类型,⽅法参数)的形式调⽤⽅法,⽽⽅法所在的类或对象则是由链条上⼀步来返回的。
同理下⾯将执⾏invoke()来获取Runtime对象,然后执⾏exec(“”),整体相当于
public static void main(String[] args)throws IOException {
}
我们得到了⼀条可以执⾏恶意代码可以被序列化的ChainedTransformer transformerChain,但是我们如何能够让它能够在正常代码中执⾏它
的ansform(null) ⽅法来执⾏呢?
重写后的readObject如何被执⾏
(这是我个⼈不整明⽩不舒服的疑问,只粗略写了调试过程,不感兴趣的话可以略过)
⾸先我们要了解为什么在重写readObject()⽅法后,在反序列化时会调⽤重写后的⽅法⽽不是原⽅法,
来看⼀个例⼦
import*;
public class Test {
public static void main(String[] args)throws Exception {
SerializeTest serializeTest =new SerializeTest();
try{
ObjectOutputStream oos =new ObjectOutputStream(new FileOutputStream("temp"));
oos.writeObject(serializeTest);
oos.close();
}catch(IOException e){
e.printStackTrace();
}
try{
ObjectInputStream ois =new ObjectInputStream(new FileInputStream("temp"));
Object p = adObject();
// p.show();
}catch(IOException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
}
}
class SerializeTest implements Serializable {
String str ="hello";
public void show(){
System.out.println(str);
}
private void readObject(ObjectInputStream ois)throws IOException, ClassNotFoundException{
System.out.println("readObject run");
}
}
在Object p = adObject();可以看见,它被反序列化为了Object对象⽽不是SerializeTest的,为什么它可以到重写后的readObject呢?下断点然后可以看到调⽤栈
⾸先通过下图的调⽤来读取序列化串来获取类名
接下来是如何调⽤重写后的readObject(),先是调⽤ObjectInputStream的readObject()⽅法
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论