目录
URLDNS
yso里面最简单的一条链子,看下调用栈:
poc:
1 | import java.io.*; |
分析:
HashMap为一个入口类,反序列化的时候掉用他的readObject方法,在readObject方法中又调用了hash方法,:
在hash方法中又掉用了url类对象的hashcode:
在hashcode中又调用了handler的hashcode方法,参数为this就是我们的那个url对象:
最终在hashcode方法中调用了getHostAddress完成了发送请求:
但是这里有两个问题,在poc中当我们 hashMap.put时候,会调用hash方法,导致会提前发送请求,并给hashcode赋值:
所以我们要通过反射来将hashcode的数值改掉,若然不然会导致两个问题,第一就是在put时提前发送请求,导致我们无法判断链子是否成功执行,第二个是因为在put时会给hashcode赋值(hashcode的默认值是-1)导致我们反序列化时无法通过以下判断,导致poc失效:
最终的poc在上面
cc1-1
Maven依赖,这里cc1,3,5,6,7都用的如下依赖:
1 | <dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency> |
cc1这条链总共有两种触发方式
先说第一种:
先放手写的exp
1 | import org.apache.commons.collections.Transformer; |
执行命令的点在InvokerTransformer.transform方法,可以通过下图看到transform方法通过反射去执行命令,并且参数都可控:
大致的调用栈是这样的:
- InvokerTransformer.transform->ChainedTransformer.transform->TransformedMap.checkSetValue->AnnotationInvocationHandler.setvalue
我们先从AnnotationInvocationHandler.readObject开始慢慢看,可以看到先头的exp里面我们是通过反射的形式去构造的AnnotationInvocationHandler对象,因为AnnotationInvocationHandler类并不是一个public类。看下AnnotationInvocationHandler类的构造方法与readObject方法:
我们到达setValue方法之前可以看到,存在一个if判断不为空:首先获得我们传进去的那个map中的key,并在我们的传进去的注解类中判断存不存在这个key 这也就是我们的exp中标红位置的由来,因为Target.class中是含有value这个值的:
到达setValue以后,会调用到TransformedMap.checkSetValue,这个地方大家可能会有点不明白,按照正常逻辑此时,应该执行到TransformedMap.setValue,但是怎么就执行到了TransformedMap.checkSetValue呢?大家可以看下下图的解释:
可以看到他说了当TransformedMap类在调用setValue方法时,会自动调用checkSetValue,所以执行到了TransformedMap.checkSetValue,并且下面标红处也是我们可控的:
但此时有一个问题,就是我们参数的问题,此时的value参数是写死的,这是因为我们调用setValue方法时,第一个参数就是写死的:
这个时候我们怎么办呢?有一个类叫ConstantTransformer,他的transform方法。就是返回一个任意的对象,并且这个对象是我们可以控制的:
最后再结合ChainedTransformer类的链式调用,成功就可以将我们的exp写出来了。
还有一个问题,就是我们的Runtime对象因为没有继承Ser,所以是不能进行序列化的,这个时候,我们可以利用Runtime.class来利用反射去构造出一个Runtime对象出来:
最后也是可以成功执行命令的:
cc1-2
这条链子,是yso上的,看下调用栈可以发现后半段和我们的cc1-1是一样的,不一样的只是前面的触发方式,前一个是TransformedMap.checkSetValue,这个是LazyMap.get:
factory是可控的。AnnotationInvocationHandler.invoke中调用到了get方法:
AnnotationInvocationHandler是一个Handler类,他调用任何一个方法时都会调用到invoke方法,并且在AnnotationInvocationHandler.readObject中,存在memberValues.entrySet(),且memberValues可控。
由此我们可以写出exp:
1 | import org.apache.commons.collections.Transformer; |
可执行命令:
cc6
这是一条万金油链子,像cc1在jdk高版本中都已经在代码层面被修复了,但是cc6这条链子,并不受jdk版本影响。
先放一下调用栈:
可以看到后半段和cc1-2那条链是一样的,前半段和urldns那条也是比较像的。
可以大概说是 cc6=cc1+urldns
我们先从HashMap.readObject开始看:
可以看到他最终调用到了key.hashCode并且key是我们可控的。我们接着看下TiedMapEntry.hashCode:
可以看到最终调用了get方法,并且这里的参数也是我们可控的,我们可以将map设置为LazyMap,这样最终就调用到了LazyMap.get。在分享urldns那条链的时候,我们知道在map.put时,也调用hash函数,这点我们要在exp的时候注意一下。
综上所述,我们可以写出exp:
1 | import org.apache.commons.collections.Transformer; |
可以执行命令:
cc3-1
在我们前面写的几条链子中,最后的触发方式都是通过 Runtime.getRuntime().exec 去执行命令,而我们接下来要写的,最后的触发方式是通过java的类加载机制去动态加载一个类去执行命令的。
众所周知我们在生成一个对象的时候,会自动执行类的静态代码块,我们的利用方式为:先写一个类,然后将恶意代码写到他的静态代码块,然后生成字节码,利用字节码动态加载创建一个对象出来。
我们先看一下 TemplatesImpl.defineTransletClasses :
可以看到他这里有注册字节码的操作,那么我们还需要一个创建对象的操作,这样我们才可以调用恶意类的静态代码块。我们在看 TemplatesImpl.getTransletInstance:
可以看到他不仅调用了defineTransletClasses,同时还有一个newInstance的操作,这就很满足我们执行恶意代码的需求。并且TemplatesImpl这个类是继承了Serializable的,他的这些属性我们也是都可以控制的:
我们在找一下,看看哪里调用了TemplatesImpl.getTransletInstance,找到了一个在TemplatesImpl.newTransformer方法:
所以我们先简单写一下通过TemplatesImpl这个类去执行命令的exp,至于属性的赋值直接反射改就行了。恶意类test.class:
1 | import com.sun.org.apache.xalan.internal.xsltc.DOM; |
生成字节码:
exp:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
在简单讲一下这个exp,有几个要注意的地方,就是我们通过反射对参数的修改的原因,首先_name参数不能为空:
_bytecodes也不能为空:
_tfactory不能为空:
还有一个要注意的点,就是我们的恶意类,为什么要继承AbstractTranslet,是因为在TemplatesImpl.defineTransletClasses方法里有一个if判断要求我们恶意类的父类等于一个常量AbstractTranslet,所以我们要继承AbstractTranslet并重写他的一些方法,如果不继承那么我们的_transletIndex就会为-1,就会导致我们进入下面的那个if,最终导致报错:
我们后半段的exp就写完了,从中可以看出我们是从调用 TemplatesImpl.newTransformer去入手来执行命令的。这个时候我们可以回想一下我们cc1-1的ChainedTransformer与InvokerTransformer,我们可以将TemplatesImpl与newTransformer传进ChainedTransformer,以此来达到调用 TemplatesImpl.newTransformer的操作。
代码:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
如图也是可以执行命令的:
所以最终exp:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
对于_tfactory这个参数其实在readObject中是赋值了的,所以如果是为了写反序列化的exp而不是为了直接调用演示的话,是可以将_tfactory通过反射赋值的那几行代码去掉的
cc3-2
这条链子其实就是yso的那条
书接上回,我们知道了我们最终rce是通过调用了TemplatesImpl.newTransformer(),那么我们在找找其他的地方看看有没有调用了newTransformer方法的类。
TrAXFilter类的构造方法中调用了newTransformer:
并且templates参数在构造方法里是我们可控的,但是这时有一个问题,就是我们的TrAXFilter类并没有继承了Serializable,对于这个问题我们同样可以像解决Runtime不能序列化那样去解决,我们可以使用TrAXFilter.class。
此时我们再接着需要找一个调用了构造方法的一个类,这里作者找的是InstantiateTransformer这个类,如图,在他的transform方法中获取了构造器调用了构造函数,并且这里的参数也是我们可控的:
我们先简单写下利用代码:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
可以看到通过instantiateTransformer.transform是可以直接执行命令的,那么后半段的exp写完了,我们在找一个调用了transform方法的地方,这里可以直接用cc1-2那条的前半段。
最终exp:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
cc4
Maven依赖,这里cc2,4都用的如下依赖,这是对commons-collections库进行的一个版本更新:
1 | <dependency><groupId>org.apache.commons</groupId><artifactId>commons-collections4</artifactId><version>4.0</version></dependency> |
cc4 最后利用的执行命令的点也是那个利用字节码的方式,就是和cc3-2的rce的点是一样的,只不过是前半段不一样。我们知道那条cc3-2 rce后半段是通过:
InstantiatTransformer.transformer->TrAXFilter.TrAXFilter->TemplatesImpl.newTransformer->defineClass->newInstance
这样去实现的,那么我们要找到一个新的调用了transformer的地方,这里是在 TransformingComparator.compare中找到的:
接着往前找 PriorityQueue.siftDownUsingComparator中又调用了compare方法:
在 PriorityQueue.siftDown中又调了siftDownUsingComparator方法:
在 PriorityQueue.heapify 中又调用了siftDown:
最后很完美,在PriorityQueue.readObject中调用了heapify:
这个时候大家可能会想到一个问题,就是为什么在commons-collections3.2.1中,这条链子不通,其实他俩的区别在TransformingComparator这个类里,在commons-collections3.2.1中,TransformingComparator这个类并没有继承
Serializable接口,而在commons-collections4中,他是继承 了Serializable接口的。
commons-collections3.2.1:
commons-collections4:
分析完以后,我们可以写出exp:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
我们再来分析一下,这个exp,首先前半段都是我们之前讲过的,就是通过字节码去执行命令,这里主要说一下两个主意的点
1是为什么要 priorityQueue.add(); 这是因为在调用heapify()时,会首先对size进行一个右移的操作:
只要size大于等于2的时候,再会调用到siftDown:
2就是我们为什么要反射修改transformingComparator的transformer属性为ChainedTransformer,而不是直接去赋值,这是因为priorityQueue.add方法最终也会调用到compare方法,这样我们的exp在本地也会直接执行,为了避免这种情况所以就先给他赋一个没用的值,这样在调用add方法的时候就不会去执行我们的exp了,最后在给他反射修改回来:
cc2
cc2和cc4呢其实区别也不是很大,最后的rce的方式也都是一样的。区别在哪呢,之前我们说过TemplatesImpl.newTransformer是可以直接进行rce的,cc2就是通过
InvokerTransformer直接去调用TemplatesImpl.newTransformer,不走InstantiateTransformer和TrAXFilter了:
简单写个exp:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
是可以执行命令的:
cc5
CC5是对CC3.1版本的利用
这条链子和我们写过的cc1-2那条,极其相似。只是入口点不一样,cc1-2用的是AnnotationInvocationHandler.invoke调用的
LazyMap.get。cc5这里用的是TiedMapEntry.toString调用的
LazyMap.get。然后我们将TiedMapEntry类再传给BadAttributeValueExpException类的val属性即可。
只有这一点区别:
TiedMapEntry.toString:
BadAttributeValueExpException.readObject:
这里的val变量是私有变量,并且通过构造函数是传不进我们想要的,他这个构造函数是会先判断是不是为空,如果不是,那么会调用toString方法并返回结果:
对于这种问题我们直接反射修改就好了,最后exp:
1 | import org.apache.commons.collections.Transformer; |
可以成功执行命令的:
cc7
先放一下调用栈:
这条链子和我们写过的cc1-2那条,也极其相似。同样也只是入口点不一样,cc1-2用的是AnnotationInvocationHandler.invoke调用的
LazyMap.get。cc7这里用的是AbstractMap.equals调用的LazyMap.get:
AbstractMap.equals,这里的m是可控的:
HashTable.reconstitutionPut 这里的key也是可控的:
HashTable.readObject中调用了reconstitutionPut:
其实到这里,我们的exp就可以写出来了,但是这里有几个问题要说一下。
1.我们在HashTable.readObject中是有条件判断的,就是说如果我们的HashTable没有一对键值,那么我们是走不到那个循环里的,就会导致我们调用不到reconstitutionPut
2.如果我们单单put了一次,就算进到了reconstitutionPut里面,也是进不到如下这个循环导致调用不到equals的,因为此时的tab[]是空的。所以我们起码要执行reconstitutionPut两次,才可以以进入到循环里面,也就是说我们要HashTable.put两次
3.此时,就算我们HashTable.put了两次以后,实际上也是调用不到equals的,这是因为对两组键的hash值进行了判断,要求相等。所以我们还要put进去的两组值的hash值一样:
但是在java中有这样的一个bug可以帮我们解决问题:
1 | "yy".hashCode()=="zZ".hashCode() |
都满足以后,下一步会开始调用LazyMap的equals方法,但是LazyMap中是没有equals方法的,但是它的父类AbstractMapDecorator有equals方法,所以就会去调用它的父类AbstractMapDecorator的equals方法:
此时我们传入一个HashMap,但是HashMap并没有equals方法,但是HashMap继承了AbstractMap,AbstractMap类中有一个equals方法,此时就会去调用AbstractMap.equals最终调用到get:
4.我们在put完以后,要lazyMap2.remove(“yy”),这是因为当调用完equals方法后,lazyMap2的key中就会增加一个yy键:
此时lazyMap1和lazyMap2中的元素个数不一样,那么在这里会直接返回false,所以我们要通过lazyMap2.remove(“yy”) 解决掉这个问题:
最终exp:
1 | import org.apache.commons.collections.Transformer; |
shiro无依赖cb
shiro环境搭建
1 | gitclone https://github.com/apache/shirocdshirogit checkout shiro-root-1.2.4 |
将 shiro/samples/web/pom.xml 中的jstl依赖改为1.2:
1 | <dependency><groupId>javax.servlet</groupId><artifactId>jstl</artifactId><version>1.2</version><scope>runtime</scope></dependency> |
点击添加配置:
添加一个tomcat服务:
在部署中选择工件:
选择这个:
最后点击启动:
搭建成功:
漏洞原理
我们先抓一下没有勾选Remember Me时的数据包:
再抓下勾选了Remember Me时的数据包:
我们对比一下可以发现,当我们勾选Remember
Me时,服务端会给我设置一个cookie值:rememberMe=xxxx,这其实就是为了在固定时间段内下一次打开网页时我们不用再输入密码,Shiro
将一些用户信息序列化并加密后保存在 Cookie 的 rememberMe 字段中,这样我们下次再登录的时候服务端就会对我们Cookie 中的
rememberMe的值进行解密并进行反序列化,以此来读取用户的信息。在 Shiro 1.2.4 版本之前内置了一个默认
Key,导致我们可以对Cookie 中的 rememberMe的值进行任意伪造,来触发反序列化漏洞。
我们可以看一下序列化代码的逻辑
首先在AbstractRememberMeManager.rememberIdentity中调用convertPrincipalsToBytes对进行序列化的数据进行加密,然后调用rememberSerializedIdentity进行base64编码:
convertPrincipalsToBytes加密:
rememberSerializedIdentity进行编码并返回:
我们再看一下进行反序列化代码的逻辑:
首先在AbstractRememberMeManager.getRememberedPrincipals中调用getRememberedSerializedIdentity对我们cookie中的rememberMe字段的值进行base64解码,然后调用convertBytesToPrincipals进行解密,解密后进行反序列化:
getRememberedSerializedIdentity:
convertBytesToPrincipals:
我们再跟进一下进行解密的函数看一下:
我们跟进一下 encrypt与decrypt都调用到了的getEncryptionCipherKey ,看一下能不能找到这个进行加解密的key,发现是一个常量:
最后一步一步找发现key是一个固定的值:
这里进行加解密的算法用的是aes,大家不熟悉的可以自己去跟一遍了解一下,在Shiro1.4.2 版本后,Shiro的加密模式AES-GCM,之前都是 AES-CBC
我们加解密的key找到了,我们就可以构造payload了。我这里在pom.xml里面放了cc的依赖:
我们这里先用最简单的URLDNS那条链来熟悉下攻击构造流程。
1 | import java.io.FileOutputStream; |
写一个加密然后编码的脚本:
1 | # pip install pycrypto |
生成payload:
这里有个要注意的地方就是,我们替换完我们的payload以后,要把cookie中的JSESSIONID字段删掉,要不然他是不会读我们rememberMe字段的内容的,而是直接通过JSESSIONID的值来辨认我们的身份:
我们删掉JSESSIONID然后发送一下数据包:
shiro重写反序列化函数导致的一些问题
在上面,我们使用了urldns那条链子进行测试,但是我们最终的目标是要进行rce。有人会想,既然有了反序列化的点,直接找相关依赖打不就可以了么,这其实是不行的,拿我们的cc那几条链来举例子,其实只有cc2可以打。这是为什么?
这是因为我们的shiro框架重写了反序列化的函数自定义了反序列化的方式。
我们先添加完cc的依赖,随便拿一条链子出来打:
我这里拿的是cc6:
看一下日志:
其实这里就是比较奇怪的,为什么别的类都可以加载到,这个类他加载不到?我们去分析一下他进行反序列化的那里:
我们可以看到他并没有调用我们常用的ObjectInputStream的readObject方法进行反序列化,而是使用它自定义的一个对象输入流ClassResolvingObjectInputStream类的readObject方法。
我们看一下ClassResolvingObjectInputStream类,他这里就定义了两个方法,一个构造方法,一个resolveClass方法:
resolveClass方法是什么呢?了解过java的类加载机制的朋友一定都熟悉这个,在我们掉用原生jdk的反序列化方法时,会创建对象,同时也会进行类加加载,从而调用resolveClass,但是我们这里重写了resolveClass方法,那么它就会调用这个重写的resolveClass方法。我们对比一下原本的resolveClass方法与重写的resolveClass方法。
原生的:
重写的:
通过对比可以发现一个是通过Class.forName进行类加载,一个是调用了各种Classloader的loadClass方法进行加载。
可以看下forName与loadClass的一个区别:一个支持数组,一个不支持数组
所以说简单点我们只能用cc2那条链子打的原因就是shiro自定义的resolveClass方法中的loadClass方法不支持数组,而我们自己构造的那几个链子都是有transform数组类的。所以我们想要是能打通的话我们就要给他改下一下,让他不出现transform数组类。
exp构造
我们之前分析过cc的几条链子,我们看看怎么用他们拼出一个不出现数组的链。
在学习我们的cc链的时候,我们知道最后执行命令的方式,无非就是两种,一种是通过Runtime.exec,一种是通过TemplatesImpl.newTransform,要走Runtime的话,如下图,就必须要走好几个InvokerTransform这种循环调用,所以这种我们不能用。
这里的基于commons-collections3版本的exp其实就是cc2和cc3结合的半部分在加上cc6的半部分。调用顺序是这样的:
1 | HashMap.readObject->TiedMapEnty.hashCode->LazyMap.get->InvokeTransformer.transform->TemplatesImpl.newTransform->defineClass.newInstance |
因为之前分析过,所以这里就放两张图吧:
最终exp:
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
对生成的payload加密发送,成功执行命令:
无依赖CB链
shiro默认是没有cc依赖的,但是存在commons-beanutils 1.8.3依赖,我们之前的那个cc库是对java中集合的一个功能增强,而这个cb是对javabean的一个增强。
什么是javaBean呢?
如图 是一个通过get方法获取JavaBean对象的属性值的例子:
利用CB库,我们可以使用更好的方法:
他这里的原理其实也是调用了我们的getAge与getName方法,因为javaBean都是有固定格式的,所以它会根据我们传进去的参数名去调用相对应的get方法。
我们之前在调试cc3的时候知道, 调用到TemplatesImpl.newTransformer是可以进行动态加载类去命令执行的。
而在TemplatesImpl类中的getOutputProperties方法中,调用到了newTransformer方法的,如图:
所以我们对一个TemplatesImpl对象调用其getOutputProperties方法,也是可以进行动态类加载进行命令执行的,我们细看getOutputProperties方法的格式,它是符合一个javaBean的格式的。
所以我们先写一个payload出来:
1 | import java.io.FileOutputStream; |
是可以RCE的:
我们再来找一下,看看哪里调用了PropertyUtils.getProperty方法,最后找到了一处是在BeanComparator.compare 中,并且参数都可控。:
并且这个compore方法在我们之前学过的cc2那条链子中是用过的,所以其实这里的前半段就可以直接去用cc2那条链子的:
先简单写一个exp:
1 | import java.io.FileOutputStream; |
打一下看一下是可以执行命令的:
但是这里其实是有一个问题的,我们这里将cc的依赖都删掉,重新打一下,发现并没有打通,我们这里看下日志:
可以看到他这里报的错是找不到cc依赖里的一个类,我们这里就会感觉到有点诧异,明明用的cb依赖,为什么还报找不到cc依赖的错,我们看这里:
如上图可知他是在BeanComparator类的构造函数中出现了一个ComparableComparator,而这个ComparableComparator实际上是cc中的:
那这里怎么解决呢?我们可以利用它的另一个构造函数进行创建对象,参数自己随便找一个符合要求的cb里带的类传一下就好了:
生成一下payload测试一下:
如图,成功利用完全原生的依赖完成rce。