ShokaX

Java反序列化(4)-CC1

发布于 字数统计 13.4k 字 阅读时长 45 分钟

Java反序列化(4)-CC1

发布于 字数统计 13,374 阅读时长 67 分钟

简介

Apache Commons Collections 是对 java.util.Collection 的扩展。它增强了Java集合框架。 它提供了几个功能来简化收集处理。 它提供了许多新的接口,实现和实用程序。

当我们学习了URL DNS链后,我们就大致直到反序列化漏洞是怎么威胁服务器的。我们就可以着手分析CC链来增加我们对反序列化漏洞的认识了。

这里我还是跟着b站:白日梦组长大佬学习的,环境的搭建可以去b站搜他的视频。

然后呢CC1其实有两条利用链,其实大差不差。一条利用了TransformerMap,一条利用了LazyMap。

TransformerMap这条链的分析

下面的分析我们还是从发掘漏洞的视角走。首先嘛,我们是需要找到一个可能存在危险方法的地方,并且这个地方是我们能够利用的。那就是从危险函数开始找。想直接找到exec又不太现实,但我们可以找反射啊,如果哪里有反射的执行,那我们就有机会利用这个反射来执行命令。然后再向上找,直到找到readObject。

InvokerTransformer类分析

首先,针对CommonsCollections这个组件,我们可以看到它有很多接口,其中Transformer这个接口,我们追踪其实现类。

image-20220918153021558

我们可以看到,这个实现类中的transform方法,它居然调用了反射去执行了一些操作。而这里我们经过分析,这个反射中的所有输入我们的可以控制。

image-20220918153032751

从这里我们可以看到,这个实现类的构造函数接收3个参数,第一个就是要执行的方法的名,数据类型只是简单的String,第二个则是这个方法的参数的数据类型,要求的参数是一个Class数组,第三个则是这个方法的参数具体值,为一个对象数组。这里这个函数用数组的原因就是不确定这个方法有多少个参数,所以它用数组来存储,很正常。

image-20220918153045354

接着我们来试试,利用这个InvokerTransformer来调用runtime实现RCE。首先我们回忆下,利用反射怎么执行命令。如下,我们简单的利用反射去实现了RCE。

image-20220918153054303

因此如果我们要通过这个类去执行runtime这个类的exec方法的话,第一个参数methodName就是exec,第二个参数就是这个方法的传参的数据类型的一个数组,第三个则是给这个方法的参数的一个数组。

image-20220918153103096

然后上面就绑定好了Class反射对象,接着需要给一个具体的对象让他执行,而这个对象的传入就是在transform方法中。

image-20220918153111279

到这里其实就差不多了,我们只需要把参数拼装给InvokerTransformer这个类就可以了。就针对这个类,给它补充它要的参数,然后执行它的危险方法即可。

image-20220918153118625

到这里我们差不多就分析完这个InvokerTransformer类的作用了。但为了方便理解,我们还是需要做个总结。

这个类的transform方法的作用其实就是用反射的方式帮我们执行我们输入的类对象的某个方法。

用一个比喻来说就是起到一个修饰的作用,实例化InvokerTransformer类就相当于创建了一个装饰器,当调用transform方法的时候才对transform的参数对象执行装饰这个动作。

寻找利用链 - ①TransformerMap分析

PS:这里其实就是找调用了transform方法的类。

因为利用链的最后一步是InvokerTransformer.transform,所以我们还是和之前一样,要找一个移花接木的类,一直向上寻找,直到找到readObject这样就能形成一个完整的反序列化链,说白了就是找调用了transform(不一定是InvokerTransformer中的transform,也可以是不同类中的同名的方法)这个方法的类。

然后再找对应的这个方法在哪被调用了,最后一直找到readObject。

首先我们寻找transform方法,发现TransformedMap这个类的checkSetValue有调用transform。

image-20220918153219261

接着我们看valueTransformer.transform(value)中的valueTransformer是什么,能否被我们控制。

往上寻找,我们发现这个值是由构造函数中创建的,但因为构造函数是protected,所以我们不能外部直接调用构造函数,但在当前类中肯定是有其他public调用了这个构造函数的。

