java 序列化与反序列化
简单例子
首先我们来看下简单的例子:
|
|
根据结果我们可以看到,序列化了2个,一个String
一个int
.
先看string:0x ACED
作为序列化的标识符0x 0005
作为序列化版本号,这些都是常量,定义在Constants
中。0x 74
表示String
类型0x 000D
string长度 为13字节0x 61626364 E698AFE6 8891E595 8A
后面对应于内容的UTF-8
编码值。
而int序列化0x 73
表示TC_OBJECT
0x 74
表示TC_CLASSDESC
(class描述符)0x 11
表示描述符长度,对应java.lang.Integer
0x 12E2A0A4 F7818738
为SerialVersionUID
,如果不指定,会随机生成8字节uid0x 02
为标记号. 该值声明该对象支持序列化0x 0001
表示域个数0x 49
表示域类型 对应I
表示Integer0x 0005
域名字长度0x 76616C75 65
对应域名字描述value0x 78
对应于TC_ENDBLOCKDATA
表示对象块结束
…0x 78
对应于TC_ENDBLOCKDATA
表示对象块结束0x 70
对应于TC_NULL
表示没有超类了0x 0000 0005
对应值5
其中的Constants
字段值可以参考java.io.ObjectStreamConstants
复杂例子
之前的例子使用的都是java常见的内部类,如果复杂一点呢?如果是我们自己定义的class,只需要实现Serializable
接口即可。Serializable
接口中没有任何方法,可以理解为一个标记,即表明这个类可以序列化。例如:
|
|
序列化后的结果如下:
我们可以看到,我们指定了uid之后,其值为12345678
.
关于字段的详细含义,可以看这篇文章Object Serialization Stream Protocol 极其推荐!极其推荐!极其推荐! 这里引用一些比较关键的点:
|
|
其中的Rules of the Grammar
可以仔细看下。
serialVersionUID
随着项目的更新,不同的serialVersionUID
被认为是不同的class
版本,在反序列化中可能会报错。如果手动设置UID
,可以遵守以下规则:
|
|
序列化引用
在之前,提到每个序列化后的stream都会分配一个handler值,从0x 007E 0000
开始,当同一类的不同对象再序列化时,其会首先查看原始是否存在,如果存在则直接引用,不会再去重新生成一个。
自定义序列化
writeObject & readObject
在序列化和反序列化过程中,我们可以实现:
|
|
来完全控制序列化和反序列化的过程
ObjectOutputStream先通过反射在要被序列化的对象的类中查找有无自定义的writeObject方法,若有,则会优先调用自定义的writeObject方法。因为查找反射方法时使用的是getPrivateMethod,所以自定以的writeObject方法的作用域要被设置为private。通过自定义writeObject和readObject方法可以完全控制对象的序列化与反序列化。
当然,我们需要对应write和read的顺序,不然会报错,如图所示,可以看到我们在所Test对象之后附带了一个字符串,且字符串和原始对象均被正常读入处理。类似这种操作一般用于session中,添加session处理时间而不将时间戳信息写入session对象当中。
writeReplace & readResolve
这两个方法分别在writeObject
和readObject
的前、后进行处理,与PHP反序列化中的__sleep
& __wakeup
类似,都是用于序列化与反序列化前后对对象进行处理的函数
implement Externalizable接口
实现Externalizable
则序列化反序列化流程完全由我们自己决定:
|
|
java反序列化漏洞
我们再看下面的例子,由于java反序列化中没有指定反序列化对象的类型,任意对象均可被反序列化,那么在反序列化过程中,如果对象本身有一些函数会自动调用,则会造成任意代码执行。
由于反序列化存在于一些公共库,例如Apache Commons Collections,所以危害影响非常广。
漏洞基本原理
如图,如果我们在readObject
代码之后还去执行了一些其他东西,这就是漏洞的基本原理,但可能会想,谁会这样写?但实际上,一些有潜在危险的地方通过精心构造,是可以实现的。
所以一般漏洞产生的条件实现了Serializable
接口以及 重写了readObject
方法
Apache Commons Collections
问题出现在commons-collections
这个apache组件当中,其中有个InvokerTransformer
存在问题。我们先看一下这个类的内容:
|
|
我们看下这个类,在transform
方法中,我们看到了和基本漏洞相似的写法:
|
|
是不是很熟悉,如果input
、iMethodName
、iParamTypes
、iArgs
都是由我们控制传入的话,是不是就可以像之前一样弹出计算器,很巧的是,在InvokerTransformer
的构造方法中,可以传入其中的三个参数,transform
的调用也可以传入1个,完美达成条件。我们来测试一下:
我们可以看到,是可以的,于是我们尝试去找谁调用了transform
这个方法。在org.apache.commons.collections.map ==> TransformedMap
中找到3处:
|
|
我们来仔细看下这个类。TransformedMap
类实现了Map
类的扩展,可以将两个实现了Transformer
对象进行转换。首先是调用decorate
方法初始化,decorate
会调用构造方法进行实例化,对外暴露2个方法,put
与putall
分别用于传入一个或多个。如果原始不为null,则会调用this.[]Transformer.transform(object)
.
再来看下其他的两个类:
ConstantTransformer
的构造方法返回传入的值到iConstant
ChainedTransformer
这个类的构造方法传入多个transformer
且其transform
可以触发每个元素的transform
方法
我们来看这个transform-chain
,我们构造了一个Transformer[]
,其内容如下:
|
|
现在我们构造了一个chain
,但是缺少一个触发chain.transform
的途径从而往下传递触发。而我们在之前看到TransformedMap
的checkSetValue
可以触发transform
方法,但checkSetValue
无法直接调用,我们进一步看:
发现MapEntry
可以直接调用setValue
方法触发checkSetValue
所以整个的POC
如下:
即构造了个TransformedMap
然后对其第一个元素进行赋值。调用了setValue
方法,触发了checkSetValue
从而调用了ChainedTransformer
的transform
方法,从而对内部的4个Transform
分别调用transform
,从而完成了命令执行,但我们对细节还是一知半解,我们通过debug来一步步看下他执行的流程.
- 首先是
transformers
的定义,为4个Transformer
的数组,然后新建hashmap
对象,并写入一个kv值 - 之后调用
TransformedMap.decorate
生成一个outerMap
对象,outerMap.map
是之前的hashmap
,outerMap.keyTransformer
为null
,keyTransformer.valueTransformer
是之前的chainedTransform
elEntry
指向Map的第一个kv对- 重点来了
之后我们看到,调用setValue
会调用parent.checkSetValue
,会返回valueTransformer.transform
,而valueTransformer
是之前的chainedTransform
从而对每一个Transform
对象都进行transform
。
注意!ChainedTransformer
的transform
是链式的(这一点我到这里才注意到,也才想通整个过程),即前一个的结果会作为参数传到下一个的transform
方法中object = iTransformers[i].transform(object);
- 第一个
ConstantTransformer
的transform
会返回java.lang.Runtime
类 - 第二个
InvokerTransformer
之后返回Runtime.getRuntime()
对象 - 第三个之后返回
Runtime()
对象 - 第三个则返回
Runtime.getRuntime().getMethod("exec", String.class).invoke(,"open.....")
最终执行命令
以上就是核心的原理,那我我们如何利用呢?参考之前的的漏洞原理,我们需要找到一个接收Map类对象反序列化,且在readObject
方法时调用Map对象的Entry的setValue方法.
所利用的类是AnnotationInvocationHandler
,这个类无法直接访问(。。。一直以为是jdk的问题删掉了…哭晕),可以直接去github上看源码,这里提供一个镜像链接。我们把它download下来看。
我们可以看到,当反序列化AnnotationInvocationHandler
类时,其memberValues
为我们传入的Map对象,在readObject
会调用Map.Entry
的setValue
函数,从而触发漏洞:
完整payload
如图,但这里本地无法成功,具体原因往下看。到这里我发现运行失败,而且无法进入AnnotationInvocationHandler
里面单步调试,一开始我以为是oracle jdk
与openjdk
区别,下载了很多版本在idea中可以用Class.forName()
调用但是就是无法import,也无法进入调试。由于不知道为什么错误也看不见具体逻辑,一度陷入僵局,而且网站的分析基本到给出payload就结束了。
于是采用一个骚操作,由于我们在镜像链接上可以看到,/sun/reflect/annotation/
中有11个类,而我们在idea import sun.reflect.annotation
中只能看到9个,唯独少了AnnotationInvocationHandler
和AnnotationTypeMismatchExceptionProxy
,且后者在前者中使用了,于是我打算把这两个类下载下来,作为我们本地类,这样就可以单步调试了。
注意,ExceptionProxy
这个类只能在内部使用,我们把它注释掉就好,没有影响。一步步调试,一切如我们预期,在反序列化中,调用了AnnotationInvocationHandler
类的readObject
方法,一直到for循环中,如下图,在Class<?> memberType = memberTypes.get(name);
中取得的值是value
而我们之前HashMap的key是name
导致不等没有进入setValue
的过程。
这下就明白了,所以我们只需要更改innerMap.put("value", "hello");
,保证key是value
即可成功执行命令:
当然代码可以改回sun.reflect.AnnotationInvocationHandler
我们再看下,为什么必须是value
,annotationtype
是注解类型,annotationType = AnnotationType.getInstance(type);
的作用是返回@Target
注解的内容,其memberTypes
的key值为value
,所以传入的内容也需要为value.
那么可以合理推测,我们将原始payload改成其他类型的注解,应该由于其memberTypes
应该是表明其注解类型,应该也是value->...
的Map形式,应该也可以成功执行,尝试后发现,符合我们预期,这样整个原理就算非常透彻的理解了。
漏洞修复
Apache Commons Collections在3.2.2版本中做了一定的安全处理,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。涉及的类包括CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。
我们可以看到,官方对于这种反序列化问题,往往采取的是黑名单,这也是之后为什么漏洞被频繁各种绕过,由于Apache Commons Collections
当时存在于各种web server或容器中,导致当时各种组件被攻击。