Java安全之Spring Security绕过总结

Java安全之Spring Security绕过总结

前言

bypass!bypass!bypass!

SpringSecurit使用

使用

@Configuration
@EnableWebSecurity //启用Web安全功能
public class AuthConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
						http.authorizeRequests() // 开启 HttpSecurity 配置
            .antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必须具备ADMIN角色
            .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 该模式需要ADMIN或USER角色
            .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色
            .anyRequest().authenticated() // 用户访问其它URL都必须认证后访问(登录后访问)
            .and().formLogin().loginProcessingUrl("/login").permitAll() // 开启表单登录并配置登录接口
            .and().csrf().disable(); // 关闭csrf



        return http.build();
    }
方法 描述
access(String) 如果给定的SpEL表达式计算结果为true,就允许访问
anonymous() 允许匿名用户访问
authenticated() 允许认证过的用户访问
denyAll() 无条件拒绝所有访问
fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问
hasAnyAuthority(String…) 如果用户具备给定权限中的某一个的话,就允许访问
hasAnyRole(String…) 如果用户具备给定角色中的某一个的话,就允许访问
hasAuthority(String) 如果用户具备给定权限的话,就允许访问
hasIpAddress(String) 如果请求来自给定IP地址的话,就允许访问
hasRole(String) 如果用户具备给定角色的话,就允许访问
not() 对其他访问方法的结果求反
permitAll() 无条件允许访问
rememberMe() 如果用户是通过Remember-me功能认证的,就允许访问

​ 也可以通过集成WebSecurityConfigurerAdapter类的方式来configure()方法来制定Web安全的细节。

1、configure(WebSecurity):通过重载该方法,可配置Spring Security的Filter链。

2、configure(HttpSecurity):通过重载该方法,可配置如何通过拦截器保护请求。

Spring Security 支持的所有SpEL表达式如下:

安全表达式 计算结果
authentication 用户认证对象
denyAll 结果始终为false
hasAnyRole(list of roles) 如果用户被授权指定的任意权限,结果为true
hasRole(role) 如果用户被授予了指定的权限,结果 为true
hasIpAddress(IP Adress) 用户地址
isAnonymous() 是否为匿名用户
isAuthenticated() 不是匿名用户
isFullyAuthenticated 不是匿名也不是remember-me认证
isRemberMe() remember-me认证
permitAll 始终true
principal 用户主要信息对象

configure(AuthenticationManagerBuilder):通过重载该方法,可配置user-detail(用户详细信息)服务。

方法 描述
accountExpired(boolean) 定义账号是否已经过期
accountLocked(boolean) 定义账号是否已经锁定
and() 用来连接配置
authorities(GrantedAuthority…) 授予某个用户一项或多项权限
authorities(List) 授予某个用户一项或多项权限
authorities(String…) 授予某个用户一项或多项权限
credentialsExpired(boolean) 定义凭证是否已经过期
disabled(boolean) 定义账号是否已被禁用
password(String) 定义用户的密码
roles(String…) 授予某个用户一项或多项角色

用户存储方式

1、使用基于内存的用户存储:通过inMemoryAuthentication()方法,我们可以启用、配置并任意填充基于内存的用户存储。并且,我们可以调用withUser()方法为内存用户存储添加新的用户,这个方法的参数是username。withUser()方法返回的是UserDetailsManagerConfigurer.UserDetailsBuilder,这个对象提供了多个进一步配置用户的方法,包括设置用户密码的password()方法以及为给定用户授予一个或多个角色权限的roles()方法。需要注意的是,roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会添加一个ROLE_前缀,并将其作为权限授予给用户。因此上诉代码用户具有的权限为:ROLE_USER,ROLE_ADMIN。而借助passwordEncoder()方法来指定一个密码转码器(encoder),我们可以对用户密码进行加密存储。

