暂无简介
CVE-2022-22978 Spring Security RegexRequestMatcher 认证绕过及转发流程分析
一、漏洞分析
这篇文章对认证绕过的分析比较简单,因为关键部分就在对正则模式的绕过。
主要花较多的篇幅在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
选择对应的版本后进行调试,直接在前面分析的部分下断点。
这里的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字符无法被匹配上,至此首尾呼应,完结撒花!
二、加上参数的情况
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();
}
}
这样业务逻辑就不会被绕过了。
- 本文作者: s3gundo
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11473
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!