前言
之前学习反序列化漏洞,全是参考网上的乱七八糟的文章,然后我觉得反序列化漏洞是非常难的一部分内容。今天我看了phith0n师傅写的文章[1],我觉得写的非常棒。本文章的内容几乎均来自phith0n师傅,部分加了一些自己理解的部分。文章里面还有一些感想,这里给大家分享一下。
学习方式的探索⭐️⭐️⭐️
有时候想要学习⼀个东⻄,网上搜索⼀下,发现有教程,于是跟着做⼀遍。这样⼀来,你会发现网上大部分Java反序列化”教程”、“⼊门”通常上来先了解Java反序列化是什么,然后很快开始讲
CommonsCollections,就好像刚知道C语言语法的同学⽴马继续学习Linux内核,我是⼗分不建议这样做的,除⾮你有非常强的理解能⼒。学习需要聪明⼀点,并独⽴思考问题。我很少参照别⼈的文章来学习,这样你学的东⻄是⼆手的,有时候连⼆手都不是,⽂章原作者也可能是参考另一篇⽂章写的。
我的建议是从⽂档和源码开始学,实在有压⼒可以参考⼀些⻛评较好的书籍或技术博客。
当然,有时候你并不知道怎样是对的怎样是错的。⽐如你作为⼀个Java反序列化漏洞的初学者,你并不知道应该先学习
URLDNS还是CommonsCollections,也许你连URLDNS的名字都没听过,而你看到⽹上⼤部分文章都是介绍CommonsCollections的,⾃然也就去学习这个了。所以有些弯路确实是避免不了的,不过如果你在学习的道路上更加富有探索精神,看到
ysoserial这种项⽬时多问问⾃己“其他的Gadget是做什么的”,对于未知的事物原理充满好奇喜欢翻翻源码,应该是可以少⾛⼀些弯路的。
简化版CC1链Demo
这里先给出phith0n师傅独创的简化版CommonsCollections1利用链[2],如下:
1 | package com.x2n.deserialization; |
运行上述的代码发现可以成功弹出计算器。