@Configuration
@EnableWebSecurity //启用Web安全功能
public class AuthConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
						http.authorizeRequests() // 开启 HttpSecurity 配置
            .antMatchers("/admin/**").hasRole("ADMIN") // admin/** 模式URL必须具备ADMIN角色
            .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')") // 该模式需要ADMIN或USER角色
            .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 需ADMIN和DBA角色
            .anyRequest().authenticated() // 用户访问其它URL都必须认证后访问(登录后访问)
            .and().formLogin().loginProcessingUrl("/login").permitAll() // 开启表单登录并配置登录接口
            .and().csrf().disable(); // 关闭csrf



        return http.build();

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("root").password("123").roles("ADMIN","DBA")
        .and()
        .withUser("admin").password("123").roles("ADMIN","USER")
        .and()
        .withUser("xxx").password("123").roles("USER");
    }
          }

2、基于数据库表进行认证:用户数据通常会存储在关系型数据库中,并通过JDBC进行访问。为了配置Spring Security使用以JDBC为支撑的用户存储,我们可以使用jdbcAuthentication()方法,并配置他的DataSource,这样的话,就能访问关系型数据库了。

3、基于LDAP进行认证:为了让Spring Security使用基于LDAP的认证,我们可以使用ldapAuthentication()方法。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置 user-detail 服务
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	// 基于 LDAP 配置认证
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare()
                .passwordAttribute("password")
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

使用远程ldap

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                // 返回一个ContextSourceBuilder 对象
                .contextSource()
                // 指定远程 LDAP 服务器 的 地址
                .url("ldap://xxx.com:389/dc=xxx,dc=com");
                
    }
}
ldapAuthentication():表示,基于LDAP的认证。
userSearchBase():为查找用户提供基础查询
userSearchFilter():提供过滤条件,用于搜索用户。
groupSearchBase():为查找组指定了基础查询。
groupSearchFilter():提供过滤条件,用于组。
passwordCompare():希望通过 密码比对 进行认证。
passwordAttribute():指定 密码 保存的属性名字,默认:userPassword。
passwordEncoder():指定密码转换器。

hasRole 和 hasAuthority

http.authorizeRequests()
        .antMatchers("/admin/**").hasAuthority("admin")
        .antMatchers("/user/**").hasAuthority("user")
        .anyRequest().authenticated()

http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().authenticated()

实际上这两个的效果都是一样的

antMatchers 配置认证绕过

package person.xu.vulEnv;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class AuthConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
   
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/test").access("hasRole('ADMIN')")
                .antMatchers("/**").permitAll();
     					//.antMatchers("/**").access("anonymous");



        // @formatter:on
        return http.build();
    }


    // @formatter:off
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
    // @formatter:on
}

绕过:http://127.0.0.1:8012/test/

mvcMatchers("/test").access("hasRole('ADMIN')") 或者使用 antMatchers("/test/**").access("hasRole('ADMIN')") 写法防止认证绕过。

regexMatchers 配置认证绕过

public class AuthConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .csrf().disable()
                .authorizeRequests()
                .regexMatchers("/test").access("hasRole('ADMIN')")
                .antMatchers("/**").access("anonymous");



        // @formatter:on
        return http.build();
    }

http://127.0.0.1:8012/test?http://127.0.0.1:8012/test/

Matchers没使用类似/test.*的方式,在传入/test?时候,正则会匹配不上,不会命中/test的规则。

安全写法

.regexMatchers("/test.*?").access("hasRole('ADMIN')")

useSuffixPatternMatch 绕过

低版本 的 spring-webmvc 及其相关组件,包括:

spring-webmvc <= 5.2.4.RELEASE
spring-framework <= 5.2.6.RELEASE
spring-boot-starter-parent <= 2.2.5.RELEASE

在代码中定义的 useSuffixPatternMatch 配置默认值为 true ,表示使用后缀匹配模式匹配路径。

/path/abc 路由也会允许 /path/abcd.ef/path/abcde.f 等增加 .xxx 后缀形式的路径匹配成功。

漏洞修复:

使用高版本的 spring-webmvc 能有效避免问题。

https://www.jianshu.com/p/e6655328b211

CVE-2022-22978

影响版本

Spring Security 5.5.x < 5.5.7

Spring Security 5.6.x < 5.6.4

漏洞分析

Spring在加载的时候会来到DelegatingFilterProxy,DelegatingFilterProxy根据targetBeanName从Spring 容器中获取被注入到Spring 容器的Filter实现类,在DelegatingFilterProxy配置时一般需要配置属性targetBeanName。DelegatingFilterProxy就是一个对于servlet filter的代理,用这个类的好处主要是通过Spring容器来管理servlet filter的生命周期,

