# 简介
Apache Commons Collections 是对 java.util.Collection 的扩展。它增强了 Java 集合框架。 它提供了几个功能来简化收集处理。 它提供了许多新的接口,实现和实用程序。
当我们学习了 URL DNS 链后,我们就大致直到反序列化漏洞是怎么威胁服务器的。我们就可以着手分析 CC 链来增加我们对反序列化漏洞的认识了。
这里我还是跟着 b 站:白日梦组长大佬学习的,环境的搭建可以去 b 站搜他的视频。
然后呢 CC1 其实有两条利用链,其实大差不差。一条利用了 TransformerMap,一条利用了 LazyMap。
# TransformerMap 这条链的分析
下面的分析我们还是从发掘漏洞的视角走。首先嘛,我们是需要找到一个可能存在危险方法的地方,并且这个地方是我们能够利用的。那就是从危险函数开始找。想直接找到 exec 又不太现实,但我们可以找反射啊,如果哪里有反射的执行,那我们就有机会利用这个反射来执行命令。然后再向上找,直到找到 readObject。
# InvokerTransformer 类分析
首先,针对 CommonsCollections 这个组件,我们可以看到它有很多接口,其中 Transformer 这个接口,我们追踪其实现类。
我们可以看到,这个实现类中的 transform 方法,它居然调用了反射去执行了一些操作。而这里我们经过分析,这个反射中的所有输入我们的可以控制。
从这里我们可以看到,这个实现类的构造函数接收 3 个参数,第一个就是要执行的方法的名,数据类型只是简单的 String,第二个则是这个方法的参数的数据类型,要求的参数是一个 Class 数组,第三个则是这个方法的参数具体值,为一个对象数组。这里这个函数用数组的原因就是不确定这个方法有多少个参数,所以它用数组来存储,很正常。
接着我们来试试,利用这个 InvokerTransformer 来调用 runtime 实现 RCE。首先我们回忆下,利用反射怎么执行命令。如下,我们简单的利用反射去实现了 RCE。
因此如果我们要通过这个类去执行 runtime 这个类的 exec 方法的话,第一个参数 methodName 就是 exec,第二个参数就是这个方法的传参的数据类型的一个数组,第三个则是给这个方法的参数的一个数组。
然后上面就绑定好了 Class 反射对象,接着需要给一个具体的对象让他执行,而这个对象的传入就是在 transform 方法中。
到这里其实就差不多了,我们只需要把参数拼装给 InvokerTransformer 这个类就可以了。就针对这个类,给它补充它要的参数,然后执行它的危险方法即可。
到这里我们差不多就分析完这个 InvokerTransformer 类的作用了。但为了方便理解,我们还是需要做个总结。
这个类的 transform 方法的作用其实就是用反射的方式帮我们执行我们输入的类对象的某个方法。
用一个比喻来说就是起到一个修饰的作用,实例化 InvokerTransformer 类就相当于创建了一个装饰器,当调用 transform 方法的时候才对 transform 的参数对象执行装饰这个动作。
# 寻找利用链 - ①TransformerMap 分析
PS:这里其实就是找调用了 transform 方法的类。
因为利用链的最后一步是 InvokerTransformer.transform,所以我们还是和之前一样,要找一个移花接木的类,一直向上寻找,直到找到 readObject 这样就能形成一个完整的反序列化链,说白了就是找调用了 transform(不一定是 InvokerTransformer 中的 transform,也可以是不同类中的同名的方法)这个方法的类。
然后再找对应的这个方法在哪被调用了,最后一直找到 readObject。
首先我们寻找 transform 方法,发现 TransformedMap 这个类的 checkSetValue 有调用 transform。
接着我们看 valueTransformer.transform (value) 中的 valueTransformer 是什么,能否被我们控制。
往上寻找,我们发现这个值是由构造函数中创建的,但因为构造函数是 protected,所以我们不能外部直接调用构造函数,但在当前类中肯定是有其他 public 调用了这个构造函数的。
寻找在当前类中调用了这个构造函数的方法。找到一个 public 方法,直接就返回构造函数的值。所以我们这里是能够正常控制这个参数的。
接着就是 checkSetValue 方法的问题了,checkSetValue 方法也是 protected 的,追踪这个方法,发现在它的父类中有个 public 方法调用了。这个类我们看不太懂什么意思,但看类名应该不难猜想出来,这不就是 Map 的遍历吗?再加上 TransformedMap 的类名和返回值为 Map,所以我们可以猜测这就是 entry 遍历时会执行的类。
简单的打个断点测试下,发现居然真的进来了,看来思路没错。并且也是能直接弹 calc 的。
# 寻找利用链 - ②AnnotationInvocationHandler 分析
接着我们继续往上找,直到找到 readObject 嘛。搜索 setValue 的用法,刚好找到有个直接就在 readObject 的。
而且是在原生 jdk 中,直接上手,看看参数是否可控。
首先是去追踪 memberValue 这个参数,发现是在构造器中赋值的,但是构造器没有 public 方法,那只能通过反射去获取了。第一个参数是注解,第二个参数是 Map
如图,我们就成功通过反射去获取到实例了。
接着我们做一个简单的序列化反序列化测试,测试下看看能不能步入到 setValue 中。因为可以看到要进入 setValue 得经过几个 if 判断。打上断点发现,当前情况下我们进不去 setValue,卡在了第一个 if 判断中。
我们继续打上更多断点去分析这个 memberType,可以发现,这个 memberType 其实就是我们第一个参数 Override.class。 然后读取 Override 这个注解的 memberTypes,遍历提取里面的一个 name。
而这里,Override 的 memberTypes 是空的,我们跟进 Overide 注解看看,发现这个注解是个空注解。因此猜想,如果换个有内容的注解会不会好点呢。于是我们找到 Retention 注解。
改变参数,把第一个参数换成 Retention.class 再看看,发现,这次 memberTypes 有内容了,但后面 memberType=memberTypes.get (name)。尝试获取这个 name 的时候没获取到,所以还是 null。
到这里其实差不多了,这个 name=memberValue.getKey (),也就是说这是我们传入的 Map 的 key,而 memberTypes 又有一个名为 Value 的数据。因此我们只要让 Map 的 key 为 Value 就行了。
到这里我们就分析得差不多了,可以确认的是,我们能够进入 memberValue.setValue(), 并且 memberValue 也是我们能操控的,接着就剩下 setValue 的参数的问题了,只要这个参数我们可控,那我们这条链就能完美的走下去。当然这个参数我们肯定是能控的,但具体的分析,我们放到后面来讲。
# 利用链小结
到这里,我们这条链其实就差不多了,简单的来看就是
AnnotationInvocationHandler.readObject->TransformedMap.checkSetValue->InvokerTransformer.transform->Runtime.exec
# 利用过程的难点
虽然我们前面分析完了,看起来确实能够利用。但想要实际利用还是有一些疑难的。
# Runtime 无法序列化的问题
我们先贴一张用反射来执行 calc 的语句,我们可以看到,即使我们通过反射来获取 runtime 对象,得到的这个对象是无法序列化的。毕竟它的本质还是 runtime 类。而 runtime 类没实现 Serializable 接口。所以我们的 Runtime 对象如何序列化就是首当其冲的第一个难题。
为了解决这个问题,我们回顾下 InvokerTransformer 的作用,它就相当于一个装饰器,我们可以预先定义一些装饰动作,只有当执行 transform 方法的时候才会确切的执行这个动作。也就是说我们可以先把生成 runtime 这个实例的所有反射构造好,只有当调用 transform 方法的时候才会创建相应的实例。这样我们就能够正常的达到我们的目的了。
因为 Runtime 这个类是没实现 Serializable 接口的,所以我们要想执行命令,就得通过 CLASS 这个类,也就是反射类去让代码在运行的过程中动态的生成获取对象,这样我们的对象才能够正常的序列化。
个人理解:相当于把生成 runtime 实例的工厂给用反射做了出来,这样我们就没有直接生成实例,只有当代码被真正执行的时候,实例才会被工厂给生成出来。
当我们理解前面 InvokerTransformer 实现反射调用 calc 的逻辑后,我们就需要考虑一个问题了:
针对上面的 InvokerTransformer,明显是一层套一层的,前面不执行 transform 创建对应的实例,后面就没法执行获取内容。这怎么整呢?
这里我们引入 transform 的另一个实现类 ChainedTransformer
下图是 ChainedTransformer 的源码,我们可以看到,它接收一个 transoformers 数组。当调用 ChainedTransformer 它自身的 transform 方法时,会遍历数组 transoformers,然后递归调用它们的 transform 方法把前一个 transoformer 的输出当作下一个 transoformer 的输入。
到这里,Runtime 无法反序列化这个难点就被解决了。
# memberValue.setValue 的参数是否可控的问题
前面我们分析入口类 AnnotationInvocationHandler 时,略过了 memberValue.setValue 参数的分析。
这里我们就得补上了。
我们先对代码打上断点分析下。追踪发现,我们成功进入 memberValue.setValue 后,当调用第一个 transformer 的 transform 时,transform 的参数是一个奇怪的值。很显然,我们得想办法让这个参数等于 Runtime.class。但这个值看起来就不是我们能够直接操控的。
这肯定不行,要想我们的命令执行能成功,这个 value 就必须得是 Runtime.class。而我们又没法从输入端直接控制这个 value 值。那怎么办呢?
这里我们再引入 Transformer 接口的另一个实现类 ConstantTransformer。
这个 ConstantTransformer 类的 transform 方法很简单,就一行,它实现的功能就是不管 transform 的输入是什么,都返回一个装饰器预定义的值。
也就是说,我们让这个 Transformer 作为装饰器链的第一条,并且让它的装饰参数为 Runtime.class,这样当执行 ConstantTransformer.transform 时,无论参数是什么,都返回的是 Runtime.class,从而后面的内容就能够被正常执行了。
# 整体代码漏洞利用
至此我们其实已经搞清楚了整个链的思路。我们把代码整合起来,就如下:
大致流程差不多就这样。结合这个图再看,会清晰很多。(自己画的图,意会一哈)
# 另一条利用链 LazyMap
CC1 还存在另一条利用链,即:
AnnotationInvocationHandler.readObject->AnnotationInvocationHandler.invoke->LazyMap.get->InvokerTransformer.transform->Runtime.exec
这条链的后部分其实和 TransformedMap 是一样的,但前部分稍有差别。直接走进分析。
# 动态代理
这条链其实用到了动态代理的知识点,因此在分析这条链之前,有必要学下下动态代理是个什么玩意。
# 场景模拟
我们用一个场景来解释动态代理的好处。首先呢如果我们有个类,他实现了如下四个方法。一开始的代码只是简单的返回 result。但后面我们要加需求,需要它们输出日志。这该如何操作呢?
OK,针对如上的需求,我们写出的代码如下:
# 代码缺陷
从上面我们可以看到,对于如上需求的代码我们存在如下缺陷:
- 对核心业务功能有干扰(把日志功能放到了核心计算功能中),导致程序员开发核心业务功能时分散了精力。
- 附加功能(日志功能)分散在各个业务功能方法中,不利于统一维护。
# 解决思路
解决两个问题,核心就是解耦。我们需要把附加功能从业务功能代码中抽取出来。
也就说说把日志功能从加减乘除的核心方法中抽离出来。
# 困难
要抽取的代码在方法内部并且不是连续执行的(日志功能分别在核心功能的前后),靠以前把子类中的重复代码抽取到父类的方法没法解决。所以需要引入新的技术 —— 代理模式。
# 代理模式(一种思想)
代理模式是二十三种设计模式中的一种,属于结构型模式。它的作用是通过提供一个代理类,让我们在调用目标方法的时,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来 —— 解耦。
调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
# 静态代理
PS: 说白了就是再多套一层类。这里通过实现 Calc 接口,把 CalcImpl 类作为属性。通过代理类去操作。
实际上未解决统一维护的问题,只是把问题转嫁给了代理对象。完全不具备任何灵活性,并且产生了大量的重复代码,日志还是分散管理的。没有统一管理。
因此提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这个代理类来实现。这就需要动态代理实现了。
# 动态代理(JDK 动态代理)
目标类必须实现某个接口。
通过 JDK 中的 api 动态(在代码运行中)的为某一个类创建它的代理类,要求必须有接口,最终生成的代理类在 com.sun.proxy 包下,类名为proxy2….. 因此会对以后我们的代码有影响。
这其实就是利用到反射的机制去实现的。因此需要引入反射包。
PS:简单的说就是调用 api 动态的生成类似上面的静态代理类。它的接口看起来有点复杂,但其实我们只需要按照格式,按部就班填就差不多了。
如下代码我们就是预定义了 CalcImpl 这个类的代理类该怎么实现,怎么做。
这里再强调下,想要使用动态代理的一些问题和动态代理的一些特性:
①目标类必须是实现了某个接口才能创建代理对象。
②动态代理的对象必定会调用 InvocationHandler 这个接口的某个实现类的 invoke 方法。也就是说,如果如果某个类实现了 InvocationHandler 这个接口,如果此时用上动态代理,那么我们就必定能执行它的 invoke 方法,从而改写我们的利用链。(从上面的代码也可以看出,我们无论执行 add 还是 sub 都会调用到 InvocationHandler 中的 invoke 方法)
# LazyMap 分析
首先,还是从 transform 这个方法走起。查找它的用法,发现在 LazyMap 这个类中的 get 方法中有用到,接着我们分析我们能否控制它的参数。
分析发现,其实和 TransformedMap 差不多的,直接尝试一下能否直接调用成功执行 calc。可以看到,这条链是能够正常利用的。接着我们就得往上寻找更多调用,直到找到 readObject 了。
# AnnotationInvocationHandler 中的 invoke 分析
向上寻找调用了 get 方法的类,但这个 get 太常用了,返回结果巨 tm 的多。这里直接看 AnnotationInvocationHandler 这个类。我们先看 readObject,发现也有用到 get 方法的调用。
但是很可惜的是,前面的对象我们没法控制。因此没法直接利用这个 readObject。
继续看,我们发现当前类的 Invoke 方法也调用了这个 get 方法,并且前面的参数我们是可以控制的。之前已经分析过了。
但问题来了,我们要怎么触发这个 invoke 方法呢?此时发现这个方法有个叫 proxy 的参数,是不是就能想到动态代理。然后我们还发现这个类实现了 InvocationHandler 接口。也就是说,我们用上动态代理,把这个类当作动态代理的第三个参数传进去,就必定能执行调用这个类的 Invoke 方法。
到这里,其实就差不多了,我们简单做下测试,看下这条链能不能走完。很显然,我们这个思路是没问题的。
# 恶意对象的构造
接着就剩最后一步,我们最终怎么和 readObject 联动起来?从前面我们得知,要想动态代理能够执行,就必须得执行代理的对象的任意方法(但不包括 readObject,亲测)。
那这里我们就想到了再套一层,因为在 AnnotationInvocationHandler 的 readObject 中调用了 memberValues 的很多方法。比如下图,箭头指的地方都是,当这个 memberValues 为代理对象时,都会执行到绑定的 InvocationHandler 的 invoke 方法。
所以我们就可以把代理对象给封装到 AnnotationInvocationHandler 中,这样,在对这个封装对象进行反序列化的时,就会执行如下函数。从而触发代理对象的 invoke,进入到利用链中。
PS:有个很奇怪的事,我打断点发现,特么的,打在 invoke 的断点是在 calc 弹出后才步入的。而且离谱的是,如果在 invoke 中打上断点后,我在后面的 InvokerTransformer 这些类打的断点都不生效。没搞懂什么情况??莫非是动态代理打断点没用??
最终我们构造完整的 POC,如下图。
往 exec 里打个断点,发现确实没什么毛病。执行流程也和我们想像中的是一样的。