“本文是对ysoserial CommonsCollections1的利用链和源码分析,也是java反序列化系列的第二篇文章”
0x01 前言
上篇文章对Apache Commons Collections反序列化漏洞利用链进行了分析,也埋坑说要结合此漏洞,深入分析ysoserial反序列化工具,本文也算是填坑之作。
ysoserial是一款知名的java反序列化利用工具,里面集合了各种java反序列化payload,而其中CC1链和上篇文章的利用点也有所不同,也涉及到了动态代理和RMI等一些新知识。
0x02: 基本概念
1)Java动态代理
代理是一种常用的设计模式,其目的就是为真实对象提供一个代理对象以控制对真实对象的访问,或在真实对象原有的功能上,增加额外的功能。代理类负责为委托类(被代理类、真实类)预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。按照代理的创建时期,代理类可分为两种:动态代理和静态代理。
动态代理是指利用Java的反射技术在运行时生成代理类对象。
示例业务逻辑:如果要找Jaychou唱歌、演戏,需要先找两个助理中的一个,然后助理去找Jaychou唱歌、演戏。

proxy类和InvocationHandler接口是动态代理的两个核心机制,其通过相互配合来实现动态代理:
Proxy类就是用来创建一个代理对象的类,它提供了很多方法,但最常用的是newProxyInstance方法。
InvocationHandler接口是proxy代理实例的调用处理程序实现的一个接口,每一个proxy代理实例都有一个关联的调用处理程序;在代理实例调用方法时,方法调用被分派到调用处理程序的invoke方法。其相当于一种代码增强,即在原先的方法逻辑上加上额外操作,在方法执行之前和之后加点通用逻辑,方便实现和维护。
2)RMI
RMI(Java Remote Method Invocation):java远程方法调用,是一种用于实现远程过程调用的应用程序编程接口,它使客户机上运行的程序可以调用远程服务器上的对象。其用于不同JAVA虚拟机之间的通信,这些JAVA虚拟机可以在不同的主机上,也可以在同一主机上。
简单来说,就是两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
RMI主要分为三部分:
RMI Registry注册中心
RMI Client 客户端
RMI Server服务端
其中RMI注册中心是一个放置所有服务器对象的名称空间。服务器每次创建一个对象时,都会向RMIregistry注册该对象(使用bind( )或reBind( )方法),这些是使用称为绑定名称的唯一名称注册的。
要调用远程对象,客户端需要该对象的引用。那时,客户端使用其绑定名称(使用lookup( )方法)从注册表中获取对象。
传输原理:
1、客户端调用客户端本地的stub类(相当于代理,用于与服务器端的通信)
2、客户端本地的stub类把信息序列化发给服务器端的skeletons类(也可认为是代理)
3、服务器端的skeletons类把信息反序列化交给服务器端的对应类进行处理
4、服务器端对应类处理完后将结果返回给服务器端的skeletions类
5、skeletions类序列化数据发送给客户端本地的stub类
6、客户端本地的stub类把数据反序列化后将结果返回给客户端

在Java中,只要一个类继承了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。Remote接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在远程接口中指定的这些方法才可被远程调用。

远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。



0x03 Ysoserial CC1利用链构造分析
TransoformedMap类的关键点在checkSetValue()方法,我们构造好的含有利用代码的ChainedTransformer利用链即transformers数组会循环进入此处。

而Ysoserial CC1链使用的LazyMap类关键点在其get( )方法,会触发transform过程。

在不看CC1链具体代码之前,我们不妨先尝试构造一下这个利用链。
要想触发transform过程,此时this.factory的值就需为我们构造好的利用代码即ChainedTransformer利用链,当调用LazyMap对象的get方法时就会触发命令执行。
那this.factory参数是否是可控的?

Ysoserial CC1链中利用的还是在上篇文章中分析过的AnnotationInvocationHandler类,该类重写了readObject( )方法,但其并没有明确的调用get()方法,此时就使用到了动态代理。
AnnotationInvocationHandler类实现了InvocationHandler接口,其invoke()方法,代码如下:

