这篇文章对认证绕过的分析比较简单,因为关键部分就在对正则模式的绕过。
主要花较多的篇幅在spring的高低版本对业务的转发上。可以选择对自己感兴趣的部分进行阅读。
如有错误请多多指出!
因为 RegexRequestMatcher
正则表达式处理的特性,导致可能某些需要认证的 Servlet
被绕过。影响版本如下:
补丁中新增了 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
过滤机制,默认会过滤特殊字符:
在这里直接将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";
}
}
其实关键点就在于如何对url进行校验。
进入org.springframework.security.web.util.matcher.RegexRequestMatcher#matches
方法中。
request.getServletPath() -->对字符解码 并且会将;之后的字符到/字符删除
request.getRequestURI() -->原样输出
所以在校验的时候会产生认证的绕过:
从过滤器走出之后,可以看下面的调用栈,要进行服务功能的执行,分发器(Dispatcher)要选择相应的处理器进行选择。
org.springframework.web.servlet.DispatcherServlet#getHandler
所有映射的内容放在了注册中心处。
RequestMappingInfoHandlerMapping继承org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
在他的方法上下断点getHandlerInternal
虽然这里会移除分号,但是在spring security前面就会被过滤的。这里是spring-web-mvc的组件
这里的返回值是lookupPath是没有被解码的,可以正常映射到/admin/*的路径上。
感兴趣的后面也可以跟一下。在匹配上之后就是选择对应路径的方法进行反射调用,唤醒业务逻辑部分代码。
切换版本至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(); } }
这样业务逻辑就不会被绕过了。