Java ysoserial学习之CommonsCollections1(二)
超长预警
0x01 前言
Apache Commons Collections是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。
Apache Commons Collections 中提供了一个Transformer的类,这个接口的功能就是将一个对象转换为另外一个对象,CC链都依赖于此。
本文CommonsCollections1利用链的限制条件:
JDK版本:jdk1.8以前(8u71之后已修复不可利用)、Commons-Collections 3.1-3.2.1
实验环境:
JDK 1.7.0_80、Commons-Collections 3.2.1
0x02 P牛简化的利用链Demo分析
1 | package org.vulhub.Ser; |
我们来逐行分析这个简单的Demo
2.1 Transformer 接口
Transformer是一个接口,它只有一个待实现的transform方法:
1 | package org.apache.commons.collections; |
疑问1: Java 中接口是不能实例化的,但是为啥可以这样写Transformer[] transformers = new Transformer[] ,P牛有在代码审计知识星球中回答:这里是实例化了一个数组,并不是实例化Transformer接口(大学学的知识都快忘完了)。
所以第一行是创建了一个数组,数组的类型是 Transformer, 该数组中有两个元素:
new ConstantTransformer(Runtime.getRuntime())
new InvokerTransformer(“exec”, new Class[]{String.class}, new Object[] {“/System/Applications/Calculator.app/Contents/MacOS/Calculator”})
2.2 ConstantTransformer 类
ConstantTransformer 是实现了Transformer接口的一个类,并且也实现了Serializable接口,说明是可序列化的
它的过程就是在构造函数的时候传入一个对象,并在transform方法将这个对象再返回。
2.3 InvokerTransformer 类
InvokerTransformer 也是实现了Transformer接口的一个类,有两个构造方法,也是实现了Serializable接口
这里P牛使用的是第二个构造方法,传入了方法名为执行命令的exec方法,参数类型为数组new Class[]{String.class},执行的是打开计算器 new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
这里也实现了transform方法,
1 | public Object transform(Object input) { |
这里通过 input.getClass(); 获取对象,然后通过 cls.getMethod(this.iMethodName, this.iParamTypes); 传入方法名、方法的的参数类型来调用对象的方法,之后通过 method.invoke(input, this.iArgs); 来执行该方法。这也就是前面初探Java反序列化漏洞(二)中说过的Java反射。
从这里我们能看出,InvokerTransformer这个类调用transform可以执行任意方法,这点非常关键,是反序列化能执行任意代码的关键。尝试使用InvokeTransformer来执行命令。
我们在这里可以尝试进行序列化与反序列化的利用
1 | package con.yhy; |
虽然成功执行,但是单单利用InvokeTransformer类来进行反序列化漏洞的利用在实战中是条件极其苛刻的。
1.目标得恰巧利用InvokerTransformer将反序列化后的数据进行转型。
2.目标得在转型后,正好调用了transform,并且参数是攻击者可以控制的。
这样是行不通的,所以这一步只能是利用链中的一条线路,需要继续寻找可以用的点。
2.4 ChainedTransformer 类
这个同样是实现Transformer接口的一个类,也是实现了Serializable接口
只有个接收数组,数组中元素类型为Transformer的一个有参构造方法。这里实现的transform方法的作用是:遍历执行传入数组元素的transform方法,同时将上个元素的返回对象作为s下个元素transform方法中的参数。
2.5 TransformedMap 类
TransformedMap 这个类是用来对 Map 进行某些变换用的,例如当我们修改Map中的某个值时,就会触发我们预先定义好的某些操作来对Map进行处理。
1 | Map transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer); |
通过decorate函数就可以将一个普通的 Map 转换为一个TransformedMap。第二个参数和第三个参数分别对应当key改变和value改变时需要做的操作,都是Transformer类型,实现transform(Object input)方法即可进行实际的变换操作,按照如上代码生成transformedMap后,如果修改了其中的任意key或value,都会调用对应的transform方法去进行一些变换操作。
动手实验一下,帮助理解
1 | package con.yhy; |
这里我通过实现了一个当 map 中的 key 变动时,输出一句话的功能。
0x03 梳理P牛简化后的利用链
经过上面的逐行分析,现在我们可以复述一下Demo的运行逻辑:通过创建了一个ChainedTransformer,向其中传入一个Transformer类型的数组,该数组中有两个元素:第一个是ConstantTransformer;第二个是InvokerTransformer。
因为TransformedMap.decorate() 会在 map 中 key 或者 value 改变时,去执行时调用对应的transformer方法,而ChainedTransformer对象的 transform方法是遍历执行传入参数的transform方法。
所以当 map 中 key 或者 value 改变时,第一个元素ConstantTransformer会调用transform方法返回当前环境的Runtime对象,然后将Runtime对象作为数组第二个元素的transform方法的参数,而InvokerTransformer的transform方法是通过反射执行传入对象的方法,也就是下面的一个过程。
1 | package con.yhy; |
最后通过 outerMap.put("test", "xxxx"); 时,触发TransformedMap.decorate()方法,执行上面一系列过程打开计算器,即执行命令。
上述过程并没有反序列化相关的链,只是一个本地运行的Demo,方便学习CommonCollections利用链的执行流程。而在实际的反序列化漏洞中,我们需要将上面最终生成的outerMap对象变成一个序列化流。
0x04 通过反序列化执行触发漏洞
上面的 Demo 需要我们手工执行改变 map 中的 key 的值,也就是手动执行 outerMap.put("test", "xxxx");才可以触发命令执行,但在实际的应用中,不可能让我们手动执行,所以需要找到一个实现了Serializable的类,它在反序列化的readObject逻辑里有类似的写入操作。
这里配合我们执行代码的类就是sun.reflect.annotation.AnnotationInvocationHandler,该类是java运行库中的一个类,实现了Serializable接口,并且包含一个Map对象属性,其readObject方法有自动修改自身Map属性的操作。
至于为什么最终会找到的这个类,我就不清楚了,应该需要对 java 的各种类都有个详细的了解。基于此我有个idea,来帮助快速寻找可能存在的利用链
我们看下jre/lib/rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class这个类(jdk1.7.0_80)
在readObject中遍历了 map ,然后在var5.setValue()会改变map中的 value,并且var2 和var4的值是可控的,都可以在构造方法中设置
所以我们可以创建一个AnnotationInvocationHandler对象,将之前构造的POC放入AnnotationInvocationHandler对象中,当目标存在反序列化漏洞时,就会自动触发setValue操作改变map中的value的值,从而触发上面分析的过程,执行命令。
因为AnnotationInvocationHandler是一个内部类,不能通过new来实例化,所以需要用到反射来获取对象
1 | Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
然后将代码拼接,并模拟一下序列化与反序列化的过程,看看能不能自动触发命令执行
出错,这是因为Runtime没有继承Serializer接口,是无法进行序列化的,所以需要反射获取Runtime对象。
1 | package con.yhy; |
我们将Runtime.getRuntime()换成Runtime.class,前者是一个java.lang.Runtime对象,后者是一个java.lang.Class 对象。Class类有实现Serializable接口,所以可以被序列化,之后利用反射的知识和InvokerTransformer实现的transform方法的特性获取getRuntime方法并执行。
改造完成后执行,我们发现虽然没有了错误,但还是没有执行命令。按理说我们的逻辑是通的:
- sun.reflect.annotation.AnnotationInvocationHandler的readObject会调用我们传入map的setValue方法,改变 map 中的value的值;
- 当 map 中的 value 的值发生改变时会调用对应的transform方法,这里是ChainedTransformer的transform方法;
- ChainedTransformer的transform方法,接受一个Transformer类型的数组,然后遍历执行数组中元素的transform方法,并且将上个元素的返回结果作为下个元素执行transform方法的参数;
- 然后我们利用了ConstantTransformer和InvokerTransformer精心构造了一个**Transformer[]**数组来执行命令
在加入AnnotationInvocationHandler之前都是可以正常执行命令的,所以问题可能出在了AnnotationInvocationHandler的readObject方法,来调试一下看看,直接在AnnotationInvocationHandler#readObject这里下断点
调试中发现,它在355行执行完后,直接到最后行末了,var5.setValue根本就没有执行的机会,在右下角的参数值里面可以看到 this.memberValues里根本就没有值,所以不会向下执行。
看了一下P牛的代码,发现在HashMap实例化后,添加了一个数据
var1.defaultReadObject();
this.memberValues就不为零了,然后经过测试发现innerMap.put("value", "yhy"); 只有添加的key的值为value才可以触发,只有当key为value时var7 != null才会成立,这点P牛给出的答案是:
那么如何让这个var7不为null呢?这一块我就不详细分析了,还会涉及到Java注释(ps:这里应该是注解)相关的技术。直接给出两个条件:
sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是 Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
被TransformedMap.decorate修饰的Map中必须有一个键名为X的元素
所以,这也解释了为什么我前面用到 Retention.class ,因为Retention有一个方法,名为value;所 以,为了再满足第二个条件,我需要给Map中放入一个Key是value的元素:
P牛说的第一点应该是这里,sun.reflect.annotation.AnnotationInvocationHandler的构造方法里确实是这样要求的,传入的第一个参数只能为注解类型,
为什么要有一个方法呢?
这里的 var7 是经过传入的注解类实例化执行memberTypes()经过下面的转化过程,返回map,得到var3,转化过程可以debug看看,当注解类存在方法时会返回最下方框的一个map集合,这时(Class)var3.get(var6);才会存在值,即var7 != null。(存在方法的注解类有两个Retention和Target,这两个传入哪个都可以)
下面找了一个不存在方法的注解类Documented进行对比,会发现最终返回时根本没有值。
第二点会好理解点:之前说过,修改 map 中的key或者value时会自动触发我们自定义的transform方法,因为Retention注解中的方法名为value,在上面转换中,将此方法名放入了转换后 map 的 key中,所以想要改动map中的value,必须使用相同的key值即value。
至此,经过P牛简化后的利用链,已经大致清晰,并且可以用以实战,但是上述生成的序列化数据只能在Java 8u71之前使用,在8u71以后大概是2015年12月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
修改jdk版本,重新运行,可以看到并没有执行命令
改动后的readObject函数不再有针对我们构造的map的赋值语句,所以触发不了漏洞。而是改成了新建一个LinkedHashMap,把值转进这个LinkedHashMap里面。
所以,后续对Map的操作都是基于这个新的 LinkedHashMap 对象,而原来我们精心构造的Map不再执 行set或put操作,也就不会触发RCE了。
0x05 ysoserial项目中的CC1链
可以看到利用链中并没有之前分析的一个关键点TransformedMap,而是变成了LazyMap,这就导致了之后的利用链完全不同
1 | /* |
5.1 ysoserial中的LazyMap
LazyMap和TransformedMap类似,都来自于CommonCollections库,并继承自AbstractMapDecorator。
TransformedMap是只要调用decorate()函数,传入key和value的变换函数Transformer,然后Map中的任意项的Key或者Value被修改,相应的Transformer(keyTransformer或者valueTransformer)的transform方法就会被调用。
LazyMap是只要执行 get 方法就会调用transform,这个利用链利用的核心条件就是去寻找一个类,在对象进行反序列化时会调用精心构造对象的 LazyMap 的get方法。
但是sun.reflect.annotation.AnnotationInvocationHandler的readObject方法中并没有直接调用到Map(this.memberValues)的get方法。
所以ysoserial找到了另一条路,因为AnnotationInvocationHandler类 同时也实现了InvocationHandler接口(动态代理),在invoke方法有调用到get
从这点就可以看出LazyMap这条链比TransformedMap较为复杂,在TransformedMap那条利用链中,我们只需要将map对象利用反射放入AnnotationInvocationHandler中,然后序列化数据即可。
而LazyMap若想要调用到invoke还需要用到Java中的动态代理,来一起简单学习吧。
5.2 Java动态代理
先来理解下什么是动态代理
在不修改类的源码的情况下,通过代理的方式为类的方法提供更多的功能。
举个例子来说(这个例子在开发中很常见):我的开发们实现了业务部分的所有代码,忽然我期望在这些业务代码中添加日志记录功能的时候,一个一个类去添加代码就会非常麻烦,这个时候我们就能通过动态代理的方式对期待添加日志的类进行代理。
在java的 java.lang.reflect 包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。
P牛写了一个Demo,我们来看下
1 | package org.vulhub.Ser; |
1 | package org.vulhub.Ser; |
运行App,我们可以发现,虽然向Map放入的hello值为world,但获取到的结果却是:
当一个类实现了InvocationHandler接口,并实现invoke方法,我们就可以通过动态代理的方式去劫持该类的内部函数调用,当执行该类的任意方法时都会调用invoke方法。
我们再来看看AnnotationInvocationHandler类,它实现了InvocationHandler,并且实现了invoke方法
所以ysoserial通过动态代理劫持了AnnotationInvocationHandler的调用
5.3 通过LazyMap构造利用链
将之前 Demo 中的 TransformedMap 换成 LazyMap
1 | Map outerMap = LazyMap.decorate(innerMap,transformerChain); |
通过动态代理劫持AnnotationInvocationHandler
1 | // 通过反射机制 实例化 AnnotationInvocationHandler |
因为我们反序列化的入口点是sun.reflect.annotation.AnnotationInvocationHandler#readObject ,所以我们需要使用AnnotationInvocationHandler对这个proxyMap进行包裹:
1 | Object proxy = ctor.newInstance(Target.class, proxyMap); |
完整POC
1 | import org.apache.commons.collections.Transformer; |
成功执行命令
5.4 其它
有时候调试上述POC的时候,会发现弹出了两个计算器,或者没有执行到readObject的时候就弹出了计算器,这是由于IDEA中Debug就利用toString,在过程中会调用代理类的toString方法从而造成非预期的命令执行(在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,)
ysoserial中对此也有一些处理,它在POC的最后才将执行命令的Transformer数组设置到transformerChain 中
1 | Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain |
还有ysoserial在transform数组中多了一个元素
P牛猜测是为了隐藏报错信息,在上述执行命令成功后,可以看到有一条报错信息,再加入这个元素后,隐蔽了启动进程的日志特征
经过测试,LazyMap与TransformedMap都不能在高版本Java(8u71)之后利用,至于如何解决那是另一条CommonCollections利用链的学习了(CommonCollections6)。
上述分析过程用到的代码都已上传到 https://github.com/yhy0/JavaSerializeDemo
0x06 参考
https://www.gettoby.com/p/0h2v47vgjznb
P牛知识星球-java安全漫谈










