如上代码所示,其主要作用是当代理的对象调用toString,hashCode,annotationType时,就返回相应的结果,如果调用了其他方法,就会去执行Object var6= this.memberValues.get(var4)。
那如果this.memberValues是可控的,且其为Map类型,就可以达到我们的目的,查看其构造函数,this.memberValues正好是Map类型。

目前只要可以调用AnnotationInvocationHandler实例对象的invoke方法就可以触发攻击链,导致代码执行。
前文提到每一个proxy代理实例都有一个关联的调用处理程序InvocationHandler,而invoke方法就是代理对象调用方法时的调用处理程序。
因此我们需要构造一个动态代理的对象,让其调用方法,从而触发invoke方法。
接着看AnnotationInvocationHandler的readObject方法。

this.memberValues是可控的,当其是一个AnnotationInvocationHandler类生成的动态代理对象时,在调用entrySet()方法时,会自动去调用invoke方法,而由于其调用的不是toString,hashCode等方法,就会执行Object var6 = this.memberValues.get(var4),从而完成攻击链条,导致代码执行。
反序列化数据流
执行redObject()方法
执行this.memberValues.entrySet()方法
触发InvocationHandler的invoke方法
执行invoke方法里的this.memberValues.get()
调用LayzMap的get()方法
执行get方法里的this.factory.transformer()
调用chainedTransformer的transform方法
循环调用InvokerTransformer的transform方法构造Runtime对象
执行Runtime对象的exec方法
0x04 Ysoserial CC1链源码分析
启动RMI服务

使用ysoserial工具来给本地的RMI服务器发送payload:
java -cp ysoserial-master-d367e379d9-1.jar ysoserial.exploit.RMIRegistryExploit 192.168.1.5 777 CommonsCollections1 "calc.exe"

