我在绕过这样鉴权时,有时候成功有时候却是 404, 这让我很疑惑。
挖洞时,我习惯第一时间去看鉴权是否能绕过。遇到过最多的鉴权绕过是自己使用错误的过滤方法,过滤某些特定的 URL
去鉴权或不鉴权,这种情况通常可以通过 URL编码绕过,或者像目录穿越一样 /***/../***
绕过,后面会详细展开。
但我在绕过这样鉴权时,有时候成功有时候却是 404
, 这让我很疑惑。这是为什么呢 ? 本文就是探讨这个问题. 环境 :Spring
, 其他的鉴权问题这里不进行探讨.
演示
我们先来看一段代码, 能绕吗? 答案是: 能,URL 编码就可以绕过了。 为什么呢 ? 这是第一个问题。
// 请求地址
String requestPath = request.getRequestURI();
// 判断是否需要鉴权
if (requestPath.indexOf("/back/") != -1) {
// 鉴权
}
// 放行
再看一段代码, 能绕过吗? 能 ? 不能 ? 答案都对, 像目录穿越一样绕过: /anything/../****
, 那什么情况下不能绕过呢 ? 这是第二个问题。
String requestURI = request.getRequestURI();
// 判断是否需要鉴权
if (requestURI.startsWith("/anything/")){
// 放行
}
// 鉴权
环境准备
我们先搭建一个简单的 Spring Web 环境, 新建一个 Java EE
项目, i注意选择 Web Application 其他自定义,直接下一步都行。
在 pom.xml
中添加相应依赖,搭建 Spring Web 项目
pom.xml
<properties>
<!-- Spring 最新版 5.3.12 -->
<spring.version>5.3.12</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
在 resources
下新建 spring-mvc.xml
添加组件扫描。扫描 Controller
src/main/resources/spring-mvc.xml
<!--Controller 组件扫描 -->
<context:component-scan base-package="com.johnson.controller"/>
配置 web.xml
src/main/webapp/WEB-INF/web.xml
<!-- 配置 SprigMVC 的前端控制器 -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
新建 Controller
com.johnson.controller.UserController
package com.johnson.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.http.HttpServletMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.MappingMatch;
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping("show")
@ResponseBody
public String save(HttpServletRequest request) {
return "Hello World";
}
}
新建拦截器, 这里先默认返回 true
全部不拦截。
com.johnson.interceptor.LoginInterceptor
package com.johnson.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
}
在 spring-mvc.xml
中,配置拦截器
src/main/resources/spring-mvc.xml
<!--配置拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.johnson.interceptor.LoginInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
启动服务,访问 /user/show
, 正常答应 Hello World
第一个问题
环境准备
在拦截器中添加拦截逻辑
com.johnson.interceptor.LoginInterceptor
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 请求地址
String requestPath = request.getRequestURI();
// 判断是否需要鉴权
if (requestPath.indexOf("/user/") != -1) {
// 鉴权
// 不实现逻辑,直接鉴权失败。
PrintWriter writer = response.getWriter();
writer.write("No Login!");
writer.close();
return false;
}
// 放行
return true;
}
保存后重启服务,此时访问被拦截器拦截。
绕过
该如何绕过呢?很简单:使用 URL 编码。请求使用 BurpSuite 截断, 将 r
进行编码
将编码后的 r (%72)
替换到请求中,成功绕过
解疑
为什么会成功呢 ?, 这就要看谁处理了路由,谁充当了路由分发。回到 web.xml
中的配置,我们都知道使用 Spring MVC 需要配置前端控制器 DispatcherServlet
. 而且配置了所有的路由都会经过 DispatcherServlet
的处理。
doService 方法中调用了 doDispatch
org.springframework.web.servlet.DispatcherServlet#doService
而 doDispatch 方法中调用了 getHandler
,在此处打上断点
org.springframework.web.servlet.DispatcherServlet#doDispatch
这里调用了 HandlerMapping
对象的 getHandler
方法来获取相应的处理。跟进去
org.springframework.web.servlet.DispatcherServlet#getHandler
继续往下跟
org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
继续跟进
org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#getHandlerInternal
这里默认会进入 false 的逻辑,跟进 UrlPathHelper#resolveAndCacheLookupPath 方法
org.springframework.web.servlet.handler.AbstractHandlerMapping#initLookupPath
继续跟进
org.springframework.web.util.UrlPathHelper#resolveAndCacheLookupPath
跟进 getPathWithinApplication 方法
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
跟进 getRequestUri 方法
org.springframework.web.util.UrlPathHelper#getPathWithinApplication
这里进入了最重要的逻辑,通过属性获取,默认是没有任何内容的
org.springframework.web.util.UrlPathHelper#getRequestUri
也就是说他是通过 request.getRequestURI()
来获取请求地址的(注意这里获取的是 URL 编码 的地址)。并对 uri
进行 decodeAndClean 处理。跟进 decodeAndCleanUriString 方法
一共对 uri
进行了三步处理,这里不再详细描述。简单的说一下 removeSemicolonContent(删除 ;
)、decodeRequestString(这一步是绕过关键,URL 解码)、getSanitizedPath(删除 //
)。
org.springframework.web.util.UrlPathHelper#decodeAndCleanUriString
我们进入 decodeRequestString 方法,可以看到对我们的参数进行了 URL解码
所以!Spring 可以拿到相应的处理方法。
回到拦截器中,通过 request.getRequestURI()
来获取请求地址,是编码后的。所以这里的匹配是匹配不到的。
小结
Spring 全版本都可以使用 URL 编码绕过该鉴权逻辑
第二个问题
环境准备
这里我们先调整一下版本使用 Spring 5.2.x 里的最高版本 5.2.18.RELEASE。调整依赖版本后,推荐删除 target 目录
pom.xml
<properties>
<!-- Spring 5.2.x 最高版本 5.2.18.RELEASE -->
<spring.version>5.2.18.RELEASE</spring.version>
</properties>
调整拦截器逻辑
com.johnson.interceptor.LoginInterceptor
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 请求地址
String requestPath = request.getRequestURI();
// 判断是否需要鉴权
if (requestPath.startsWith("/anything/")) {
// 放行
return true;
}
// 鉴权
// 不实现逻辑,直接鉴权失败。
PrintWriter writer = response.getWriter();
writer.write("No Login!");
writer.close();
return false;
}
启动服务,访问 /user/show
此时访问被拦截器拦截。
绕过
请求使用 BurpSuite 截断, 像目录穿越漏洞一样 /anything/../user/show
绕过
解疑
那么为什么可以绕过呢,和第一个问题一样,跟进 getHandler 方法
org.springframework.web.servlet.DispatcherServlet#doDispatch
继续跟进 getHandler 方法
org.springframework.web.servlet.DispatcherServlet#getHandler
继续跟进 getHandlerInternal 方法
org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler
跟进 getLookupPathForRequest 方法
org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#getHandlerInternal
跟进 getPathWithinApplication 方法
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
这里不再往里跟进, 和第一个问题中一样是获取请求并解码消毒 URL 的
org.springframework.web.util.UrlPathHelper#getPathWithinApplication
alwaysUseFullPath 属性默认是 false
,这里跟进 getPathWithinServletMapping 方法
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
进入最重要的方法了,跟进 getServletPath 方法
org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping
这里默认是获取不到任何值的,所以执行的是 request.getServletPath();
org.springframework.web.util.UrlPathHelper#getServletPath
注意这里 URL 变成正常的 URL 了,这里是 Tomcat 帮我们处理的。感兴趣的师傅可以去跟一下,往下的判断不会对结果产生影响。
org.springframework.web.util.UrlPathHelper#getServletPath
这里默认就返回了 servletPath
,一个正常的 URL
org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping
所以最后也能正常匹配到处理方法。
不能绕过?
那什么情况下不能绕过呢?版本。Spring >= 5.3.x 就不存在该问题了,所以刚刚需要调整版本。但 Spring < 5.3.x 就存在该问题。
环境准备
将版本调整到 5.3.0
, 5.3.x 的第一个版本
pom.xml
<properties>
<spring.version>5.3.0</spring.version>
</properties>
发送请求,继续跟进 getHandler 方法,和之前一样一直跟进就好了。
org.springframework.web.servlet.DispatcherServlet#doDispatch
来到 getLookupPathForRequest 方法
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
这里 getPathWithinApplication 方法就不跟进了,就是获取请求并解码消毒 URL 的逻辑。注意这里的判断,alwaysUseFullPath 属性默认为 false
, 取反为 ture
,这里跟进 skipServletPathDetermination 方法
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
这里 getMappingMatch()
默认为 DEFAULT
,MappingMatch.PATH
默认为 PATH
所以运算结果为 false
取反侯为 true
那么最后结果是 true
org.springframework.web.util.UrlPathHelper#skipServletPathDetermination
回到 getLookupPathForRequest 判断进行了一次取反所以最后结果为 false
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
这是肯定匹配不到相应的处理方法的
org.springframework.web.util.UrlPathHelper#getLookupPathForRequest
手动跟进 判断内 getPathWithinServletMapping 方法, 很熟悉。如果进入这里那么就可以绕过了。
org.springframework.web.util.UrlPathHelper#getPathWithinServletMapping
小结
Spring < 5.3.x 时可以通过目录穿越的方式绕过该鉴权逻辑
总结
如下鉴权全版本都可以通过 URL编码 绕过鉴权逻辑
// 请求地址
String requestPath = request.getRequestURI();
// 判断是否需要鉴权
if (requestPath.indexOf("/back/") != -1) {
// 鉴权
}
// 放行
如下鉴权在 Spring < 5.3.x 时候可以通过目录穿越的方式 (/***/../***/
) 绕过鉴权逻辑
String requestURI = request.getRequestURI();
// 判断是否需要鉴权
if (requestURI.startsWith("/anything/")){
// 放行
}
// 鉴权
- 本文作者: JOHNSON
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/829
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!