Shiro550+CB
通过Shiro550学习CB无依赖
Shiro550漏洞分析
前言
Shiro 是一个Java的安全框架,它包含身份认证、登录、验证权限、会话管理等功能。
Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie,在服务端接收cookie值后,Base64解码–>AES解密–>反序列化。攻击者只要找到AES加密的密钥,就可以构造一个恶意对象,对其进行序列化–>AES加密–>Base64编码,然后将其作为cookie的rememberMe字段发送,Shiro将rememberMe进行解密并且反序列化,最终造成反序列化漏洞。
环境搭建
流程很简单,照着参考文章做几分钟就搭起来了。
漏洞分析
前置知识
RememberMeManager
org.apache.shiro.mgt.RememberMeManager
,这个接口提供了如下 5 个方法:
getRememberedPrincipals
在指定上下文中找到记住的 principals,也就是 RememberMe 的功能。forgetIdentity
:忘记身份标识。onSuccessfulLogin
:在登陆校验成功后调用,登陆成功时,保存对应的 principals 供程序未来进行访问。onFailedLogin
:在登陆失败后调用,登陆失败时,在程序中“忘记”该 Subject 对应的 principals。onLogout
: 在用户退出时调用,当一个 Subject 注销时,在程序中“忘记”该 Subject 对应的 principals。
AbstractRememberMeManager
AbstractRememberMeManager
是一个Shiro自带的抽象类它实现了RememberMemanager
接口,先了解一下这个类中几个重要的成员变量。
DEFAULT_CIPHER_KEY_BYTES
:一个 Base64 的硬编码的 AES Key,也是本次漏洞的关键点,这个 key 会被同时设置为加解密 key 成员变量:encryptionCipherKey/decryptionCipherKey 。serializer
:Shiro 提供的序列化器,用来对序列化和反序列化标识用户身份的 PrincipalCollection 对象。cipherService
:用来对数据加解密的类,实际上是org.apache.shiro.crypto.AesCipherService
类,这是一个对称加密的实现,所以加解密的 key 是使用了同一个。
通过其构造方法可以看到,这个类在在初始化时会创建DefaultSerializer
作为序列化器,AesCipherService
作为加解密实现类,DEFAULT_CIPHER_KEY_BYTES
作为加解密的 key。
CookieRememberMeManager
CookieRememberMeManager
继承了AbstractRememberMeManager
这个抽象类,而且它实现了在HTTP无状态协议中使用cookie记录用户信息的相关能力。其中我们需要注意的方法是getRememberedSerializedIdentity
,它的逻辑大概如下(获取Cookie中的内容并Base64解码返回byte数组):
漏洞点
这个方法就是我们整个Shiro550漏洞点,我们先简单看看它的逻辑:
通过前置知识我们知道,在CookieRememberMeManager
对 getRememberedSerializedIdentity
的实现是获取 Cookie 并 Base64 解码。
然后将解码后的 byte 数组传入 convertBytesToPrincipals
。这个方法执行了两个操作:decrypt
和 deserialize
。
1.decrypt:
通过我们之前对AbstractRememberMeManager
类中的成员变量的解释。cipherService
是用来对数据加解密的类,实际上是 org.apache.shiro.crypto.AesCipherService
类,这是一个对称加密的实现,所以加解密的 key 是使用了同一个。
所以我们可以知道整个decrypt就是相当于用AesCipherService
进行解密,最后返回解密后的数组。
2.deserialize
deserialize
调用 this.serializer#deserialize
方法反序列化解密后的数据。
在 Shiro 中,序列化器的默认实现是 DefaultSerializer,可以看到其 deserialize
方法使用 Java 原生反序列化,使用 ByteArrayInputStream 将 byte 转为 ObjectInputStream ,并调用 readObject
方法执行反序列化操作。反序列化得到的 PrincipalCollection 会被 set 到 SubjectContext 供后续的校验调用。
以上就是 Shiro 创建 Subject 时执行的逻辑,跟下来后就看到了完整的漏洞触发链:攻击者构造恶意的反序列化数据,使用硬编码的 AES 加密,然后 Base64 编码放在 Cookie 中,即可触发漏洞利用。
CB链
为什么要在Shiro中介绍CB呢?这是因为Shiro本身就依赖commons-beanutils
组件。其实CC和CB组件在底层还是有很多关联的(比如我们稍后会分析到的BeanComparator)。所以有的文章会给Shiro加上commons-collections
,来打Shiro的cc依赖,其实就是C2+C3+C6部分关键代码排列组合。我们本篇主要介绍Shiro无依赖的CB链。
前置知识
什么是JavaBean???
JavaBean是符合如下标准的Java类:
- 类是公共的
- 有一个无参公共的构造方法
- 有私有属性
- 有对应公共的getter、setter
比如:
1 |
|
BeanComparator
BeanComparator 是 commons-beanutils 提供的用来比较两个 JavaBean 是否相等的类,其实现了java.util.Comparator
接口。
BeanComparator 在初始化时可以指定 property 属性名称和 comparator 对比器,如果不指定,则默认是 ComparableComparator 。
BeanComparator 的 compare 方法接收两个对象,分别调用 PropertyUtils.getProperty()
方法获取两个对象的 property 属性的值,然后调用 internalCompare()
方法调用实例化时初始化的 comparator 的 compare 方法进行比较。
第二个参数(comparator)可以传入实现Comparator
且可序列化的任意类
PropertyUtils.getProperty()
1 |
|
在commons-beanutils中提供了一个静态方法PropertyUtils.getProperty()
它可以直接调用任意JavaBean的getter方法。它接收两个参数:一个是JavaBean的实例对象,一个是JavaBean的属性。
举个栗子:
1 |
|
分析过程
TemplatesImpl 类位于com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,实现了 Serializable
接口,因此它可以被序列化,我们来看一下漏洞触发点。
首先我们注意到该类中存在一个成员属性 _class
,是一个 Class 类型的数组,数组里下标为_transletIndex
的类会在 getTransletInstance()
方法中使用 newInstance()
实例化。
注:
- 首先得保证_name不等于null,否则会直接返回null。
- 其次需要保证_class等于null,因为我们要调用
defineTransletClasses()
- 其次我们注意到, _ class是成员属性,他是一个Class类型的数组,数组下标为
_transletIndex
的类会在getTransletInstance()
方法中使用newInstance()
实例化
而类中的 getOutputProperties()
方法调用 newTransformer()
方法,而 newTransformer()
又调用了 getTransletInstance()
方法。
而 getOutputProperties()
方法就是类成员变量 _outputProperties
的 getter 方法。
这就给了我们调用链,那 _class
中的类是否可控呢?看一下调用,发现在 readObject
、构造方法以及 defineTransletClasses()
中有赋值的动作。
其中 defineTransletClasses()
在 getTransletInstance()
中,如果 _class
不为空即会被调用,看一下 defineTransletClasses()
的逻辑(也就是我们上面蓝色部分的第二步):
代码功能解释:
- 检查
_bytecodes
是否为空,为空则抛出异常 - 接着就会调用自定义的ClassLoader去加载
_bytecodes
中的byte[]。而_bytecodes也是该类的成员属性。 - 如果这个类的父类为
ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,就会将类成员属性的,_transletIndex
设置为当前循环中的标记位,而如果是第一次调用,就是_class[0]
。如果父类不是这个类,将会抛出异常。
所以我们只需要构造一个 TemplatesImpl 类的反序列化字符串,其中 _bytecodes
是我们构造的恶意类的类字节码,这个类的父类是 AbstractTranslet,最终这个类会被加载并使用 newInstance()
实例化。再串到上面的compare中,进行间接调用_outputProperties()
的getter方法最终形成调用。
但是我们还需要满足几个条件:
_name
不为null(很简单,随便传String类型的字符串即可)_tfactory
不为null(直接传new TransformerFactoryImpl()
,因为在其readObject里有对_tfactory赋值,所以直接拿来用即可)
那么好,我们先梳理一下现在的链条
1 |
|
而 CommonsBeanutils 利用链中核心的触发位置就是 BeanComparator.compare()
函数,当调用 BeanComparator.compare()
函数时,其内部会调用我们前面说的 getProperty
函数,进而调用 JavaBean 中对应属性的 getter 函数。
这里会调用PropertyUtils.getProperty()
方法 因此通过给 o1赋值构造好的templates对象,property赋值为TemplatesImpl的 outputProperties属性,即可调用 TemplatesImpl.getOutputProperties()
往下就是TemplatesImpl的利用链
那么往上找 哪里调用 compare()呢?可以利用CC2/4链中用的 PriorityQueue.readObject()
结果展示
代码
1 |
|
Test.java
1 |
|
链条
1 |
|