可以看到,使用的是ysoserial.exploit.RMIRegistryExploit类,跟进该类。
public class RMIRegistryExploit {..............public static void main(final String[] args) throws Exception {//获取到命令的第一个参数 即RMI服务端IP 192.168.1.5final String host = args[0];//获取到命令的第二个参数 即RMI服务端端口 777final int port = Integer.parseInt(args[1]);//获取的要执行的命令 "calc.exe"final String command = args[3];//获取192.168.1.5:777 上存在的RMI服务Registry registry = LocateRegistry.getRegistry(host, port);//获取paylaod类名即:ysoserial.payloads.CommonsCollections1final String className = CommonsCollections1.class.getPackage().getName() + "." + args[2];//获取ysoserial.payloads.CommonsCollections1类的对象final Class<? extends ObjectPayload> payloadClass = (Class<? extends ObjectPayload>) Class.forName(className);// test RMI registry connection and upgrade to SSL connection on failtry {registry.list();} catch(ConnectIOException ex) {registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());}//将获取到的RMI服务对象,payloads所在的类CommonsCollections1类的对象,要执行的命令作为参数传入,调用exploit方法// ensure payload doesn't detonate during construction or deserializationexploit(registry, payloadClass, command);}public static void exploit(final Registry registry,final Class<? extends ObjectPayload> payloadClass,final String command) throws Exception {new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {//获取CommonsCollections1类无参构造器运行时类的对象ObjectPayload payloadObj = payloadClass.newInstance();//调用CommonsCollections1类运行时类的对象的getObject方法,并将要执行的命令作为参数传入//getObject方法 即前文Client利用代码Object payload = payloadObj.getObject(command);String name = "pwned" + System.nanoTime();//使用基于AnnotationInvocationHandler的动态代理,从而加载rmi协议指定的类Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);try {//将远程引用绑定到RMI注册服务器中的指定nameregistry.bind(name, remote);} catch (Throwable e) {e.printStackTrace();}Utils.releasePayload(payloadObj, payload);return null;}});}}
重点关注的代码部分如下:

ysoserial.payloads.CommonsCollections1类
//payloadClass为获取的ysoserial.payloads.CommonsCollections1类的对象//payloadObj即获取的CommonsCollections1类无参构造器运行时类的对象ObjectPayload payloadObj = payloadClass.newInstance();Object payload = payloadObj.getObject(command);
调用CommonsCollections1类运行时类的对象的getObject方法,并将要执行的命令作为参数传入 ,跟进getObject方法。

前半部分代码不再详细解释,上篇文章有分析,主要是生成ChainedTransformer实例,把一些transformer链接到一起,构成一组链条,对一个对象依次通过链条内的每一个transformer进行转换。
蓝框的代码先是生成一个Map实例,并调用LayMap的decorate()方法,将Map实例和ChainedTransformer实例作为参数传入,创建一个LazyMap对象,之后利用了两层的动态代理来封装lazyMap对象。

第一次调用createMemoitizedProxy生成mapProxy代理对象,层层跟进:

此时就用到了在前文提到的动态代理的两个核心机制,proxy类和InvocationHandler接口。和该类最常用的是newProxyInstance方法。
此处使用proxy.newProxyInstance()方法生成代理对象,该方法有三个参数。
参数一loader:生成代理对象使用哪个类装载器【一般使用的是被代理类的装载器】参数二interfaces:生成哪个对象的代理对象,通过接口指定【指定要被代理类的接口】参数三h:生成的代理对象的方法里干什么事,动态代理方法在执行时,会调用h里面的invoke方法去执行【即InvocationHandler接口的实现】
其中参数三就是前文提到的InvocationHandler接口的实现,动态代理方法在执行时,会调用里面的invoke方法去执行,此处参数三ih即AnnotationInvocationHandler的实例对象。
//ANN_INV_HANDLER_CLASS="sun.reflect.annotation.AnnotationInvocationHandler"(InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
而其中lazyMap赋给了AnnotationInvocationHandler实例对象的memberValues属性。

因此当动态代理在调用方法时,会调用AnnotationInvocationHandler的invoke方法,从而调用this.memberValues.get()。

同样第二次调用createMemoizedInvocationHandler生成handler代理对象,将mapProxy对象赋给AnnotationInvocationHandler的memberValues属性。

二次调用后的memberValues关系大致如下:
handler.memberValues == mapProxymapProxy.handler.memberValues == lazyMap

之后将生成的handler代理对象返回。
反序列化
返回到ysoserial.exploit.RMIRegistryExploit类,继续往下看,调用createMemoitizedProxy生成remote代理对象,CommonsCollections1类返回的handler对象会作为createMap的参数传入。

跟进createMap,此处生成了一个map对象,并更新了一组数据{key为"pwned”加随机时间,val为handler代理对象}。

调用createMemoitizedProxy生成remote代理对象,将map赋给它的AnnotationInvocationHandler的memberValues属性,而Map的value为handler对象。

生成的remote代理对象会作为registry.bind()的参数传入,向RMI注册中心序列化传输远程对象。
当RMI注册中心反序列远程对象时,会调用AnnotationInvocationHandler类重写的readObject方法。

当readObject方法执行entrySet()时,动态代理会调用AnnotationInvocationHandler的invoke方法,而this.memberValues为lazyMap实例。
invoke方法里的this.memberValues.get(),即调用lazyMap的get()方法。

调用lazyMap的get()方法,从而触发transform利用链,造成远程代码执行,而这跟我们之前构造的利用链是相吻合的。

攻击链条:
ObjectInputStream.readObject()AnnotationInvocationHandler.readObject()Map(Proxy).entrySet()AnnotationInvocationHandler.invoke()LazyMap.get()ChainedTransformer.transform()ConstantTransformer.transform()InvokerTransformer.transform()Method.invoke()Class.getMethod()InvokerTransformer.transform()Method.invoke()Runtime.getRuntime()InvokerTransformer.transform()Method.invoke()Runtime.exec()
0x05 结语
ysoserial cc1链和Apache Commons Collections反序列化利用链的点还是有些区别,也涉及到了动态代理等一些新知识,比较复杂,而ysoserial的利用链还有很多,需要去不断深入分析,尽快填坑吧~
参考资料:
[1]https://mp.weixin.qq.com/s/bC71HoEtDAKKbHJvStu9qA
[2]https://zrquan.github.io/posts/ysoserial-cc1/#反序列化




