Apache Shiro之前披露了CVE-2023-22602,主要是由于 1.11.0 及之前版本的 Shiro 只兼容 Spring 的ant-style路径匹配模式(pattern matching),而高版本的 Spring使用的是PathPatternParser,当使用不同的路径匹配模式时,攻击者在特定的场景下访问可绕过 Shiro 的身份验证。
在修复该漏洞时,主要是通过Spring动态的读取文件留下的扩展接口来将路径匹配模式强制修改为了AntPathMatcher:
确实这样的话保证了两者请求解析模式的一致性,避免了解析差异导致的安全问题,但是众所周知,Shiro 是一个功能强大且易于使用的 Java 安全框架,可以提供提供身份验证的功能。那么有没有可能某个框架与Shiro间同样存在类似的解析差异问题导致身份认证绕过呢?
大致的内容是对于类似目录穿越的请求,在非springframework特定的场景下可能存在权限绕过的可能。
Shiro中对于URL的获取及匹配在org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain
方法,其会根据URL路径匹配,解析出ServletRequest请求过程中要执行的过滤器链。其中主要是通过getPathWithinApplication方法获取应用程序内的URI的相对路径:
具体方法的实现如下,首先通过request.getServletPath()+request.getPathInfo()方法获取URI,然后再调用removeSemicolon和normalize方法处理:
在normalize方法中,会处理路径穿越符进行处理:
获取到对应的请求路径后,最终会根据URL进行路径匹配,解析出ServletRequest请求过程中要执行的过滤器链。详细的过程可以参考https://forum.butian.net/share/2231 。
那么Jersey又是如何对请求进行处理的呢?
这里直接看Jersey是如何根据请求路径找到对应的资源的。ApplicationHandler是Jersey的核心组件之一,其handle方法主要是调用this.runtime.process(request)方法来处理HTTP请求:
process()方法会根据ContainerRequest对象的内容和Jersey应用程序的配置信息,定位到适合处理该请求的资源类或方法,并调用其对应的处理函数来处理客户端请求。
在这个方法中,首先设置了基础 URI,然后使用 Stages.process()
方法对请求进行处理,并获取了端点引用。如果无法找到端点,则抛出 NotFoundException
异常:
在Stages.process方法中实际上主要是遍历rootStage(其中存储了解析请求的阶段相关的信息,如请求 URI、请求方法、请求头等),调用其apply方法进行处理:
跟请求路径相关的主要是org.glassfish.jersey.server.internal.routing.RoutingStage
,用于解析请求 URI 并将其映射到相应的资源方法或类上。查看其apply方法的具体实现,主要是调用 _apply()
方法来查找路由匹配结果,其会返回一个 RoutingStage.RoutingResult
对象,包含找到的端点引用以及处理后的请求上下文信息:
在_apply()
方法中,使用传入的 request
和 router
参数调用 router.apply(request)
方法,返回一个 org.glassfish.jersey.server.internal.routing.Router.Continuation
对象,其中包含了匹配成功的子路由和处理后的请求上下文对象。然后,使用 continuation.next().iterator()
方法获取所有匹配成功的子路由,并迭代遍历它们。对于每个子路由,递归调用 _apply()
方法以查找匹配的端点引用。
如果找到了匹配的端点引用,则返回一个 RoutingStage.RoutingResult
对象,其中包含了找到的端点引用和处理后的请求上下文对象。否则,继续查找下一个子路由,直到找到匹配的端点引用为止:
首先,通过调用org.glassfish.jersey.server.internal.routing.MatchResultInitializerRouter#apply
将请求对象和根路由器对象打包成一个 Continuation
对象,并返回给调用方。通过这个 Continuation
对象,Jersey 可以获取与请求相关的所有信息,并继续处理后续的请求逻辑:
在Jersey中,会使用 RoutingContext
对象执行路由匹配操作,是一个十分重要的属性,具体看看这里pushMatchResult方法的调用,这里主要是调用org.glassfish.jersey.server.ContainerRequest#getPath对请求的上下文进行处理,如果decode属性为ture(默认为false),会进行一系列的解码处理:
否则会调用encodedRelativePath方法获取当前请求的编码后的相对路径:
处理结束后会创建SingleMatchResult
实例:
这里会调用stripMatrixParams方法进行额外的处理,这里主要是剔除URI 中的矩阵参数,将 URI 按照斜杠字符进行分割,并将每个分段中的第一个分号字符及其后面的所有内容全部删除。最后,它将所有分段重新拼合起来,并返回处理后的结果:
然后返回对应的结果并push到matchResults列表中:
可以看到Jersey在整个解析过程中并没有对路径穿越符../进行额外的处理。有点类似Spring高版本PathPattern。
根据前面的分析,Jersey跟Shiro在解析过程中是存在差异的,利用这一点在某种特定情况下会造成身份验证绕过的风险。
以shiro为例,对应的身份认证如下,/api
目录下的所有接口都需要经过安全认证才能访问:
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
bean.setLoginUrl("/login");
bean.setSuccessUrl("/index");
bean.setUnauthorizedUrl("/unauthorizedurl");
Map map = new LinkedHashMap<>();
map.put("/doLogin/", "anon");
map.put("/api/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
假设对应的请求资源Resource Class如下:
@GET
@Path("/{path : .*}")
public Response getUser(@PathParam("path") @Encoded String path) throws IOException {
return Response.ok().entity(path).build();
}
正常情况下,在缺少安全认证的情况下访问/api/page,会返回302状态码重定向到login页面:
结合前面分析的,shiro会解析..
而Jersey不会的差异,因为可以这里路由匹配的正则表达式为.*
表示匹配任意字符,那么发送如下请求达到绕过权限控制的效果:
漏洞的修复主要是通过InvalidRequestFilter过滤器进行处理。
可以看到在过滤器的isValid中增加了一个containsTraversal方法:
在该方法中对类似./
的字符进行了检测,同时也考虑了url编码绕过的问题:
作为Shiro的“同僚”SpringSecurity,其也是一个功能强大且高度可定制的身份验证和访问控制框架。针对类似的安全问题,在Spring Security中提供了一个HttpFirewall接口,用于处理掉一些非法请求。目前一共有两个实现类:
Spring Security缺省使用的是StrictHttpFirewall,其会拦截和处理的一些内容:
拦截内容 |
---|
分号(;或者%3b或者%3B) |
斜杠(%2f或者%2F) |
反斜杠(\或者%5c或者%5B) |
%25(URL编码了的百分号%) |
英文句号.(%2e或者%2E) |
虽然没办法兼顾各个框架间的解析差异,但是确实对一些不合法的http请求path进行拦截能在一定程度上规避掉风险。
64 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!