image-20220918153303845

寻找在当前类中调用了这个构造函数的方法。找到一个public方法,直接就返回构造函数的值。所以我们这里是能够正常控制这个参数的。

image-20220918153312600

接着就是checkSetValue方法的问题了,checkSetValue方法也是protected的,追踪这个方法,发现在它的父类中有个public方法调用了。这个类我们看不太懂什么意思,但看类名应该不难猜想出来,这不就是Map的遍历吗?再加上TransformedMap的类名和返回值为Map,所以我们可以猜测这就是entry遍历时会执行的类。

image-20220918153320543

简单的打个断点测试下,发现居然真的进来了,看来思路没错。并且也是能直接弹calc的。

image-20220918153328282

image-20220918153331310

image-20220918153336424

寻找利用链 - ②AnnotationInvocationHandler分析

接着我们继续往上找,直到找到readObject嘛。搜索setValue的用法,刚好找到有个直接就在readObject的。

而且是在原生jdk中,直接上手,看看参数是否可控。

image-20220918153349421

首先是去追踪memberValue这个参数,发现是在构造器中赋值的,但是构造器没有public方法,那只能通过反射去获取了。第一个参数是注解,第二个参数是Map

image-20220918153359429

如图,我们就成功通过反射去获取到实例了。

image-20220918153404619

接着我们做一个简单的序列化反序列化测试,测试下看看能不能步入到setValue中。因为可以看到要进入setValue得经过几个if判断。打上断点发现,当前情况下我们进不去setValue,卡在了第一个if判断中。

image-20220918153411937

我们继续打上更多断点去分析这个memberType,可以发现,这个memberType其实就是我们第一个参数Override.class。 然后读取Override这个注解的memberTypes,遍历提取里面的一个name。

image-20220918153420851

而这里,Override的memberTypes是空的,我们跟进Overide注解看看,发现这个注解是个空注解。因此猜想,如果换个有内容的注解会不会好点呢。于是我们找到Retention注解。

image-20220918153440149

image-20220918153446446

image-20220918153449382

改变参数,把第一个参数换成Retention.class再看看,发现,这次memberTypes有内容了,但后面memberType=memberTypes.get(name)。尝试获取这个name的时候没获取到,所以还是null。

image-20220918153456716

到这里其实差不多了,这个name=memberValue.getKey(),也就是说这是我们传入的Map的key,而memberTypes又有一个名为Value的数据。因此我们只要让Map的key为Value就行了。

image-20220918153505869

image-20220918153509523

到这里我们就分析得差不多了,可以确认的是,我们能够进入memberValue.setValue(),并且memberValue也是我们能操控的,接着就剩下setValue的参数的问题了,只要这个参数我们可控,那我们这条链就能完美的走下去。当然这个参数我们肯定是能控的,但具体的分析,我们放到后面来讲。

利用链小结

到这里,我们这条链其实就差不多了,简单的来看就是

AnnotationInvocationHandler.readObject->TransformedMap.checkSetValue->InvokerTransformer.transform->Runtime.exec

利用过程的难点

虽然我们前面分析完了,看起来确实能够利用。但想要实际利用还是有一些疑难的。

Runtime无法序列化的问题

我们先贴一张用反射来执行calc的语句,我们可以看到,即使我们通过反射来获取runtime对象,得到的这个对象是无法序列化的。毕竟它的本质还是runtime类。而runtime类没实现Serializable接口。所以我们的Runtime对象如何序列化就是首当其冲的第一个难题。

image-20220918153932604

为了解决这个问题,我们回顾下InvokerTransformer的作用,它就相当于一个装饰器,我们可以预先定义一些装饰动作,只有当执行transform方法的时候才会确切的执行这个动作。也就是说我们可以先把生成runtime这个实例的所有反射构造好,只有当调用transform方法的时候才会创建相应的实例。这样我们就能够正常的达到我们的目的了。

因为Runtime这个类是没实现Serializable接口的,所以我们要想执行命令,就得通过CLASS这个类,也就是反射类去让代码在运行的过程中动态的生成获取对象,这样我们的对象才能够正常的序列化。