还有就是如果filter中需要一些Spring容器的实例,可以通过spring直接注入,
另外读取一些配置文件这些便利的操作都可以通过Spring来配置实现。

@Override
protected void initFilterBean() throws ServletException {
	synchronized (this.delegateMonitor) {
		if (this.delegate == null) {
			// If no target bean name specified, use filter name.
                        //当Filter配置时如果没有设置targentBeanName属性,则直接根据Filter名称来查找
			if (this.targetBeanName == null) {
				this.targetBeanName = getFilterName();
			}

			WebApplicationContext wac = findWebApplicationContext();
			if (wac != null) {
                                //从Spring容器中获取注入的Filter的实现类
				this.delegate = initDelegate(wac);
			}
		}
	}
}
 
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		//从Spring 容器中获取注入的Filter的实现类
		Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}
@Override
protected void initFilterBean() throws ServletException {
	synchronized (this.delegateMonitor) {
		if (this.delegate == null) {
			// If no target bean name specified, use filter name.
                        //当Filter配置时如果没有设置targentBeanName属性,则直接根据Filter名称来查找
			if (this.targetBeanName == null) {
				this.targetBeanName = getFilterName();
			}

			WebApplicationContext wac = findWebApplicationContext();
			if (wac != null) {
                                //从Spring容器中获取注入的Filter的实现类
				this.delegate = initDelegate(wac);
			}
		}
	}
}
 
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
		//从Spring 容器中获取注入的Filter的实现类
		Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}

image-20221017080808111

从Spring 容器中获取注入的Filter的实现类,然后调用org.springframework.web.filter.DelegatingFilterProxy#invokeDelegate方法

image-20221017081128574

来到org.springframework.security.web.FilterChainProxy#doFilterInternal

    private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest)request);
        HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse)response);
        List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
        if (filters != null && filters.size() != 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(LogMessage.of(() -> {
                    return "Securing " + requestLine(firewallRequest);
                }));
            }

            VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
            virtualFilterChain.doFilter(firewallRequest, firewallResponse);
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace(LogMessage.of(() -> {
                    return "No security for " + requestLine(firewallRequest);
                }));
            }

            firewallRequest.reset();
            chain.doFilter(firewallRequest, firewallResponse);
        }
    }

this.firewall默认装载的是StrictHttpFirewall,而不是DefaultHttpFirewall。反而DefaultHttpFirewall的校验没那么严格

FirewalledRequest是封装后的请求类,但实际上该类只是在HttpServletRequestWrapper的基础上增加了reset方法。当spring security过滤器链执行完毕时,由FilterChainProxy负责调用该方法,以便重置全部或者部分属性。
FirewalledResponse是封装后的响应类,该类主要重写了sendRedirect、setHeader、addHeader以及addCookie四个方法,在每一个方法中都对其参数进行校验,以确保参数中不含有\r和\n。

在FilterChainProxy属性定义中,默认创建的HttpFirewall实例就是StrictHttpFirewall。
FilterChainProxy是在WebSecurity#performBuild方法中构建的,而WebSecurity实现了ApplicationContextAware接口,并实现了接口中的setApplicationContext方法,在该方法中,从spring容器中查找到HttpFirewall对并赋值给httpFirewall属性。最终在performBuild方法中,将FilterChainProxy对象构建成功后,如果httpFirewall不为空,就把httpFirewall配置给FilterChainProxy对象。
因此,如果spring容器中存在HttpFirewall实例,则最终使用spring容器提供的实例;如果不存在,则使用FilterChainProxy中默认定义的StrictHttpFirewall。

org.springframework.security.web.firewall.StrictHttpFirewall#getFirewalledRequest

 public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        this.rejectForbiddenHttpMethod(request);
        this.rejectedBlocklistedUrls(request);
        this.rejectedUntrustedHosts(request);
        if (!isNormalized(request)) {
            throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
        } else {
            String requestUri = request.getRequestURI();
            if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
                throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
            } else {
                return new StrictFirewalledRequest(request);
            }
        }
    }

方法会判断请求的方法是否是可允许的

