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

0x00 无内鬼

很早以前审计过Spring Security的源码,个人感觉还是相对于shiro安全很多的,不过后面shiro也引入了一些字符的拦截机制,相比于从前,安全性已经提高很多了,最起码,出现的新洞都很水吧。

Spring Security最大的优势在于它实至名归的Security,它集成了很多东西,认证、鉴权、anti csrf、session manager、url字符拦截等等。

这篇文章会分享一个Spring Security存在的auth bypass问题,它不仅仅出现在Spring Security中,在曾经的Apache Shiro中,亦或者现在的Spring interceptor,都存在这个问题。而且,在开发编写认证鉴权配置时,很大的概率会出现。

附带着,也说了下在SpringBoot 1.x.x版本中,Spring Security、Apache Shiro、Spring Interceptor某些权限配置下的通用bypass。

0x01 小笔记

先来点小笔记,简单的把Spring Security关键点列出来一下,一个是Spring Security filter chains的默认(为什么叫默认,是因为可以自己定顺序,也可以自定义的关闭一些filter)执行顺序,另一个是url字符拦截点,最后一个是url匹配鉴权点。

  1. 默认filter和顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    -> DelegatingFilterProxy 

    -> FilterChainProxy(url字符拦截点)

    -> WebAsyncManagerIntegrationFilter

    -> SecurityContextPersistenceFilter

    -> HeaderWriterFilter

    -> CsrfFilter

    -> LogoutFilter

    -> AbstractAuthenticationProcessingFilter

    -> UsernamePasswordAuthenticationFilter

    -> DefaultLoginPageGeneratingFilter

    -> DefaultLogoutPageGeneratingFilter

    -> SecurityContextHolderAwareRequestFilter

    -> AnonymousAuthenticationFilter

    -> SessionManagementFilter

    -> ExceptionTranslationFilter

    -> FilterSecurityInterceptor(url匹配鉴权点)
  2. 检查uri的代码点

    1
    2
    3
    4
    5
    6
    7
    org.springframework.security.web.FilterChainProxy#doFilterInternal

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

    -org.springframework.security.web.firewall.StrictHttpFirewall#rejectedBlacklistedUrls

    -org.springframework.security.web.firewall.StrictHttpFirewall#isNormalized

rejectedBlacklistedUrls方法:

url不能包含以下子字符串(encodedUrlBlacklist),会检查request.getContextPath()和request.getRequestURI():

1
// � %2F%2f %2F%2F %00 %25 %2f%2f %2f%2F %5c %5C %3b %3B %2e %2E %2f %2F ; \

url不能包含以下子字符串(decodedUrlBlacklist),会检查request.getServletPath()和request.getPathInfo():

1
// � %2F%2f %2F%2F %00 % %2f%2f %2f%2F %5c %5C %3b %3B %2f %2F ; \

isNormalized:

不允许出现”.”, “/./“ or “/.”,检查request.getRequestURI() request.getContextPath() request.getServletPath() request.getPathInfo()

containsOnlyPrintableAsciiCharacters:

不允许出现 小于 ‘\u0020’ 或大于 ‘\u007e’ 的字符,request.getRequestURI()

上面的字符串拦截,还是挺苛刻的,但实际上,写一个特定的接口,加上认证鉴权,然后burp去遍历一下,大概率能fuzz出来。

  1. url匹配鉴权点

栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke

org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation

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

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

org.springframework.security.web.util.matcher.AntPathRequestMatcher.SpringAntMatcher#matches

org.springframework.util.AntPathMatcher#match

org.springframework.util.AntPathMatcher#doMatch

image
image
image
image

0x02 auth bypass

上图:
image
image
image
image

image
image
少了

1
2
curl 'http://127.0.0.1:8080/api/list'  ->  403
curl 'http://127.0.0.1:8080/api/list/' -> 200

可以看到,在url最后加个/后缀,就能auth bypass了,主要原因是因为,加了/后缀,spring还能路由到controller接口,并且spring security的AntPathRequestMatcher在默认构造情况下是使用fullMatch = true的匹配模式