个人理解:相当于把生成runtime实例的工厂给用反射做了出来,这样我们就没有直接生成实例,只有当代码被真正执行的时候,实例才会被工厂给生成出来。

image-20220918153950847

当我们理解前面InvokerTransformer实现反射调用calc的逻辑后,我们就需要考虑一个问题了:

针对上面的InvokerTransformer,明显是一层套一层的,前面不执行transform创建对应的实例,后面就没法执行获取内容。这怎么整呢?

这里我们引入transform的另一个实现类ChainedTransformer

下图是ChainedTransformer的源码,我们可以看到,它接收一个transoformers数组。当调用ChainedTransformer它自身的transform方法时,会遍历数组transoformers,然后递归调用它们的transform方法把前一个transoformer的输出当作下一个transoformer的输入。

image-20220918154031565

image-20220918154036906

到这里,Runtime无法反序列化这个难点就被解决了。

memberValue.setValue的参数是否可控的问题

前面我们分析入口类AnnotationInvocationHandler时,略过了memberValue.setValue参数的分析。

这里我们就得补上了。

image-20220918154105287

我们先对代码打上断点分析下。追踪发现,我们成功进入memberValue.setValue后,当调用第一个transformer的transform时,transform的参数是一个奇怪的值。很显然,我们得想办法让这个参数等于Runtime.class。但这个值看起来就不是我们能够直接操控的。

image-20220918154117364

这肯定不行,要想我们的命令执行能成功,这个value就必须得是Runtime.class。而我们又没法从输入端直接控制这个value值。那怎么办呢?

这里我们再引入Transformer接口的另一个实现类ConstantTransformer。

这个ConstantTransformer类的transform方法很简单,就一行,它实现的功能就是不管transform的输入是什么,都返回一个装饰器预定义的值。

image-20220918154134602

也就是说,我们让这个Transformer作为装饰器链的第一条,并且让它的装饰参数为Runtime.class,这样当执行ConstantTransformer.transform时,无论参数是什么,都返回的是Runtime.class,从而后面的内容就能够被正常执行了。

image-20220918154146612

整体代码漏洞利用

至此我们其实已经搞清楚了整个链的思路。我们把代码整合起来,就如下:

image-20220918154206172

大致流程差不多就这样。结合这个图再看,会清晰很多。(自己画的图,意会一哈)

image-20220918154215502

另一条利用链LazyMap

CC1还存在另一条利用链,即:

AnnotationInvocationHandler.readObject->AnnotationInvocationHandler.invoke->LazyMap.get->InvokerTransformer.transform->Runtime.exec

这条链的后部分其实和TransformedMap是一样的,但前部分稍有差别。直接走进分析。

动态代理

这条链其实用到了动态代理的知识点,因此在分析这条链之前,有必要学下下动态代理是个什么玩意。

场景模拟

我们用一个场景来解释动态代理的好处。首先呢如果我们有个类,他实现了如下四个方法。一开始的代码只是简单的返回result。但后面我们要加需求,需要它们输出日志。这该如何操作呢?

image-20220918154403566

OK,针对如上的需求,我们写出的代码如下:

image-20220918154413873

代码缺陷

从上面我们可以看到,对于如上需求的代码我们存在如下缺陷:

  • 对核心业务功能有干扰(把日志功能放到了核心计算功能中),导致程序员开发核心业务功能时分散了精力。
  • 附加功能(日志功能)分散在各个业务功能方法中,不利于统一维护。

解决思路

解决两个问题,核心就是解耦。我们需要把附加功能从业务功能代码中抽取出来。

也就说说把日志功能从加减乘除的核心方法中抽离出来。

困难

要抽取的代码在方法内部并且不是连续执行的(日志功能分别在核心功能的前后),靠以前把子类中的重复代码抽取到父类的方法没法解决。所以需要引入新的技术——代理模式。

代理模式(一种思想)

代理模式是二十三种设计模式中的一种,属于结构型模式。它的作用是通过提供一个代理类,让我们在调用目标方法的时,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。

调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。

image-20220918154733932

静态代理

PS

。这里通过实现Calc接口,把CalcImpl类作为属性。通过代理类去操作。