org.springframework.security.web.firewall.StrictHttpFirewall#rejectForbiddenHttpMethod

 private void rejectForbiddenHttpMethod(HttpServletRequest request) {
        if (this.allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) {
            if (!this.allowedHttpMethods.contains(request.getMethod())) {
                throw new RequestRejectedException("The request was rejected because the HTTP method \"" + request.getMethod() + "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
            }
        }
    }


    private static Set<String> createDefaultAllowedHttpMethods() {
        Set<String> result = new HashSet();
        result.add(HttpMethod.DELETE.name());
        result.add(HttpMethod.GET.name());
        result.add(HttpMethod.HEAD.name());
        result.add(HttpMethod.OPTIONS.name());
        result.add(HttpMethod.PATCH.name());
        result.add(HttpMethod.POST.name());
        result.add(HttpMethod.PUT.name());
        return result;
    }

org.springframework.security.web.firewall.StrictHttpFirewall#rejectedBlocklistedUrls

 private void rejectedBlocklistedUrls(HttpServletRequest request) {
        Iterator var2 = this.encodedUrlBlocklist.iterator();

        String forbidden;
        do {
            if (!var2.hasNext()) {
                var2 = this.decodedUrlBlocklist.iterator();

                do {
                    if (!var2.hasNext()) {
                        return;
                    }

                    forbidden = (String)var2.next();
                } while(!decodedUrlContains(request, forbidden));

                throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
            }

            forbidden = (String)var2.next();
        } while(!encodedUrlContains(request, forbidden));

        throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
    }

encodedUrlBlocklist = {HashSet@7373}  size = 18
 0 = "//"
 1 = ""
 2 = "%2F%2f"
 3 = "%2F%2F"
 4 = "%00"
 5 = "%25"
 6 = "%2f%2f"
 7 = "%2f%2F"
 8 = "%5c"
 9 = "%5C"
 10 = "%3b"
 11 = "%3B"
 12 = "%2e"
 13 = "%2E"
 14 = "%2f"
 15 = "%2F"
 16 = ";"
 17 = "\"
decodedUrlBlocklist = {HashSet@7374}  size = 16
 0 = "//"
 1 = ""
 2 = "%2F%2f"
 3 = "%2F%2F"
 4 = "%00"
 5 = "%"
 6 = "%2f%2f"
 7 = "%2f%2F"
 8 = "%5c"
 9 = "%5C"
 10 = "%3b"
 11 = "%3B"
 12 = "%2f"
 13 = "%2F"
 14 = ";"
 15 = "\"
 private static boolean encodedUrlContains(HttpServletRequest request, String value) {
        return valueContains(request.getContextPath(), value) ? true : valueContains(request.getRequestURI(), value);
    }

    private static boolean decodedUrlContains(HttpServletRequest request, String value) {
        if (valueContains(request.getServletPath(), value)) {
            return true;
        } else {
            return valueContains(request.getPathInfo(), value);
        }
    }

private static boolean valueContains(String value, String contains) {
        return value != null && value.contains(contains);
    }

优先从request.getContextPath()里面取值,如果存在黑名单,即返回flase抛异常。

org.springframework.security.web.firewall.StrictHttpFirewall#rejectedUntrustedHosts

private void rejectedUntrustedHosts(HttpServletRequest request) {
    String serverName = request.getServerName();
    if (serverName != null && !this.allowedHostnames.test(serverName)) {
        throw new RequestRejectedException("The request was rejected because the domain " + serverName + " is untrusted.");
    }
}

org.springframework.security.web.firewall.StrictHttpFirewall#isNormalized(java.lang.String)

  private static boolean isNormalized(String path) {
        if (path == null) {
            return true;
        } else {
            int slashIndex;
            for(int i = path.length(); i > 0; i = slashIndex) {
                slashIndex = path.lastIndexOf(47, i - 1);
                int gap = i - slashIndex;
                if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
                    return false;
                }

                if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
                    return false;
                }
            }

            return true;
        }
    }

检查request.getRequestURI() request.getContextPath() request.getServletPath() request.getPathInfo() 不允许出现., /./ 或者 /.

request.getRequestURI();调用org.springframework.security.web.firewall.StrictHttpFirewall#containsOnlyPrintableAsciiCharacters

   private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
        int length = uri.length();

        for(int i = 0; i < length; ++i) {
            char ch = uri.charAt(i);
            if (ch < ' ' || ch > '~') {
                return false;
            }
        }

        return true;
    }