image
image
image

在匹配时,会先根据/斜杆,把url和pattern进行分组,比如

1
2
3
url: /user/threedr3am/info -> "user","threedr3am","info"         -  size=3

pattern: /user/*/info -> "user","*","info" - size=3

依次匹配,星号能匹配/斜杆以外的字符,所以,这个url和pattern的每一个元素能匹配上,且数组size也为3,那么,这个url和pattern就会返回true,但我们再加上/斜杆后缀的时候,情况变了,变成了

1
2
3
url: /user/threedr3am/info/ -> "user","threedr3am","info",""   - size=4

pattern: /user/*/info -> "user","*","info" - size=3

那么,就无法匹配上了,故返回了false。无论是/api/,还是/admin,结果也是一样的,除非/api/改成/api/*,或者同时存在/api/和/api/*/。

所以,通过加/斜杆后缀,可以成功绕过匹配。Spring interceptor和老版本的Apache Shiro也是一样的,大差不差,利用这个特性也是能绕过匹配的。

PS: 并且在SpringBoot 1.x.x版本的某个加后缀的特性中,通过加.html等,也能绕过Spring Security、Apache Shiro最新版(现在应该是1.8.0)、Spring interceptor。

例:

1
2
3
url: /user/threedr3am/info.html

pattern: /user/*/info

0x03 开发&审计经验

一般情况下,开发者配置这些鉴权时,最好遵循一个原则,就是”优先使用/**认证兜底,明确哪些接口无需认证,而不是明确哪些接口需要认证”,什么意思呢?

就是,比如你写一个web系统,存在很多接口(/api/{name}/info、/api/user/list、/api/status),我优先使用/*去兜底,让所有接口需要认证,然后再明确哪些接口无需认证就可以访问,比如/api/status。而不是说,我明确/api//info需要认证,/api/user/*需要认证,这样的话,会很大几率存在一些绕过的问题。

讲得比较模糊,大概有以下两个例子说明:

没问题:

1
2
3
/api/status       - 无需认证

/** - 需要认证

存在问题:

1
2
3
4
5
/api/*/info       - 需要认证

/api/user/* - 需要认证

/api/status - 无需认证

也就是说,如果你遇到Spring Security、Apache Shiro、Spring Interceptor,你可以根据这个原则,去快速判断,这个web系统的鉴权认证配置,会不会有问题。有一个高star的开源项目alibaba-canal,它使用的Spring Interceptor配置,就是遵循这样的一个原则,可以参考以下。

0x04 test uri匹配(fullMatch = true)

对一些情况进行了简单的test后,得到的一个结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

path: /api/login - pattern: /api/login - pass

path: /api/login - pattern: /api/login/ - no

path: /api/login/ - pattern: /api/login - no

path: /api/login/ - pattern: /api/login/ - pass

path: /api/login - pattern: /api/login/* - no

path: /api/login/ - pattern: /api/login/* - pass

path: /api/login - pattern: /api/login/** - pass

path: /api/login - pattern: /api/login/a/* - no

path: /api/login/ - pattern: /api/login/a/* - no

path: /api/login - pattern: /api/login/a/** - no

path: /api/login/a - pattern: /api/login - no

path: /api/login/a - pattern: /api/login/** - pass



path: /user/threedr3am/info - pattern: /user/*/info - pass

path: /user/threedr3am/info/ - pattern: /user/*/info - no



path: /user/list - pattern: /user/* - pass

path: /user/list/ - pattern: /user/* - no

path: /user/list - pattern: /user/** - pass

path: /user/list/ - pattern: /user/** - pass

如果认证鉴权配置使用了上面的pattern,则pass表示能匹配上,意味无法绕过,no表示不能匹配,可绕过

0x05 other

这是个很水的bypass,理论上应该能在url加入一些特殊字符,使其无法匹配上(默认情况下是fullMatch = true),进行绕过,这样才是最优的bypass,听说rr有,而我,不会,太菜了。