实际上未解决统一维护的问题,只是把问题转嫁给了代理对象。完全不具备任何灵活性,并且产生了大量的重复代码,日志还是分散管理的。没有统一管理。

因此提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这个代理类来实现。这就需要动态代理实现了。

image-20220918154827904

image-20220918154836975

动态代理(JDK动态代理)

目标类必须实现某个接口。

通过JDK中的api动态(在代码运行中)的为某一个类创建它的代理类,要求必须有接口,最终生成的代理类在com.sun.proxy包下,类名为proxy1,proxy1,proxy2…..因此会对以后我们的代码有影响。
这其实就是利用到反射的机制去实现的。因此需要引入反射包。
PS:简单的说就是调用api动态的生成类似上面的静态代理类。它的接口看起来有点复杂,但其实我们只需要按照格式,按部就班填就差不多了。

如下代码我们就是预定义了CalcImpl这个类的代理类该怎么实现,怎么做。

image-20220918154924580

image-20220918154930156

这里再强调下,想要使用动态代理的一些问题和动态代理的一些特性:

①目标类必须是实现了某个接口才能创建代理对象。

②动态代理的对象必定会调用InvocationHandler这个接口的某个实现类的invoke方法。也就是说,如果如果某个类实现了InvocationHandler这个接口,如果此时用上动态代理,那么我们就必定能执行它的invoke方法,从而改写我们的利用链。(从上面的代码也可以看出,我们无论执行add还是sub都会调用到InvocationHandler中的invoke方法)

image-20220918155122580

LazyMap分析

首先,还是从transform这个方法走起。查找它的用法,发现在LazyMap这个类中的get方法中有用到,接着我们分析我们能否控制它的参数。

image-20220918155153505

分析发现,其实和TransformedMap差不多的,直接尝试一下能否直接调用成功执行calc。可以看到,这条链是能够正常利用的。接着我们就得往上寻找更多调用,直到找到readObject了。

image-20220918155201579

AnnotationInvocationHandler中的invoke分析

向上寻找调用了get方法的类,但这个get太常用了,返回结果巨tm的多。这里直接看AnnotationInvocationHandler这个类。我们先看readObject,发现也有用到get方法的调用。

但是很可惜的是,前面的对象我们没法控制。因此没法直接利用这个readObject。

image-20220918155223069

继续看,我们发现当前类的Invoke方法也调用了这个get方法,并且前面的参数我们是可以控制的。之前已经分析过了。

image-20220918155230466

但问题来了,我们要怎么触发这个invoke方法呢?此时发现这个方法有个叫proxy的参数,是不是就能想到动态代理。然后我们还发现这个类实现了InvocationHandler接口。也就是说,我们用上动态代理,把这个类当作动态代理的第三个参数传进去,就必定能执行调用这个类的Invoke方法。

image-20220918155238026

到这里,其实就差不多了,我们简单做下测试,看下这条链能不能走完。很显然,我们这个思路是没问题的。

image-20220918155245213

恶意对象的构造

接着就剩最后一步,我们最终怎么和readObject联动起来?从前面我们得知,要想动态代理能够执行,就必须得执行代理的对象的任意方法(但不包括readObject,亲测)。

那这里我们就想到了再套一层,因为在AnnotationInvocationHandler的readObject中调用了memberValues的很多方法。比如下图,箭头指的地方都是,当这个memberValues为代理对象时,都会执行到绑定的InvocationHandler的invoke方法。

所以我们就可以把代理对象给封装到AnnotationInvocationHandler中,这样,在对这个封装对象进行反序列化的时,就会执行如下函数。从而触发代理对象的invoke,进入到利用链中。

PS:有个很奇怪的事,我打断点发现,特么的,打在invoke的断点是在calc弹出后才步入的。而且离谱的是,如果在invoke中打上断点后,我在后面的InvokerTransformer这些类打的断点都不生效。没搞懂什么情况??莫非是动态代理打断点没用??

image-20220918155327271

最终我们构造完整的POC,如下图。

image-20220918155418448

往exec里打个断点,发现确实没什么毛病。执行流程也和我们想像中的是一样的。

image-20220918155426455

画个图理清流程

image-20220918155438329