不允许出现的特殊字符

!
"
#
$
%
&
'
(
)
*
+
,
-
.
/
:
;
<
=
>
?
@
[
\
]
^
_
`
{
|
}
~

获取filters,调用virtualFilterChain.doFilter走入下面会遍历调用doFilter,走入 Filter执行链

image-20221017103415312

  public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
            if (this.currentPosition == this.size) {
                if (FilterChainProxy.logger.isDebugEnabled()) {
                    FilterChainProxy.logger.debug(LogMessage.of(() -> {
                        return "Secured " + FilterChainProxy.requestLine(this.firewalledRequest);
                    }));
                }

                this.firewalledRequest.reset();
                this.originalChain.doFilter(request, response);
            } else {
                ++this.currentPosition;
                Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);
                if (FilterChainProxy.logger.isTraceEnabled()) {
                    FilterChainProxy.logger.trace(LogMessage.format("Invoking %s (%d/%d)", nextFilter.getClass().getSimpleName(), this.currentPosition, this.size));
                }

                nextFilter.doFilter(request, response, this);
            }
        }

org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    this.invoke(new FilterInvocation(request, response, chain));
}

   public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        if (this.isApplied(filterInvocation) && this.observeOncePerRequest) {
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } else {
            if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
                filterInvocation.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
            }

            InterceptorStatusToken token = super.beforeInvocation(filterInvocation);

            try {
                filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
            } finally {
                super.finallyInvocation(token);
            }

            super.afterInvocation(token, (Object)null);
        }
    }

调用 super.beforeInvocation(filterInvocation);

image-20221017105720870

org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource#getAttributes

image-20221017110034575

org.springframework.security.web.util.matcher.RegexRequestMatcher#matches

image-20221017110542737

进行正则匹配。

这里先换成漏洞的配置

@Configuration
@EnableWebSecurity
public class AuthConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests((authorize) -> authorize
                        .regexMatchers("/admin/.*").authenticated()
                )
                .httpBasic(withDefaults())
                .formLogin(withDefaults());

        return http.build();
    }

使用regexMatchers即会用org.springframework.security.web.util.matcher.RegexRequestMatcher#matches类来处理编写的规则

image-20221017120907141

访问/admin/123是命中这条规则的,在配置里面这个规则是需要走认证的。

但是访问``/admin/123%0d`这里的正则匹配是为flase,并没有命中这条规则,从而走到下一条规则从而实现绕过。

image-20221017121212680

这里的问题就在于用了.*的正则去匹配,而传入数据%0d的话是匹配不上的。Pattern默认的规则是不匹配\r\n等的。

public class test {
    public static void main(String[] args) {
        String regex = "a.*b";

        //输出true,指定Pattern.DOTALL模式,可以匹配换行符。
        Pattern pattern1 = Pattern.compile(regex,Pattern.DOTALL);
        boolean matches1 = pattern1.matcher("aaabbb").matches();

        System.out.println(matches1);
        boolean matches2 = pattern1.matcher("aa\nbb").matches();

        System.out.println(matches2);
        //输出false,默认点(.)没有匹配换行符
        Pattern pattern2 = Pattern.compile(regex);
        boolean matches3 = pattern2.matcher("aaabbb").matches();
        boolean matches4 = pattern2.matcher("aa\nbb").matches();
        System.out.println(matches3);
        System.out.println(matches4);

    }
}


//true
//true
//true
//false

但是如果加上Pattern.DOTALL参数的话即便有\n,也会进行匹配。所以后面版本修复使用到了Pattern.DOTALL

https://github.com/spring-projects/spring-security/commit/70863952aeb9733499027714d38821db05654856

参考

Spring Security的一个简单auth bypass和一些小笔记

spring-security 三种情况下的认证绕过

Spring security中的HttpFirewall

Spring Security原理篇(四) FilterChainProxy

spring security源码分析

SpringSecurity

github commit

结尾

Spring Securit要比Shiro要安全不少,自带的StrictHttpFirewall把一些可测试的危险字符限制比较死。

posted @ 2022-10-17 12:57  nice_0e3  阅读(7588)  评论(1编辑  收藏  举报