CVE-2022-22978 Spring Security RegexRequestMatcher 认证绕过及转发流程分析

2022-6-23 10:33:50 Author: xz.aliyun.com 阅读量:8 收藏

这篇文章对认证绕过的分析比较简单,因为关键部分就在对正则模式的绕过。
主要花较多的篇幅在spring的高低版本对业务的转发上。可以选择对自己感兴趣的部分进行阅读。
如有错误请多多指出!

1、漏洞成因

因为 RegexRequestMatcher 正则表达式处理的特性,导致可能某些需要认证的 Servlet 被绕过。影响版本如下:

  • 5.5.x prior to 5.5.7
  • 5.6.x prior to 5.6.4
  • Earlier unsupported versions

补丁中新增了 Pattern.DOTALL (0x20 可以在源码中看到注释),默认情况下正则表达式 . 不会匹配换行符,设置了 Pattern.DOTALL 模式后,才会匹配所有字符包括换行符。这里把dotall模式的注解和谷歌翻译贴在下面。

Enables dotall mode.
In dotall mode, the expression . matches any character, including a line terminator. By default this expression does not match line terminators.
Dotall mode can also be enabled via the embedded flag expression (?s). (The s is a mnemonic for "single-line" mode, which is what this is called in Perl.)

启用dotall模式。

在dotall模式下,表达式。匹配任何字符,包括行终止符。默认情况下,此表达式与行终止符不匹配。

也可以通过嵌入的标志表达式(?s)启用Dotall模式。(s是“单行”模式的助记符,在Perl中就是这样称呼的。)

可以考虑在 URL 中加入换行符( \r\n )来绕过正则表达式匹配。
/admin/..;/*** 。而Spring Security 存在 StrictHttpFirewall 过滤机制,默认会过滤特殊字符:

2、环境搭建

在这里直接将pom文件提供给大家。测试springboot所使用的环境是2.7.0。在一开始使用2.5.3环境的时候,会遇到路由转发的问题,导致404。后面会详细把代码贴出来。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>  //这个版本一定要高
    </parent>

    <groupId>org.example</groupId>
    <artifactId>springsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-security.version>5.6.3</spring-security.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- web模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

实现WebSecurityConfigurerAdapter添加需要认证的接口。

package start.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityDemo extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.authorizeRequests().regexMatchers("/admin/.*","/admin2").authenticated();
    }

}

admin接口和admin2接口。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SecAdminController {

    @GetMapping("/admin/*")
    public String admin(){
        return "hello Admin";
    }
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NewController {

    @GetMapping("/admin2")
    public String noatuh(){
        return "hello admin2";
    }
}

3、调试分析

其实关键点就在于如何对url进行校验。

进入org.springframework.security.web.util.matcher.RegexRequestMatcher#matches方法中。

request.getServletPath() -->对字符解码 并且会将;之后的字符到/字符删除
request.getRequestURI()  -->原样输出


所以在校验的时候会产生认证的绕过:

4、如何转发到对应业务

Spring-boot 2.7.0

从过滤器走出之后,可以看下面的调用栈,要进行服务功能的执行,分发器(Dispatcher)要选择相应的处理器进行选择。

org.springframework.web.servlet.DispatcherServlet#getHandler
所有映射的内容放在了注册中心处。
RequestMappingInfoHandlerMapping继承org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
在他的方法上下断点getHandlerInternal


虽然这里会移除分号,但是在spring security前面就会被过滤的。这里是spring-web-mvc的组件

这里的返回值是lookupPath是没有被解码的,可以正常映射到/admin/*的路径上。
感兴趣的后面也可以跟一下。在匹配上之后就是选择对应路径的方法进行反射调用,唤醒业务逻辑部分代码。

Spring-boot 2.5.3

切换版本至2.5.3 先把这个版本下的结果公布一下:

选择对应的版本后进行调试,直接在前面分析的部分下断点。

这里的lookupPath会将%0a解码,导致映射不到对应的路由上面。
所以跟进initLookupPath里面,看看他是如何获取的。

在上面的方法返回的时候,走进了org.springframework.web.util.UrlPathHelper#decodeAndCleanUriString,在decodeRequestString那行将url解码返回了。导致路由映射不到相应的handler。

这里是直接从directPath里面寻找。先匹配directPath再去匹配带有通配符的path,所以path分配的优先级在这里就可以体现。

然后在对应所有的path里面去寻找相应的匹配。到这里可以看到,我们前面分析的其实都白费了= = 。最终去匹配的竟然还是request对象。好的 那我们进入这个方法重新开始。

具体的代码逻辑可以看这里面这个方法org.springframework.util.AntPathMatcher#doMatch

跟到最后面会发现非常有意思的是,springcore在进行路由匹配的时候也是使用相同的pattern的模式,导致路由也无法匹配上。

我把RegexRequestMatcher的Pattern和用于路由选择匹配的Pattern放在一起,他们使用同一种flag为0的模式,导致\n字符无法被匹配上,至此首尾呼应,完结撒花!

最后找不到对应的handler就会出现404

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class NewController {

    @GetMapping("/admin2")
    public String noatuh(){
        return "hello admin2";
    }
}

可以看到这里的逻辑也存在一些问题会导致认证被绕过。将参数拼接之后并与需要校验认证的路径进行对比,导致认证被绕过。

其实这个问题也可以说是开发人员在路径匹配的时候,没有加正确的匹配模式导致的。我们将路径改为

/admin/(?s).*
@Configuration
public class SecurityDemo extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception{
        httpSecurity.authorizeRequests().regexMatchers("/admin/(?s).*","/admin2").authenticated();
    }
}


这样业务逻辑就不会被绕过了。


From: https://xz.aliyun.com/t/11473