总感觉活在一种小矛盾中,急着长大去看看未知的世界,又想永恒活在现在拥有这份热烈的时刻。继续学习Java反序列化,有种不知道路将通往何方的感觉= =
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类
简化的demo
pom.xml添加
1 | <dependency> |
然后新建一个类
1 | import org.apache.commons.collections.Transformer; |
把C:\\WINDOWS\\system32\\calc.exe
修改为计算器所在路径,我的是windows的路径,运行之后可以看到弹出了计算器。
几个接口和类
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⽅法,这个过程就类似在调⽤⼀个”回调 函数“,这个回调的参数是原始对象。
ConstantTransformer
ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个 对象,并在transform⽅法将这个对象再返回:
1 | public ConstantTransformer(Object constantToReturn) { |
他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。
InvokerTransformer
InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序列化能执⾏任意代码的关键。
在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表:
后⾯的回调transform⽅法,
1 | Class cls = input.getClass(); // 获取输入对象的类 |
就是执⾏了input对象的iMethodName⽅法。
ChainedTransformer
ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串 在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊。
1 | public ChainedTransformer(Transformer[] transformers) { |
TransformedMap链
理解demo
1 | Transformer[] transformers = new Transformer[]{ |
创建了⼀个ChainedTransformer,其中包含两个Transformer:第⼀个是ConstantTransformer, 直接返回当前环境的Runtime对象;第⼆个是InvokerTransformer,执⾏Runtime对象的exec⽅法,参数是C:\\WINDOWS\\system32\\calc.exe
当然,这个transformerChain只是⼀系列回调,我们需要⽤其来包装innerMap,使⽤的前⾯说到的 TransformedMap.decorate
:
1 | Map innerMap = new HashMap(); |
最后,怎么触发回调呢?就是向Map中放⼊⼀个新的元素:
1 | outerMap.put("test", "xxxx"); |
生成一个可利用的序列化对象
AnnotationInvocationHandler
想要触发这个漏洞需要往Map中放入一个新的元素,但是在实际反序列化时,我们需要找到一个 类,它在反序列化的readObject逻辑里有类似的写入操作。
这个类是 sun.reflect.annotation.AnnotationInvocationHandler
(这是8u71以前的代码,8u71以后做了一些修改)
1 | private void readObject(java.io.ObjectInputStream s) |
确保在反序列化过程中,对象(可能是一个注解)的成员的类型仍然与预期的类型兼容。如果检测到类型不匹配,将创建一个AnnotationTypeMismatchExceptionProxy
来处理这种情况。
memberValues就是反序列化后得到的Map,也是经过了TransformedMap修饰的对象,这里遍历了它 的所有元素,并依次设置值。在调用setValue设置值的时候就会触发TransformedMap里注册的 Transform,进而执行我们为其精心设计的任意代码。 所以,我们构造POC的时候,就需要创建一个AnnotationInvocationHandler对象,并将前面构造的 HashMap设置进来:
1 | Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
修改后的demo
1 | import org.apache.commons.collections.Transformer; |
在writeObject的时候出现异常了
原因是,Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable
接口。而我们最早传给ConstantTransformer
的是 Runtime.getRuntime()
,Runtime
类是没有实现 java.io.Serializable
接口的,所以不允许被序列化。
转换成Transformer的写法就是如下:
1 | Transformer[] transformers = new Transformer[] { |
修改后的demo,和之前最大的区别就是将 Runtime.getRuntime()
换成了 Runtime.class
,前者是一个 java.lang.Runtime 对象,后者是一个 java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化。
1 | import org.apache.commons.collections.Transformer; |
成功输出了反序列化流,但是没有弹计算器。
这个实际上和AnnotationInvocationHandler
类的逻辑有关,我们可以动态调试就会发现,在 AnnotationInvocationHandler:readObject
的逻辑中,有一个if语句对var7进行判断,只有在其不是null的时候才会进入里面执行setValue
,否则不会进入也就不会触发漏洞:
我第一次测试的jdk版本太高了java version “1.8.0_401”,这个漏洞已经被修复了
jdk8u/jdk8u/jdk: f8a528d0379d (openjdk.org)
改动后,不再直接 使用反序列化得到的Map对象,而是新建了一个LinkedHashMap
对象,遍历了 streamVals
中的每一个成员。对于每个成员,首先获取其名称,并根据名称从 memberTypes
中获取对应的类型信息。然后,获取成员的值,并进行类型检查。如果成员的值不是其声明的类型,将会创建一个 AnnotationTypeMismatchExceptionProxy
实例,并将其设置为该成员的值。最后,将成员的名称和值添加到 mv
中。 所以,后续对Map的操作都是基于这个新的LinkedHashMap
对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了。
1 | import org.apache.commons.collections.Transformer; |
但是这个Payload有一定局限性,在Java 8u71以后的版本中,由于 sun.reflect.annotation.AnnotationInvocationHandler
发生了变化导致不再可用
换成JDK8U65就可以弹计算器了
LazyMap链
LazyMap也来自于Common-Collections库,并继承AbstractMapDecorator
类。
LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执行transform,而LazyMap是在其get方法中执行的factory.transform
。
LazyMap仍然无 法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。
当在get找不到值的时候,它会调用factory.transform
方法去获取一个值
相比于TransformedMap
的利用方法,LazyMap后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler
的readObject方法中并没有直接调用到 Map的get方法。 所以ysoserial找到了另一条路,AnnotationInvocationHandler
类的invoke方法有调用到get:
Java对象代理
如果想劫持一个对象内部的方法调用,我们需要用到 java.reflect.Proxy :
1 | Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); |
Proxy.newProxyInstance
的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。
写这样一个类ExampleInvocationHandler
1 | import java.lang.reflect.InvocationHandler; |
ExampleInvocationHandler
类实现了invoke方法,作用是在监控到调用的方法名是get的时候,返回一 个特殊字符串 Hacked Object 。
在外部调用这个ExampleInvocationHandler:
1 | //App.java |
运行后发现调用了get方法获取一个键值对后输出了Hacked Object,可以得知成功执行了get方法。
回看 sun.reflect.annotation.AnnotationInvocationHandler
,会发现实际上这个类实际就 是一个InvocationHandler
,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler#invoke
方法中,进而触发我们的 LazyMap#get
。
在方法调用时拦截并执行自定义行为,可以看出动态代理功能强大。
使用LazyMap构造利用链
因为我们入口点是 sun.reflect.annotation.AnnotationInvocationHandler#readObject
,所以我们还需要再用 AnnotationInvocationHandler对这个proxyMap进行包裹:
1 | handler = (InvocationHandler) construct.newInstance(Retention.class, |
poc
1 | import org.apache.commons.collections.Transformer; |
成功弹出计算器
参考: