Apache Shiro爆出不少漏洞,之前一直忙着其他事情,没有去跟进分析,这次总算凑出时间来写一篇分析文章。
万变不离其宗,我们就从旧的版本开始分析。
0x01 环境搭建
首先是环境的搭建,我直接放到github上,供大家下载。https://github.com/apwgss/Apache-Shiro-one.git
0x02 初始化分析
搭建好环境后找到ShiroConfig文件,这个是shiro的核心配置类,里面配置了那些路径需要登陆才能访问,如图
这里简单解释一下,authMap里面的key就是拦截的路径,value代表需要哪些权限,权限我在注释里写的很清楚了,就不多说了。这里主要就是设置要拦截的路径,然后设置到过滤器链中,在后面进行权限校验的时候会取出来和requestURI做匹配。
我觉得如果只是简单复现跟一下源码,那么就和网上的那些没有什么区别了,对你也不会有太大帮助,所以我准备从过滤器链如何初始化到如何绕过过滤器链的顺序去讲解该漏洞,帮助你更好的理解漏洞的前因后果。
首先我们在ShiroFilterFactoryBean中的getObject()方法打上断点,如图
因为ShiroFilterFactoryBean实现了FactoryBean接口,Spring在初始化的时候会调用ShiroFilterFactoryBean的getObject()来获取实例,而ShiroFilterFactoryBean也在此时做了一系列初始化操作。
以debug的模式启动程序,启动后会走到getObject这个方法,一开始instance为null,所以会调用this.createInstance()进行一系列初始化操作,我们主要就跟进这个方法来看一下,如图
可以看到createInstance方法中做了一系列操作,我们一步一步来看。这里的SecurityManager就是我们ShiroConfig配置类中设置的默认安全管理器。我们往下走直接走到this.createFilterChainManager()方法中,这个方法中做了一些关键的初始化操作,直接把代码贴出来
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters();
Iterator var3 = defaultFilters.values().iterator();
while(var3.hasNext()) {
Filter filter = (Filter)var3.next();
this.applyGlobalPropertiesIfNecessary(filter);
}
Map<String, Filter> filters = this.getFilters();
String name;
Filter filter;
if (!CollectionUtils.isEmpty(filters)) {
for(Iterator var10 = filters.entrySet().iterator(); var10.hasNext(); manager.addFilter(name, filter, false)) {
Entry<String, Filter> entry = (Entry)var10.next();
name = (String)entry.getKey();
filter = (Filter)entry.getValue();
this.applyGlobalPropertiesIfNecessary(filter);
if (filter instanceof Nameable) {
((Nameable)filter).setName(name);
}
}
}
Map<String, String> chains = this.getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
Iterator var12 = chains.entrySet().iterator();
while(var12.hasNext()) {
Entry<String, String> entry = (Entry)var12.next();
String url = (String)entry.getKey();
String chainDefinition = (String)entry.getValue();
manager.createChain(url, chainDefinition);
}
}
return manager;
}
这里先是new了一个DefaultFilterChainManager对象,使用的是无参的构造方法,我们进去看一下,如图
看到调用了addDefaultFilters(false)方法,点进去可以看到,该方法中获取了DefaultFilter里面的值然后遍历添加到DefaultFilterChainManager类中去了,点进DefaultFilter,该类是一个枚举类,里面定义了Shiro内置的一些过滤器,如图
标注的是两个常用的类型,日常生活中用的最多的过滤器。然后回到createFilterChainManager方法中,继续往下走,我们的关注点在这一段代码
Map<String, String> chains = this.getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
Iterator var12 = chains.entrySet().iterator();
while(var12.hasNext()) {
Entry<String, String> entry = (Entry)var12.next();
String url = (String)entry.getKey();
String chainDefinition = (String)entry.getValue();
manager.createChain(url, chainDefinition);
}
}
这里会获取我们在ShiroConfig中配置的过滤规则,也就是获取authMap,之前是set进去的现在取出来。拿到map集合判断不为空,进行迭代器遍历,获取对应的key和value,如图
然后我们跟进createChain方法,这里主要关注this.addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);方法,如图
跟进去如图
这里重点关注this.ensureChain(chainName);方法,点进去如图
这里this.filterChains默认里面是找不到chainName对应的对象的,所以会创建一个新的SimpleNamedFilterList对象,然后将chainName和chain存入this.filterChains集合中,跟到这里初始化就差不多可以告一段落了,因为验证漏洞时就是从this.filterChains中获取chainName来和requestURI做对比的,后面直接放行即可。
0x03 漏洞分析
现在我们开始真正的漏洞分析,刚刚程序已经是以debug模式启动的了,我们需要在PathMatchingFilterChainResolver文件中的getChain方法上打上断点,因为HTTP请求是从Tomcat过来的,当一个请求到达Tomcat时,Tomcat以责任链的形式调用了一系列Filter,OncePerRequestFilter就是众多Filter中的一个。它所实现的doFilter()方法调用了自身的抽象方法doFilterInternal(),这个方法在它的子类AbstractShiroFilter中被实现了。
PathMatchingFilterChainResolver.getChain()就是被在doFilterInternal()中被一步步调用的调用的。
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,
final FilterChain chain) throws ServletException, IOException {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
}
这里先获获取滤器,然后执行。
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
获取过滤器方法如下。
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
return origChain;
}
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
chain = resolved;
} else {
}
return chain;
}
通过getFilterChainResolver()就拿到了上面提到的过滤器执行链解析器PathMatchingFilterChainResolver,然后再调用它的getChain()匹配获取过滤器,最终过滤器在executeChain()中被执行。
由此可知,http请求都会在getChain方法处理。我们访问localhost:8080/xxx/…;/admin/test,可以发现触发debug如图
之后就如很多博客文章讲述的那样,进入getPathWithinApplication方法中,这里也是漏洞的核心触发点,跟进如图
继续跟进getPathWithinApplication方法,如图
继续跟进,如图
问题主要是在decodeAndCleanUriString方法中,我们跟进去看看
这里是先对uri进行了一次url解码,然后通过indexOf找到分号 ; 的位置,再返回从分号前截取的字符串,比如我这里的uri是 /xxx/…;/admin/test,经过截取后,就成了/xxx/…,这也就是为什么能绕过匹配的原因,因为我们之前的authMap中压根就没有这个uri,所以当然匹配不上。
我们返回到getPathWithinApplication方法,然后往下走返回到getChain方法,如图
这里调用DefaultFilterChainManager中的getChainNames()方法,我们进去看一下
可以看到就是获取前面初始化的时候存入的过滤路径,取key就是把路径都取出来到一个集合中。然后回到刚刚的方法,有一个while循环,我们只需要保持while里面的条件一直为true,即可绕过shiro的过滤器检测了。因为this.pathMatches(pathPattern, requestURI)方法中,requestURI是在map中找不到的,所以不会匹配返回的是false,while里面是用的取反运算符,pathMatches结果为true则while条件为false,pathMatches结果为false则while条件为true,不难理解,所以最后会在do里面返回null。
这里只是shiro的绕过,还有springboot对uri处理的缺陷才导致最后我们可以访问到相对应的web资源。找到org.springframework.web.util.UrlPathHelper类,在对应的getPathWithinServletMapping方法上打上debug,如图
问题主要出现在getPathWithinApplication方法上,点进去如图
继续跟进,如图
这里使用decodeAndCleanUriString方法对uri进行了一些处理,点进去看看
继续跟进方法,如图
跟进去,如图
这里的操作我给一张截图就能看明白了
截取后的uri就是这样的 /xxx/…/admin/test,然后返回到getPathWithinServletMapping方法,如图
这里因为经过getServletPath方法,所以路径变成了/admin/test,/xxx/…/被自动替换了,相当于返回到上一级目录。最终回到我们请求的controller,如图
能到这一步,就说明已经成功bypass身份验证访问到受保护的web资源了,至此漏洞分析结束。
免责声明:本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!
转载声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
订阅查看更多复现文章、学习笔记
伟盾网络安全
专注网络安全,用心做好安全这件事。
个人博客:博客
个人知乎:知乎