这个鉴权到底能不能绕

我在绕过这样鉴权时,有时候成功有时候却是 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() 默认为 DEFAULTMappingMatch.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/")){
    // 放行
}

// 鉴权
  • 发表于 2021-11-02 10:25:10
  • 阅读 ( 10505 )
  • 分类:漏洞分析

1 条评论

wuerror
或许应该用小于等于?项目里spring 5.3.20也能这么绕
请先 登录 后评论
请先 登录 后评论
JOHNSON
JOHNSON

12 篇文章

站长统计