下面介绍一下整个过程中涉及的几个接口和类。
TransformedMap
TransformedMap用于对Java标准数据结构Map做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map。
1 | Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer); |
其中,keyTransformer是处理新元素的key的回调,valueTransformer是处理新元素的value的回调。我们这里所说的“回调”,并不是传统意义上的一个回调函数,而是一个实现了Transformer接口的类。
Transformer
Transformer是一个接口,它只有一个待实现的方法:
1 | public interface Transformer { |
TransformedMap在转换Map的新元素时,就会调用transform方法,这个过程就类似在调用一个“回调函数”,这个回调的参数是原始对象(Object对象)。
ConstantTransformer
ConstantTransformer是实现了Transformer接口的一个类,它的过程就是在构造函数的时候传入一个对象,并在transform方法将这个对象再返回。
1 | public ConstantTransformer(Object constantToReturn) { |
所以它的作用其实就是包装任意一个对象,在执行回调时返回这个对象,进而方便后续操作。
InvokerTransformer
InvokerTransformer是实现了Transformer接口的一个类,这个类可以用来执行任意方法,这也是反序列化能执行任意代码的关键。
在实例化这个InvokerTransformer时,需要传入三个参数:
-
第一个参数就是待执行的方法名
-
第二个参数是这个函数的参数列表的参数类型
-
第三个参数是传给这个函数的参数列表
1 | public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { |
后面的回调transform方法,就是执行了input对象的iMethodName方法。
1 | public Object transform(Object input) { |
ChainedTransformer
ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串在一起。通俗来说就是,前一个回调返回的结果,作为后一个回调的参数传入,示意图如下:

它的代码也比较简单,如下:
1 | public ChainedTransformer(Transformer[] transformers) { |
理解简化版Demo
了解了前面的几个Transformer的意义以后,再回头看一看我们的demo的代码,这就比较好理解了。
先看前两段代码:
1 | Transformer[] transformers = new Transformer[]{ |
创建了一个ChainedTransformer对象,根据前面说的,ChainedTransformer的作用是将内部的多个Transformer串在一起。ChainedTransformer需要的参数是Transformer[],我们创建了Transformer[],其中包含了两个Transformer:
- 第一个是
ConstantTransformer,直接返回当前环境的Runtime对象; - 第二个是
InvokerTransformer,执行Runtime对象的exec方法,参数是/System/Applications/Calculator.app/Contents/MacOS/Calculator。
这个transformerChain只是一系列回调,我们需要用其来包装innerMap,使用前面说到的TransformedMap.decorate:
1 | Map innerMap = new HashMap(); |
最后,怎么触发回调呢?就是向Map中放入一个新的元素:
1 | outerMap.put("xxx","tttt"); |
上面的代码执行demo,它只是一个用来在本地测试的类。在实际反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成一个序列化流。
用TransformedMap编写真正的POC[3]
前面我们说到了CommonsCollections1的简化版的demo,但离一个真正可利用的POC还有很大的距离,现在我们着手对其进行修改。
AnnotationInvocationHandler
我们前面说过,触发这个漏洞的核心,在于我们需要向Map中加入一个新的元素。在demo中,我们可以手工执行outerMap.put("xxx","tttt");来触发漏洞,但在实际反序列化时,我们需要找到一个类,它在反序列化的readObjection逻辑里有类似的写入操作。
这个类就是sun.reflect.annotation.AnnotationInvocationHandler,我们查看它的readObject方法(这是8u71以前的代码,8u71以后做了一些修改,这个后面会说):
1 | private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { |
核心逻辑就是Map.Entry<String, Object> memberValue : memberValues.entrySet()和 memberValue.setValue(...)。
memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的Transform,进而执行我们为其精心设计的任意代码。
所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的HashMap设置进来:
1 | Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
这里因为sun.reflect.annotation.AnnotationInvocationHandler是在JDK内部的类,不能直接使用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化了。
AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是参数就是前面构造的Map。
这里大家可以思考一下:什么是
Annotation类?为什么我这里需要使用Retention.class?
为什么需要使用反射?
上一小节我们构造了一个AnnotationInvocationHandler对象,它就是我们反序列化利用链的起点了。我
们通过如下代码将这个对象生成序列化流:
1 | ByteArrayOutputStream barr = new ByteArrayOutputStream(); |
我将这几段代码拼接到demo代码的后面,组成一个完整的POC。
我们试着运行这个POC,看看能否生成序列化数据流:

运行代码发现报错了,在writeObject的时候出现异常了: java.io.NotSerializableException: java.lang.Runtime。
原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实现了 java.io.Serializable接口。而我们最早传给ConstantTransformer的是Runtime.getRuntime(),Runtime类是没有实现java.io.Serializable接口的,所以不允许被序列化。
那么,如何避免这个错误呢?我们可以变通一下,看前面p牛的《Java安全漫谈 - 反射篇》[4]的同学应该知道,我们可以通过反射来获取到当前上下文中的Runtime对象,而不需要直接使用这个类。
1 | Method f = Runtime.class.getMethod("getRuntime"); |
转换成Transformer的写法就是如下:
1 | Transformer[] transformers = new Transformer[]{ |
其实和demo最大的区别就是将Runtime.getRuntime()换成了Runtime.class,前者是一个java.lang.Runtime对象,后者是一个java.lang.Class对象。Class类有实现Serializable接口,所以可以被序列化。
为什么仍然无法触发漏洞?
修改Transformer数组后再次运行,发现这次没有报错异常,而且输出了序列化后的数据流,但是反序列化时仍然没弹出计算器,这是为什么?

这个实际上和AnnotationInvocationHandler类的逻辑有关,我们可以动态调试就会发现,在AnnotationInvocationHandler:readObject的逻辑中,有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue,否则不会进入也就不会触发漏洞。
断点调试代码:

那么如何让这个var7不为null呢?这一块我就不详细分析了,还会涉及到Java注释相关的技术。直接给出两个条件:
-
sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X -
被
TransformedMap.decorate修饰的Map中必须有一个键名为X的元素。
所以,这样就解释了为什么我前面用到Retention.class,因为Retention有一个方法,名为value,所以,为了再满足第二个条件,我需要给Map中放入一个key是value的元素[5]:
1 | innerMap.put("value", "xxxx") |
成功弹出计算器。

为什么Java高版本无法利用?
已经成功弹出计算器了,但拿着这串序列化流,跑到服务器上进行反序列化时就会发现,又无法成功执行命令了。这又是为什么呢?
前文说了,我们是在Java 8u71以前的版本上进行测试的,我使用的Java版本为8u65,在8u71以后大概是2015年12月的时候,Java官方修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大。改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。
总结
我们将给出的demo扩展成为了一个真实可利用的POC,完整的代码如下:
1 | package com.x2n.deserialization; |
但是这个Payload有一定局限性,在Java 8u71以后的版本中,由于sun.reflect.annotation.AnnotationInvocationHandler发生了变化导致不再可用,原因前文也说了。我们查看ysoserial的代码,发现它没有用到我demo中的TransformedMap,而是改用了LazyMap。有的同学包括我,之前以为这就是在解决CommonCollections1这个利用链在高版本Java中不可用的问题,其实不然,即使使用LazyMap仍然无法在高版本的Java中使用这条利用链,主要原因还是出在sun.reflect.annotation.AnnotationInvocationHandler这个类的修改上,不过本篇文章先不讲了。
下一篇文章,再给大家分析如何破局。