
shiro-cve-2020-17523 취약점의 두 가지 우회 방식 분석 및 관련 취약점 환경
Apache Shiro는 강력하고 사용하기 쉬운 Java 보안 프레임워크로, 인증, 권한 부여, 암호 및 세션 관리를 수행합니다. Shiro의 이해하기 쉬운 API를 사용하면 가장 작은 모바일 애플리케이션에서 가장 큰 네트워크 및 엔터프라이즈 애플리케이션에 이르기까지 모든 애플리케이션을 빠르고 쉽게 구현할 수 있습니다.
Spring과 함께 사용할 때, 특정 권한 매칭 규칙 하에서 공격자는 특수하게 조작된 HTTP 요청 패킷을 구성하여 인증을 우회할 수 있습니다.
영향 범위: Apache Shiro < 1.7.1
shiro 1.7.0
https://github.com/jweny/shiro-cve-2020-17523 두 가지 방식의 취약점 환경이 모두 업데이트되었습니다.
방식 1:
http://127.0.0.1:8080/admin/%20 또는 http://127.0.0.1:8080/admin/%20/
공백 등의 빈 문자를 사용하여 shiro 인증을 우회할 수 있습니다.

방식 2:
p0desta 님과의 논의 결과, 또 다른 특수 시나리오에서의 이용 방식이 발견되었습니다.
http://127.0.0.1:8080/admin/%2e 또는 http://127.0.0.1:8080/admin/%2e/
하지만 . (그리고 /)는 Spring의 경로 매칭 규칙에서 경로 구분자로 취급되며, 일반 문자로 매칭되지 않습니다. 따라서 기본 조건에서 /admin/.에 접근하면 404가 반환됩니다.
그러나 전체 경로 모드가 활성화된 경우 setAlwaysUseFullPath(true)에서는 정상적으로 매칭됩니다.

Shiro에서 URL을 가져오고 매칭하는 부분은 org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain에 있습니다.
먼저 getChain 메서드를 간단히 살펴보겠습니다.


이 메서드는 먼저 requestURI가 /로 끝나는지 확인하고, 그렇다면 마지막 /를 제거합니다.
그런 다음 경로 매칭 루프에서 먼저 경로 규칙 pathPattern이 /로 끝나는지 확인하고, 그렇다면 역시 제거합니다. 그런 다음 pathMatches() 메서드를 호출하여 경로 매칭을 수행합니다.
따라서 두 가지 이용 방식에서 /로 끝나는지 여부는 중요하지 않습니다. getChain 메서드를 통과하면 제거되기 때문입니다.
pathMatches() 메서드를 살펴보겠습니다.
Evaluate를 열어 pathMatches("/admin/*","/admin/1")와 pathMatches("/admin/*","/admin/ ")를 각각 계산해 보면, 전자는 정상적으로 매칭되고 후자는 매칭에 실패합니다.


디버깅을 시작합니다. 오랜 F7 과정을 거쳐 doMatch("/admin/*","/admin/ ")에 도달합니다. tokenizeToStringArray가 반환한 pathDirs에 두 번째 경로 레벨이 없는 것을 확인할 수 있습니다. 따라서 /admin/*와 /admin 이 매칭되지 않습니다.

tokenizeToStringArray 메서드를 따라가 보면, 해당 메서드를 호출할 때 trimTokens 매개변수가 true임을 확인할 수 있습니다.

tokenizeToStringArray 메서드는 trimTokens가 true일 때 trim() 처리를 거치므로 공백이 제거됩니다. 다시 getChain으로 돌아가면 마지막 /가 제거됩니다. 따라서 tokenizeToStringArray가 반환한 pathDirs에 두 번째 경로 레벨이 없습니다.

요약하자면, 취약한 shiro 버전에서는 tokenizeToStringArray 메서드 호출 시 trimTokens 매개변수가 기본적으로 true이므로 공백이 trim() 처리되어 제거됩니다. 다시 getChain으로 돌아가면 마지막 /가 제거되므로 /admin과 /admin/*의 매칭이 실패하여 인증 우회가 발생합니다. 반면 Spring이 수신하는 접근 경로는 /admin/%20이므로 정상적인 로직으로 응답을 반환하여 권한 우회가 발생합니다.
두 번째 방식인 /.와 /./을 보면, 익숙한 메서드가 떠오르지 않나요? 바로 normalize()입니다.

간단히 번역하면 다음과 같습니다.
| 조건 | 예시 |
|---|---|
| 슬래시를 백슬래시로 처리 | \ -> / |
| 이중 슬래시를 슬래시로 처리 | // -> / |
| /. 또는 /..로 끝나면 끝에 / 추가 | /. -> /./ /.. -> /../ |
| /./ 정규화 처리 |
따라서 /admin/.는 /admin/./로 처리된 후 /admin/이 됩니다.

org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver#getChain 처리를 거쳐, /로 끝나므로 마지막 /를 제거하여 /admin이 됩니다. /admin은 /admin/*와 매칭되지 않으므로 shiro 인증을 우회합니다.

이때 Spring이 수신한 요청은 /admin/.입니다. 전체 경로 매칭이 활성화되지 않은 경우, Spring에서 .와 /는 경로 구분자로 취급되어 경로 매칭에 참여하지 않습니다. 따라서 매핑을 찾을 수 없어 404를 반환합니다.

전체 경로 매칭이 활성화된 경우 전체 URL을 매칭하므로 Spring이 200을 반환합니다.
여기에 전체 경로 매칭을 활성화하는 코드를 첨부합니다.
@SpringBootApplication
public class SpringbootShiroApplication extends SpringBootServletInitializer implements BeanPostProcessor {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootShiroApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootShiroApplication.class, args);
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof RequestMappingHandlerMapping) {
((RequestMappingHandlerMapping) bean).setAlwaysUseFullPath(true);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}
위 분석을 통해 shiro 권한 우회의 원인은 두 가지입니다.
tokenizeToStringArray 함수가 공백을 올바르게 처리하지 않음./를 처리하는 로직이 경로 매칭 루프 이전에 위치해서는 안 됨.따라서 공식 수정 방안은 다음과 같습니다.
https://github.com/apache/shiro/commit/0842c27fa72d0da5de0c5723a66d402fe20903df
tokenizeToStringArray의 trimTokens 매개변수를 false로 설정. 
/를 제거하는 로직 조정. 먼저 원래 경로를 매칭하고, 매칭 실패 시에만 마지막 /를 제거하는 로직을 수행하도록 수정. 
원리적으로 trim()은 문자열 앞뒤의 모든 공백 문자(whitespace)를 제거합니다. 공백은 그중 하나일 뿐입니다. 그러나 테스트 결과 공백 외의 다른 공백 문자(예: %08, %09, %0a)는 spring+tomcat에서 처리 시 400을 반환합니다.
따라서 첫 번째 방식에서는 공백 외에 유용한 payload는 아직 발견되지 않았습니다.
https://github.com/apache/shiro/commit/0842c27fa72d0da5de0c5723a66d402fe20903df
| /./ -> / |
| 경로 점프 | /aaa/../bbb -> /bbb |