Undertow URL解析特性及其安全风险分析
一、背景
在某次风险处理过程发现一处越权漏洞,攻击人员使用类似这样的请求访问到后台地址:
http://www.xxx.com/admin/..;/index.action
Web服务响应正常且返回了后台界面,而正确的请求应该是:
http://www.xxx.com/admin/index.action
近期再次发现一些未授权的漏洞利用连接中也有/..;/ 这样的特殊字符串,猜测可能是同一个原因导致,可能属于通用性的问题。
二、原理分析
2.1 正常请求
经过初步调查,业务环境使用的是Undertow+Springboot,Undertow是基于java nio的Web容器,等同于Tomcat,但是要比Tomcat更加轻量级。上述的漏洞利用链接格式与BlackHat 2018中Orange所提到一个Tomcat解析特性相似,相关文章。
Tomcat会将 /..;/ 这样的URL路径识别成根目录,而业务系统解析 /..;/ 这样的链接时可能会采取与tomcat完全不同的解析方式,比如上述文章提及的Nuxeo系统在进行鉴权时以 “;” 为分隔符,只对 “;” 之前的路径进行鉴权。代码如下所示:
protected static String getRequestedPage(HttpServletRequest httpRequest) {
String requestURI = httpRequest.getRequestURI();
String context = httpRequest.getContextPath() + '/';
String requestedPage = requestURI.substring(context.length());
int i = requestedPage.indexOf(';');
return i == -1 ? requestedPage : requestedPage.substring(0, i);
}
根据上述的逻辑,假设用户访问https://www.xxx.com/nuxeo/login.jsp;/..;/[unauthorized_area]
鉴权的时候分号之前是通过的,而访问的时候却是 unauthorized_area。为了确定URL的解析逻辑,下面通过测试和源码分析两个角度看一下实现过程。首先建立一个测试项目,定义两个FIlter文件(顺便看一下两种FIlter定义方式有何异同)、一个Config文件、一个Controller文件:
IndexFIlter.java
package com.undertow.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
public class IndexFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器被创建1");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
StringBuffer path = httpRequest.getRequestURL();
System.out.println("IndexFilter " + path);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("过滤器被销毁1");
}
}
对于IndexFIlter通过FilterRegistrationBean的方式声明,FilterConfig.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Autowired
private IndexFilter indexFilter;
@Bean
public FilterRegistrationBean registerIndexFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(indexFilter);
registration.addUrlPatterns("*.action");
registration.setName("indexFilter");
return registration;
}
}
IndexFIlter2.java,直接通过WebFilter注解进行定义,这种方式要在启动类加上 @ServletComponentScan 注解
package com.undertow.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.FilterConfig;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(filterName = "IndexFilter2", urlPatterns = "*.action")
public class IndexFilter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("过滤器被创建2");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
StringBuffer path = httpRequest.getRequestURL();
System.out.println("IndexFilter2 " + path);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("过滤器被销毁2");
}
}
IndexController.java
package com.undertow.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@RequestMapping(value = "/index.action")
public String index(){
System.out.println("index");
return "index";
}
}
运行,然后先执行一次正常的请求
然后看请求日志
两个FIlter都成功拦截到请求,并最终将请求转发到了Controller,以上是正常的请求,在Tomcat和Undertow中都执行正常。然后我们看一下当使用带有/..;/ 的异常连接进行请求的时候Tomcat和Undertow是如何执行的。
2.2 Tomcat 解析过程
在Tomcat中我们使用如下的异常链接进行一次请求
http://127.0.0.1:8080/admin/..;/index.actio
此时程序逻辑跟之前正常链接访问是一致的
两个Filter成功拦截到了请求。为了进一步研究其中的原理,我们动态调试Tomcat解析过程,看一下源码是如何实现的。我们首先在 org.apache.catalina.connector.CoyoteAdapter.java 中的 service 函数下断点,为什么在这里呢,因为此时 req 对象中的 URI 仍然是 /admin/..;/index.action 我们要看看它后续是如何变化的。
然后在337行跳转进入 org.apache.catalina.connector.CoyoteAdapter.java 中的 postParseRequest 函数,根据函数介绍其作用就是对 request/response 进行必要的处理。我们重点看对URI的处理:
/**
Perform the necessary processing after the HTTP headers have been parsed
to enable the request/response pair to be passed to the start of the
container pipeline for processing.
*/
可以看到在601行和622行分别声明了两个变量,undecodedURI和decodedURI,前者是原始的URL的值,后者是解码的值(为空),然后625行将undecodedURI拷贝到decodedURI。
然后进入到 parsePathParameters 函数,这个函数是处理路径形如 /path;name=value;name2=value2 的请求,它定义了对分号的处理方式,图片中我折叠了部分代码,最终的处理结果就是删除了其中的分号,变成了这样的形式:/admin/../index.action
返回到 postParseRequest 函数后又进入到同文件的 normalize 函数,这个函数是对URI进行标准化
/**
* This method normalizes "\", "//", "/./" and "/../".
*/
经过上一步处理后,URI中有/../这样的关键字符,我们重点看下它是如何处理的
其实过程很简单,以 /admin/../index.action 为例,代码先找到 /../ 的首字符位置,然后在此基础上加3,也就是 /../ 末尾字符的位置,然后将后面所有的字符前移,得到这样的字符:
/index.actionex.action
然后再根据 /../ 后面所有字符长度作为新字符串的长度,进行截断,得到最终的字符串,说白了就是删除 /../ 及其之前的字符串
/index.action
此时 decodedURI 的值就是 /index.action,undecodedURI 仍是 /admin/..;/index.action。然后在695行进入 org.apache.catalina.mapper.Mapper.java 中的 internalMapWrapper 函数,这个函数定义了7种URI映射规则,比如第一种精准匹配,第二种前缀匹配。它本质就是个URI映射器,决定了某个请求由哪个Servlet处理。
如果所有的规则都不匹配,则由最后的 Default servlet 处理。
这里相当于是解析 Controller 中定义的URI匹配规则,本示例代码中就是将 /index.action 请求解析到 IndexController。然后在 CoyoteAdapter.java 的343行程序经过多层调用进入 org.apache.catalina.ApplicationFilterFactory.java 中的 createFilterChain 函数,在103行的这个for循环里逐个检测请求的URI是否符合定义的 UrlPatterns。
以上就是Tomcat解析URL以及的Controller和FIlter匹配URI的过程。
2.3 Undertow 解析过程
Undertow 需要显示的引用相关的包并排除tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
然后在Undertow的环境下,使用2.2 节中Tomcat一样的异常链接进行一次请求,请求日志如下所示:
我们发现Filter没有拦截到请求,但是 Controller 正确的执行了。再做一个测试,将 FilterConfig.java 的
registration.addUrlPatterns("*.action");
修改为
registration.addUrlPatterns("/admin/..");
然后再请求一次,后台日志如下所示
可以看到IndexFilter 能拦截到,但是IndexFilter2 依旧不行,同样分析一下源码,Undertow URL 处理过程感觉要更绕一些,这里我们反向溯源,在 IndexController 中下断点,然后可以看到 RequestMapping 的匹配的是 "/index.action"
这个值来自于request变量
在 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.java 中的 getHandlerInternal 函数中将这个值放到 request 变量中
然后看一下 lookupPath 是如何生成的,在 org.springframework.web.util.UrlPathHelper.java 中的 getPathWithinServletMapping 函数经过多次处理
首先在 org.springframework.web.util.UrlPathHelper.java 中的 decodeAndCleanUriString 函数对URI做清理,去除其中的分号
此时URI 变成 /admin/../index.action ,然后在同文件中的 getRemainingPath 函数中,将 mapping 尝试与 requestUri 进行匹配,若匹配成功,则返回剩余的字符串,这里 mapping 是 /admin/.. 那么剩余部分就是 /index.action
也就是说,当我们输入 /admin/..;/index.action 链接时,最终 Spring 进行路由匹配的路径是 /index.action。接着往下看,mapping的值是如何获得的,在 io.undertow.server.protocol.http.HttpRequestParser.java 中的 handlePath 函数中定义了处理步骤,412行以分号为分隔符,413行的servletRequestContext 函数将前半部分存储到 exchange 中,414行的 handlePathParameters 的函数将后半部分作为参数处理。
那么filter链是如何构造的呢?在 io.undertow.servlet.handlers.ServletInitialHandler.java 中的99行是分发请求的代码,
97行的 servletRequestContext 变量取自 exchange 变量,它已经保存了filter链,图中是将IndexFilter的URI匹配规则改为 "/admin/.." 后的结果。
2.4 结论
在使用Tomcat时,URL解析Filter和Controller是一致的,不存在歧义,如果自己的URL解析代码逻辑正确或者不自己解析就没有安全问题;在使用Undertow时,URL解析时如果遇到分号,容器会和框架会出现不一致,Filter解析分号前的URI,Controller解析分号后的URI,会在鉴权或恶意检测等场景出现绕过的风险。以
http://127.0.0.1/admin/..;/index.action
为例,当执行如下流程时可能会出现越权问题:
1、 在Filter中设置的鉴权逻辑,当检测到 *.action 且未登录时跳转到 /login.action
2、 在Controller中设置路由,当检测到 /index.action 时跳转到后台主页
3、 在Undertow环境下,Filter无法匹配分号前的 /admin/..,绕过鉴权的Filter,而Controller刚好匹配 /index.action,造成未授权访问
而这本质上不算一个漏洞,只是Web容器都有各自的解析特性,但使用不当可能会有安全问题。
为了避免以上问题,在Undertow时,可以自行获取完整的请求路径进行鉴权。或者干脆对带有/**;/这样的请求进行全局检测,比如自行定义一个filter对类似的请求进行特殊的处理,可以去除异常字符也可以直接拒绝访问。
@WebFilter(filterName="Filter", urlPatterns="/**")
另外合理的使用spring security 也是一个不错的选择,测试发现spring-security 不仅能够有效进行访问控制,默认情况下还会对带分号的请求进行拦截:
The request was rejected because the URL contained a potentially malicious String ";"
三、参考文献
https://www.freebuf.com/vuls/181389.html
https://www.cnblogs.com/youzhibing/p/9866690.html
https://zhuanlan.zhihu.com/p/65658315
百度安全应急响应中心
百度安全应急响应中心,简称BSRC,是百度致力于维护互联网健康生态环境,保障百度产品和业务线的信息安全,促进安全专家的合作与交流,而建立的漏洞收集以及应急响应平台。地址:https://bsrc.baidu.com
长按关注
关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
随时掌握互联网精彩
- 1 准确把握守正创新的辩证关系 7999911
- 2 中国黄金原董事长家搜出大量黄金 7902735
- 3 空调英文不会男生盯着考场空调看 7875833
- 4 消费品以旧换新“加速度” 7730885
- 5 被铁路售票员的手速惊到了 7654092
- 6 网红赤木刚宪爆改赵露思 7576160
- 7 县委原书记大搞“刷白墙”被通报 7465139
- 8 山姆代购在厕所分装蛋糕 7363318
- 9 马龙刘诗雯穿正装打混双 7267261
- 10 刘强东提前发年终奖 7118943