从0-1的Shiro rememberMe 反序列化分析

作者: print("") 分类: Java学习 发布时间: 2021-11-18 23:50

第一次分析Java的代码。错误的请大家见谅

一、环境准备

    首先需要安装IDEA

随便那个那边都可以了。然后下载一个tomcat 

具体怎么配置tomat 百度吧。

Shiro 代码  【p神是永远的神】

https://github.com/phith0n/JavaThings/tree/master/shirodemo

下载完成之后。解压即可。

选择你下载的文件目录

打开这个pom.xml 、然后进行等待即可。会自动下载包的。

设置一下tomcat  如果你需要设置其他端口就换成其他端口。不需要则不需要动

然后选择debug 启动

启动之后。首先需要看看能否访问

启动成功之后,是可以打开登陆页面的。

二、代码调试

首先要知道shiro是一个用来做身份验证的框架,其原理是基于servlet的filter进行的。在web.xml中定义了ShiroFilter,作用范围是当前目录下所有的url

  <listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
  </listener>

  <filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

漏洞原理:

Apache Shiro框架提供了记住我的功能(RememberMe),用户登陆成功后会生成经过加密并编码的cookie。cookie的key为RememberMe,

cookie的值是经过对相关信息进行序列化,然后使用aes加密,最后在使用base64编码处理形成的。


rememberMe生成过程:

       序列化 》AES加密》Base64 编码》 生成rememberMe内容


服务端接收cookie值时:

      检索cookie中的rememberMe内容 》 Base64 解密》 AES解密 (加密密钥硬编码)》反序列化(未作过滤处理)


AES的加密密钥在shiro的1.2.4之前版本中使用的是硬编码,其默认密钥的base64编码后的值可在代码中找到。只要找到密钥后就可以通过构造恶意的序列化对象进行编码,

加密,然后作为cookie加密发送,服务端接收后会解密并触发反序列化漏洞

首先使用Xray 扫描一下得到结果

验证Key

GET /shirodemo_war/login.jsp HTTP/1.1
Host: 192.168.0.11:8081
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: rememberMe=rk0ArlKoRNWDhClOxAZyFEK7LO14GfjNLFXhDNErl7LTXu3D2Ne/HCOyNYciJ7RMntXtKBZEHBIvlh2tZ4JUZvXlutMbY24GEp0SwFkYZBB/IWDSJWjzr/3WZN+/1IqJzhSwQ1r9Pcy/EuoDk0dXMabi08nrY1T+VH34K3L8r+mmsLvxfdDEVPPoRtDn5nLF
Connection: close

三、代码断点

1. 加密

 序列化 》AES加密》Base64 编码》 生成rememberMe内容

处理Cookie的类是CookieRememberMeManaer,该类继承AbstractRememberMeManager类,跟进AbstractRememberMeManager类,很容易看到AES的key。

断点打AbstractRememberMeManager类的onSuccessfulLogin

用户名root 密码secret。选择Remember me 然后段点进行跟踪 

首先是onSuccessfulLogin 后面进入到rememberIdentity  –>convertPrincipalsToBytes –>encrypt

主要的函数在convertPrincipalsToBytes 。首先是进行序列化,然后进行AES加密

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = serialize(principals); // 进行序列化
        if (getCipherService() != null) {
            bytes = encrypt(bytes); // AES加密
        }
        return bytes;
    }

encrypt 函数

    protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        CipherService cipherService = this.getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
            value = byteSource.getBytes();
        }

        return value;
    }

进行AES加密,利用arraycopy()方法将随机的16字节IV放到序列化后的数据前面,完成后再进行AES加密。  秘钥为Base64.decode(“kPH+bIxk5D2deZiIxcaaaA==”);

AES操作在 JcaCipherService类的encrypt

    private ByteSource encrypt(byte[] plaintext, byte[] key, byte[] iv, boolean prependIv) throws CryptoException {
        int MODE = true;
        byte[] output;
        if (prependIv && iv != null && iv.length > 0) {
            byte[] encrypted = this.crypt(plaintext, key, iv, 1);
            output = new byte[iv.length + encrypted.length];
            System.arraycopy(iv, 0, output, 0, iv.length);
            System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
        } else {
            output = this.crypt(plaintext, key, iv, 1);
        }

        if (log.isTraceEnabled()) {
            log.trace("Incoming plaintext of size " + (plaintext != null ? plaintext.length : 0) + ".  Ciphertext " + "byte array is size " + (output != null ? output.length : 0));
        }

        return Util.bytes(output);
    }

