从源码diff分析Apache-Shiro 1.7.1版本的auth bypass(CVE-2021-41303))

0x00 没事找事

最近有点茫(mang),写了几个月的CRUD代码,研究安全的兴趣越来越低了。恰巧昨晚主管发了个shiro邮件截图,看内容描述,1.8.0修复了一个auth的bypass,一开始,惯性思维认为“只有最早的两个auth bypass比较有用”,觉得后面的好几个CVE都是需要条件的鸡肋洞,索然无味。但群友好像比较感兴趣,故撩起了我的一丝兴趣。

image

0x01 简单分析

问了下赛博群友们拿链接,勃勃给了个1.7.1&1.8.0的diff链接,手边没电脑,所以手机看了下,发现没有太多的code changes,可疑的地方只有两个,一个是对小写jsessionid参数提取的支持(因为shiro认证成功后的set-cookies name就是JSESSIONID,但有的时候,比如SSO站点和业务站点domain不一样,不能直接set-cookie,需要用redirect的方式把JSESSIONID带在URL重定向,但可能一些网关或者什么的原因,导致redirect后JSESSIONID变成了小写的jsessionid),另一个是filterChainManager.proxy方法的参数变动。

  1. 小写jsessionid参数提取的支持

image

  1. filterChainManager.proxy方法的参数变动

image

翻到最后其实可以看到两个testcase的改动,以宝友多年的经验看,这个参数的变动处,极度可疑。

image

其实这里有个jira的链接,进去看了下内容,发现是一个bug,会导致一些请求抛异常。

通过简单的测试,按照testcase的条件,去配置filter chain,会发现确实存在bug,导致接口不可访问了。

image

image

image

结论:这里的改动,主要修复了一个导致抛异常的bug

0x02 深入分析

到这里有个疑惑,主要的代码变动是为了修一个导致抛异常的bug,那么,auth bypass的bug在哪里呢?

这里想起了昨晚勃勃分析的时候,说的一个现象,就是这两个testcase在1.7.1以前的版本能pass,1.8.0版本也能pass,思考了一下,是不是意味着,在1.7.1版本的迭代,导致出现了新的bug?

通过比较1.7.1的代码和1.7.1之前的代码,可以发现,PathMatchingFilterChainResolver类的getChain方法,确实有点不太一样。

  • 1.7.1之前版本:

image

  • 1.7.1版本:

image

结果很明显了,1.7.1把外部可控的requestURINoTrailingSlash(requestURI去掉了/后缀)带入到了

1
filterChainManager.proxy(originalChain, requestURINoTrailingSlash)

导致了match时的pathPattern,和实际进入proxy的不一样,比如:

1
2
3
URI:/bypass/threedr3am/index

pathPattern:/bypass/*/index

但这里,细心看逻辑,可以发现,如果以上面的例子去访问的话,是无法进到这个else的代码中的,因为requestURI和pathPattern直接就匹配上了,直接进入到了if,就无从谈利用了。

恰巧的是,如果我们在URI后多加一个/,就能让requestURI和pathPattern匹配不上,直接进入else,并且能在else中的if使其pathPattern和requestURINoTrailingSlash成功匹配上:

1
2
3
URI:/bypass/threedr3am/index/

pathPattern:/bypass/*/index

然后进入filterChainManager.proxy(originalChain, requestURINoTrailingSlash),但进入里面之后,会发现抛异常了,就像jira中的issue描述一模一样。通过分析报错栈信息,可以发现是因为传入的requestURINoTrailingSlash无法

在鉴权配置map中找到(字符串比较)对应的value数据,导致抛异常,在1.7.1版本前的代码,传入的是pathPattern,这个字符串是鉴权配置map中的key,自然能顺利找到对应的value数据,而1.7.1变成了requestURI去掉/后缀的requestURINoTrailingSlash,那除非访问的URI是/bypass/*/index/,不然的话,肯定是无法找到对应的数据的,抛异常那就自然而然了。

回到鉴权配置,我看到代码中使用的是LinkedHashMap,这是一个有序的HashMap,有人会疑惑,HashMap为什么会有序,实际上LinkedHashMap内部维护了一个链表,把元素添加时的顺序维护起来了。

image
image

这里我认为shiro的认证鉴权会根据配置的先后顺序去依次实施,但这个只是我创建实例的选择,理论上我可以配置成TreeMap、HashMap等等,那有序可能就不是必然了。

再看向DefaultFilterChainManager#proxy -> getChain -> filterChains

image
image
image

从这里的代码上,我可以初步确定,在大部分情况下,filter chains是有序的。

那么,转换到开发思维,如果存在这样的一个鉴权配置:

image

在1.7.1以前的版本,根据鉴权顺序,我通过以下请求进行访问,将会抛出401,提示我进行认证才能访问,因为/bypass/*/index这个authc filter进行了处理:

1
2
3
URI:/bypass/threedr3am/index/

pathPattern:/bypass/*/index

但在1.7.1版本中,根据鉴权顺序,我通过以下请求进行访问,它虽然匹配上了/bypass/*/index这个pathPattern的authc filter,但真正进入到filterChainManager#proxy的参数是被去掉了/后缀的/bypass/threedr3am/index,导致在DefaultFilterChainManager#proxy -> getChain中获取到的filter是(“/bypass/threedr3am/index”,”anon”),进而顺利的bypass auth访问到了接口。

image

0x03 结论(我不是100%确认,大概99%确认吧)

  1. 这个auth pass是1.7.1版本特有

  2. 必须存在这样的配置(格式、顺序)

image

  1. 如果是这个洞的话,限制有点大