长亭百川云 - 文章详情

一种另类的 shiro 检测方式

CT Stack

20

2022-03-18

0x01 前言

shiro 这玩意今年出现在大众视野里,众多师傅大喊hvv没有shiro不会玩,实际上追溯这个洞最早开始时候是2016年的事情了,也就是说因为某些攻防演练,这个洞火了起来,当然我也聊一点不一样东西,因为其他东西师傅们都玩出花了。

0x02 过程

首先判断 shirokey 这个过程,我之前采用的逻辑就是 YSOURLDNS 针对 dnslog 进行处理,如果没有 dnslog 的情况下,考虑直接用CC盲打,判断延迟。这种会存在一些小问题,比如当这个 shiro 没有 dnslog ,且 gadget 不是CC的情况下,可能就会漏过一些漏洞。

大家判断是否是 shiro 的逻辑,普遍都是在 requestcookie 中写入 rememberMe=1 ,然后再来看 responseset-cookie 是否出现的 rememberMe=deleteMe 。下文就针对这个 rememberMe=deleteMe 进行深入研究,看看为啥会这样。

网上已经有很多文章,包括我自己树立了一遍 shiro 反序列化的整个过程,这里就不多赘述,核心点在 AbstractRememberMeManager#getRememberedPrincipals 这段代码中。

1    public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
2        PrincipalCollection principals = null;
3
4        try {
5            byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
6            if (bytes != null && bytes.length > 0) {
7                principals = this.convertBytesToPrincipals(bytes, subjectContext);
8            }
9        } catch (RuntimeException var4) {
10            principals = this.onRememberedPrincipalFailure(var4, subjectContext);
11        }
12
13        return principals;
14    }

好了,下面我们分别来看两种情况。

1、key不正确的情况

当key错误的时候,我们知道 AbstractRememberMeManager#decrypt 是处理解密的过程。

1    protected byte[] decrypt(byte[] encrypted) {
2        byte[] serialized = encrypted;
3        CipherService cipherService = this.getCipherService();
4        if (cipherService != null) {
5            ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
6            serialized = byteSource.getBytes();
7        }
8
9        return serialized;
10    }

这里代码会进入cipherService.decrypt(encrypted, this.getDecryptionCipherKey());进行处理,由于key错误自然是解不出自己想要的内容,所以进入到 JcaCipherService#crypt(Cipher cipher, byte[] bytes) 这里会抛出异常。

image-20200721113728646

这里抛出异常之后,自然会进入到我们最开始核心点 AbstractRememberMeManager#getRememberedPrincipalscatch 异常捕获的逻辑当中,别急,先慢慢品一下这个。

1catch (RuntimeException var4) {
2            principals = this.onRememberedPrincipalFailure(var4, subjectContext);
3        }

跟进去 onRememberedPrincipalFailure 方法,这里代码就4行,不多赘述继续跟进 forgetIdentity 方法。

1    protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) {
2        if (log.isDebugEnabled()) {
3            log.debug("There was a failure while trying to retrieve remembered principals.  This could be due to a configuration problem or corrupted principals.  This could also be due to a recently changed encryption key.  The remembered identity will be forgotten and not used for this request.", e);
4        }
5
6        this.forgetIdentity(context);
7        throw e;
8    }

forgetIdentity 方法当中从 subjectContext 对象获取 requestresponse ,继续由forgetIdentity(HttpServletRequest request, HttpServletResponse response) 这个构造方法处理。

    public void forgetIdentity(SubjectContext subjectContext) {
        if (WebUtils.isHttp(subjectContext)) {
            HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
            HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
            forgetIdentity(request, response);
        }
    }

跟进forgetIdentity(HttpServletRequest request, HttpServletResponse response) ,看到一个 removeFrom 方法。

1    private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
2        getCookie().removeFrom(request, response);
3    }

继续跟进 removeFrom 方法,发现了给我们的 Cookie 增加 deleteMe 字段的位置了。

1    public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
2        String name = getName();
3        String value = DELETED_COOKIE_VALUE;                    //deleteMe
4        String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
5        String domain = getDomain();
6        String path = calculatePath(request);
7        int maxAge = 0; //always zero for deletion
8        int version = getVersion();
9        boolean secure = isSecure();
10        boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all
11
12        addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
13

2、反序列化gadget

还有一种情况,大家用反序列化 gadget 生成之后,拿shiro加密算法进行加密,但是最后依然在 response 里面携带了rememberMe=deleteMe

image-20200721134734696

这里再来品一下,还是回到 AbstractRememberMeManager#convertBytesToPrincipals 方法当中,这里的key肯定是正确的,所以经过 decrypt 处理之后返回 bytes 数组,进入了 deserialize 方法进行反序列化处理。

1    protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
2        if (this.getCipherService() != null) {
3            bytes = this.decrypt(bytes);
4        }
5
6        return this.deserialize(bytes);
7    }

跟进 deserialize 方法,下面重点来了。

1    protected PrincipalCollection deserialize(byte[] serializedIdentity) {
2        return (PrincipalCollection)this.getSerializer().deserialize(serializedIdentity);
3    }

反序列化的 gadget 实际上并不是继承了 PrincipalCollection ,所以这里进行类型转换会报错。

image-20200721135854719

但是在做类型转换之前,先进入了 DefaultSerializer#deserialize 进行反序列化处理,等处理结束返回 deserialized 时候,进行类型转换自然又回到了上面提到的类型转换异常,我们 key 不正确的情况下的 catch 异常捕获的逻辑里,后面的流程就和上述一样了。

image-20200721140044707

image-20200721140247649

0x03 构造

那么总结一下上面的两种情况,要想达到只依赖shiro自身进行key检测,只需要满足两点:

1.构造一个继承 PrincipalCollection 的序列化对象。

2.key正确情况下不返回 deleteMe ,key错误情况下返回 deleteMe

基于这两个条件下 SimplePrincipalCollection 这个类自然就出现了,这个类可被序列化,继承了 PrincipalCollection

image-20200721140626782

构造POC实际上也很简单,构造一个这个空对象也是可以达到效果的。

1        SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
2        ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream("payload"));
3        obj.writeObject(simplePrincipalCollection);
4        obj.close();

key正确:

image-20200721141108560

key错误:

image-20200721141256100

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2