最后在CookieRememberMeManager类的rememberSerializedIdentity()方法中进行base64加密:

    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
        if (!WebUtils.isHttp(subject)) {
            if (log.isDebugEnabled()) {
                String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

        } else {
            HttpServletRequest request = WebUtils.getHttpRequest(subject);
            HttpServletResponse response = WebUtils.getHttpResponse(subject);
            String base64 = Base64.encodeToString(serialized);
            Cookie template = this.getCookie();
            Cookie cookie = new SimpleCookie(template);
            cookie.setValue(base64);
            cookie.saveTo(request, response);
        }
    }

2、解密


检索cookie中的rememberMe内容 –> Base64 解密 –> AES解密 (加密密钥硬编码) —>反序列化(未作过滤处理)

有了AES的key、加密模式AES/CBC/PKCS5Padding,由于AES是对称加密,所以我们已经可以解密AES的密文了。

第一步:获取rememberMe的Cookie

第二步:base64解码。CookieRememberMeManager类的getRememberedSerializedIdentity()方法

    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
        if (!WebUtils.isHttp(subjectContext)) {
            if (log.isDebugEnabled()) {
                String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }

            return null;
        } else {
            WebSubjectContext wsc = (WebSubjectContext)subjectContext;
            if (this.isIdentityRemoved(wsc)) {
                return null;
            } else {
                HttpServletRequest request = WebUtils.getHttpRequest(wsc);
                HttpServletResponse response = WebUtils.getHttpResponse(wsc);
                String base64 = this.getCookie().readValue(request, response);
                if ("deleteMe".equals(base64)) {
                    return null;
                } else if (base64 != null) {
                    base64 = this.ensurePadding(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Acquired Base64 encoded identity [" + base64 + "]");
                    }

                    byte[] decoded = Base64.decode(base64);
                    if (log.isTraceEnabled()) {
                        log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
                    }

                    return decoded;
                } else {
                    return null;
                }
            }
        }
    }

解密出Base64

第三步解密出AES,

解密完之后会到AbstractRememberMeManager 类的getRememberedPrincipals 函数

关键函数。

    public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
        PrincipalCollection principals = null;

        try {
            byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
            if (bytes != null && bytes.length > 0) {
                principals = this.convertBytesToPrincipals(bytes, subjectContext);
            }
        } catch (RuntimeException var4) {
            principals = this.onRememberedPrincipalFailure(var4, subjectContext);
        }

        return principals;
    }

进入到convertBytesToPrincipals

    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
        if (this.getCipherService() != null) {
            bytes = this.decrypt(bytes); //AES 解密
        }

        return this.deserialize(bytes);  //反序列化
    }

第四步 反序列化

    protected PrincipalCollection deserialize(byte[] serializedIdentity) {
        return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
    }
    public T deserialize(byte[] serialized) throws SerializationException {
        if (serialized == null) {
            String msg = "argument cannot be null.";
            throw new IllegalArgumentException(msg);
        } else {
            ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
            BufferedInputStream bis = new BufferedInputStream(bais);

            try {
                ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
                T deserialized = ois.readObject();
                ois.close();
                return deserialized;
            } catch (Exception var6) {
                String msg = "Unable to deserialze argument byte array.";
                throw new SerializationException(msg, var6);
            }
        }
    }

因为是知道为AES 加密的方式和CBC的模式,外部的Cookie 可以控制。然后只需要爆破出Key    就能进行反序列化操作。对于xray 的命令执行的操作。下次研究一下。

参考:

https://y4er.com/post/shiro-rememberme-rce/

https://xz.aliyun.com/t/8445

https://xz.aliyun.com/t/6493

https://xz.aliyun.com/t/7207

https://paper.seebug.org/shiro-rememberme-1-2-4/

https://www.cnblogs.com/loong-hon/p/10619616.html

https://www.bilibili.com/read/cv9949257/

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

您的电子邮箱地址不会被公开。