一、前 言
二、Servlet URL匹配模式
三、Resin Servlet 路由机制
四、URI规范化特性导致认证绕过
五、总 结
权限认证是一种用于控制系统资源访问的安全机制,如果权限认证存在缺陷,将对Web应用的保密性、完整性、可用性造成严重影响。此次分享的内容是之前挖掘国内某OA漏洞时,受tomcat下路由分发特性导致认证绕过思路影响去分析的。旨在讨论Resin URI规范化特性导致的权限认证绕过问题。
在 java
应用中通常通过xml声明Servlet
也可以在类定义出上使用Servlet
规范中定义的WebServlet
注解去声明。
Servlet声明时可以不同类型的urlpattern,当路由请求时,Servlet 会根据不同的URL模式进行匹配。匹配方式主要有4种,优先级是精确匹配 > 路径匹配 > 后缀匹配 > 缺省匹配
精确匹配
uri必须和url-pattern完全一致,如/hello/1.Servlet
最长路径匹配
也叫路径匹配,指以 *
结尾的url-pattern,表示匹配以该路径开头的所有请求,越长的路径匹配优先级越高。
后缀匹配
指 .xxx
这样的,如 .jsp
、 .servlet
。
缺省匹配
指url-pattern是 /
,通常这个模式匹配所有未匹配的请求,适用于处理应用中的默认资源(如静态文件),或者用于 404 页面处理。
Resin
是由Caucho Technology开发的一款高性能的 Java 应用服务器(HTTP服务器+Servlet容器),广泛应用于企业级 Java 应用的部署。Resin
相比 Tomcat
在性能和稳定性上表现更优;相比 Weblogic
架构更轻量,配置更简单,但不像 WebLogic
那样支持 Java EE 的所有规范。
Servlet容器有一个至关重要的作用是接收来自客户端(通常是Web浏览器)的HTTP请求,将这些请求解析为Servlet请求对象,然后将其路由到相应的Servlet来处理。Servlet处理后,容器会负责将其响应包装成HTTP响应并发送回客户端。以下就分析一下路由信息的维护以及路由分发过程。
Servlet-mapping添加过程
java中定义了ServletContext接口,里边提供了对Servlet、listener、filter的相关操作方法,Resin下的实现在com.caucho.server.webapp.WebApp
,负责封装和管理一个独立的 Web 应用,包括应用的配置、资源、生命周期以及与 Servlet 相关的各项设置。
每个<Servlet-mapping>
标签对应一个ServletMapping
对象,包括Servlet-class
、Servlet-name
、url-pattern
以及关联的Servlet
实例。
当程序启动时会调用 com.caucho.server.webapp.WebApp#addServletMapping
开始建立URL与 Servlet
实例建的映射关系。其中 init
方法接收的参数是 com.caucho.server.webapp.WebApp#_ServletMapper
成员变量,类型是 com.caucho.server.dispatch.ServletMapper
,该类作用是负责维护Web 应用中所有 ServletMapping
对象及相关信息。
init
方法中获取 urlPattern
,然后调用 ServletMapper#addUrlMapping
方法。
ServletMapper#addUrlMapping
中调用 _ServletMap
成员变量(类型是 com.caucho.server.dispatch.UrlMap
)的 addMap
方法,传入的2个主要参数是 urlPattern
和 ServletMapping
对象。
com.caucho.server.dispatch.UrlMap
的_regexps
成员变量中维护着其内部类com.caucho.server.dispatch.UrlMap.RegexpEntry
的动态数组。RegexpEntry
中存储着匹配优先级prefixlength
、urlPattern
、urlPattern
生成的正则表达式以及和正则表达式关联的对象_value
等。UrlMap和RegexpEntry
类共同协作完成不同URL映射规则的存储及检索。当一个 URI
请求到达 Resin
服务器时,UrlMap
会遍历其内部存储的 RegexpEntry
对象列表,尝试使用每个 RegexpEntry
对象的正则表达式模式来匹配该 URL
,如果找到匹配的 RegexpEntry
对象,则返回关联的_value
。
UrlMap#addMap
的作用是解析urlpattern
转换成正则表达式cb和匹配优先级prefixLength
,最后调用addRegexp
方法,代码如下:
prefixLength
是根据urlpattern
中从左到右连续的必须要存在的路径(暂且称作确定路径)的长度表示的,长度越大优先级越高。以下是精确匹配、路径匹配、后缀匹配生成正则的例子:
规则类型 |
确定路径 |
prefixLength |
正则 |
|
/hello/1.Servlet |
精确匹配 |
/hello/1.Servlet |
16=len(确定路径) |
^/hello/1.Servlet$ |
/hello/* |
路径匹配 |
/hello/ |
5=len(确定路径)-2 |
^/hello(?=/)|^/helloz |
*.jsp |
后缀匹配 |
无 | 0 |
^.*.jsp(?=/)|^.*.jspz |
Resin解析漏洞xxxx.jsp/xxxx
会路由到xxx.jsp
就是生成的正则表达式用正向先行断言(?=/)
导致xxxx.jsp/xxxx
可以匹配到`^.*.jsp(?=/)|^.*.jspz
正则表达式引起的。
UrlMap#addRegexp
中新建一个RegexpEntry
对象维护到_regexps
属性中,此时_value
是ServletMapping
对象。
Servlet路由过程
当java应用启动完毕,用户访问一个URL时,Resin则需要根据URL匹配并路由到对应Servlet上,总共分两步:
-
根据uri匹配到正确的
ServletMapping
,构建Invocation
对象; -
路由分发到
Servlet
的service
方法上。
路由分发在HttpRequest#handleRequest
中,先通过getInvocation
方法获取响应的Invocation
对象,然后再调用service
方法去路由。
getInvocation
方法根据uri(指去掉webapp路径的uri))获取构建Invocation
对象,构建成功后加入缓存中,之后访问该uri不需要再次构建。
AbstractHttpRequest#buildInvocation
中先调用decoder.splitQueryAndUnescape
,然后再调用后buildInvocation
InvocationDecoder#splitQueryAndUnescape
中先调用normalizeUriEscape
方法URL解码一次,再调用normalizeUri
方法对进行规范化处理,之后设置为Invocation
的uri
和contextURI
属性。
uri进行规范化处理后返回,再跟进buildInvocation
,直到ServletMapper#mapServlet
中,先调用stripPathParameters
对前边设置的contextURI
属性再做一次处理,然后调用this._ServletMap.map
方法去匹配。
map方法遍历_regexps
动态数组,用规范化后uri去匹配RegexpEntry
中维护的正则表达式,将_prefixLength
最长的Servletmapping
对象返回回来。
经过上述分析可知Resin将Servletmapping
对象和其正则表达式等信息封装在RegexpEntry
对象维护到Servletcontext
中。路由时将uri先经过normalizeUri处理,再经过stripPathParameters处理,最后遍历所有的RegexpEntry
对象的正则去匹配处理后的uri,匹配到就返回对应的Servletmapping
对象。如果一个uri匹配到多个,则返回prefixLength
最长(优先级最高)的那个。
Resin路由时RUI规范化特性
低版本(<4.0.60)
InvocationDecoder#normalizeUri
(Resin4.0.58)方法主要的目的是为了将/../
以及/./
这样的uri处理成规范的uri,将 处理成
/
,但附带还有一些其他的操作,代码逻辑如下:
ServletInvocation#stripPathParameters
(Resin4.0.58)方法,主要是想处理 ;
的截断问题,代码逻辑如下:
根据以上分析可知uri经过InvocationDecoder#normalizeUri
和ServletInvocation#stripPathParameters
后的处理规则如下。
序号 |
uri(备注 该列/代指 /和 ) |
normalizeUri |
stripPathParameters() |
---|---|---|---|
1 |
/xxx/./ |
/xxx/ |
/xxx/ |
2 |
// |
/ |
/ |
3 |
/123/xxx/../ |
/123/ |
/123/ |
4 |
/xxx{多个.或者空格}/ |
win下/xxx/ linux下不变 |
win下/xxx/ linux下不变 |
5 |
xxx{一个或多个.或者空格} (uri末尾) |
win下xxx linux下不变 |
win下xxx linux下不变 |
6 |
/abc;xxx(uri末尾) |
/abc;xxx |
/abc |
7 |
/abc;xxx/yy |
/abc;xxx/yy |
/abc/yy |
8 |
/.;xxx/xxx |
/.;xxx/xxx |
/./xxx |
9 |
/1;%0a/1.jsp |
/1;n/1.jsp |
/1/1.jsp |
10 |
/1/1.jsp/xxx |
/1/1.jsp/xxx |
/1/1.jsp/xxx |
11 |
/..a(a表示不是/的字符) |
异常 |
所以存在一些uri通过上述两个方法处理后能和某个与servletMaping
关联的正则表达式匹配,从而能正确路由。以/1/1.jsp
为例,如下的uri都可以正确路由。不止下边这些方式,可以根据上述处理规则自由组合。
对应上表序号 |
uri |
---|---|
1 |
/1/./1.jsp |
2 |
/1//1.jsp |
3 |
/1/a/../1.jsp |
4 |
/1.%20./1.jsp |
5 |
/1/./1.jsp.%20. |
6 |
/1/1.jsp;;; |
7 |
/1;xxx/1.jsp |
8 |
/.;xx/1/1.jsp |
9 |
/.;/1/1.jsp |
10 |
/1/1.jsp/asd |
高版本(>=4.0.60))
在高版本下做了修复,以下代码是Resin4.0.66
normalizeUri方法新增了三点:
-
uri中有
/;
或者/.;
直接异常,所以序号8被修复; -
将
/abc{连续或单个.或者空格}/
中的.
或者空格替换为_
,所以序号4被修复; -
uri最后一位
.
或者空格替换为_
,所以序号6被修复;
代码逻辑如下:
stripPathParameters
方法新增了uri中有/.;
直接异常,所以序号8被修复
通过分析Resin
Servlet 路由机制可知,路由时使用规范化后的uri和RegexpEntry
对象中的正则表达式去匹配,所以一些本身不能匹配的uri经过规范化后却能正确路由、后缀匹配生成的一些不合理的正则表达式(如^.*.jsp(?=/)|^.*.jspz
)导致一些不合理的uri(如/1/1.jsp/xxx
)也能正确路由。
有很多Web应用会在filter等中设置一些uri规则去做权限认证,如果使用javax.Servlet.http.HttpServletRequest#getRequestURI
方法获取未规范化的uri去判断当前请求是否需要认证,那么找到一个能正常路由但不满足认证条件的uri,则可以达成权限认证绕过的效果。
如下是Windows环境下某OA的一个例子,当uri满足如下if条件时,则需要判断loginid是否为true,不为true则认证失败。
正常访问时会被认证规则拦截,响应404。
结合前边的分析的规范化特性,给出一个能正常路由且能绕过认证条件的uri,如/messager/eimforward.jsp.
。
这里以某OA的一个例子说明存在这种漏洞模式,但存在构造的某些理论上可以正确路由的uri也会被拦截,这是由于某OA有很多拦截规则,可能过了某个规则但恰好被别的规则拦截,这里就不具体展开了。
基于Servlet URL 的匹配模式、 urlpattern
的正则生成逻辑和路由分发时的uri规范化特性,可以更深入理解 Servlet 路由机制的核心原理。这种规范化过程中处理 uri的方式是认证绕过的关键点,尤其是当一些 Web 应用基于获取未规范化的uri进行权限认证时,可能导致认证机制被绕过。为了更直观地展示这一技术原理,文中结合实际案例展示了如何利用此机制实现认证绕过。
【版权说明】
本作品著作权归Mmuz所有
未经作者同意,不得转载
Mmuz
天工实验室安全研究员
专注于代码审计、Web安全
原文始发于微信公众号(奇安信天工实验室):Resin URL解析特性导致权限认证绕过分析