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把一些可測試的危險字符限制比較死。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章