漏洞描述
Apache Shiro before 1.13.0 or 2.0.0-alpha-4, maybe susceptible to a path traversal attack that results in an authentication bypass when used together with path rewriting
Mitigation:Update to Apache Shiro 1.13.0+ or 2.0.0-alpha-4+, or ensure blockSemicolon
is enabled (this is the default).
[1]
从漏洞描述可知,是路径穿越导致验证绕过。
path rewriting在spring中指类似“/path/{param}”的路由配置
漏洞条件
-
shiro < 1.13.0 or 2.0.0-alpha-4,
-
blockSemicolon == false
这个选项其实默认为ture,所以说条件还是很苛刻的。
漏洞复现
环境配置
shiro : 1.12.0
spring-boot: 2.7.4
pom.xml在必要配置基础上要添加以下依赖,使得springboot走AntPathMatcher路径匹配策略
<!-- CVE-2023-22602补丁在里面-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.12.0</version>
</dependency>
这个依赖与下面这个配置中ShiroRequestMappingConfig.class联合使用,可以让springBoot在获取查询路径时调用shiro的WebUtils.getPathWithinApplication
,从而使得获取到的路径一致
shiro:
@Configuration
@Import({ShiroBeanConfiguration.class,
ShiroWebConfiguration.class,
ShiroRequestMappingConfig.class
})
public class ShiroConfig{
@Bean
public IniRealm getIniRealm() {
return new IniRealm("classpath:shiro.ini");
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
MyShiroFilterFactoryBean bean = new MyShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/login");
bean.setSuccessUrl("/loginSuccess");
bean.setUnauthorizedUrl("/unauthorized");
LinkedHashMap<String, String> map = new LinkedHashMap<>();
//特例放前面
map.put("/file/anonUser/**","anon");
map.put("/file/**","authc");
map.put("/login", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
private class MyShiroFilterFactoryBean extends ShiroFilterFactoryBean{
@Override
public AbstractShiroFilter createInstance() throws Exception{
SecurityManager securityManager = getSecurityManager();
FilterChainManager manager = createFilterChainManager();
//设置BlockSemicolon
InvalidRequestFilter filter = (InvalidRequestFilter) manager.getFilters().get("invalidRequest");
filter.setBlockSemicolon(false);
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
}
class SpringShiroFilter extends AbstractShiroFilter {
public SpringShiroFilter(WebSecurityManager securityManager, FilterChainResolver filterChainResolver) {
setSecurityManager(securityManager);
setFilterChainResolver(filterChainResolver);
}
}
controller
@ResponseBody
@GetMapping("/file/{user}/{fileName}")
public String userFile(@PathVariable String user, @PathVariable String fileName) {
if (!fileName.isEmpty()){
return "get file from " + "/fileRoot/" + user + "/" + fileName;
}
return "no file";
}
@GetMapping("/file")
public String fileRoot(){
return "...."
}
测试
payload: /file/anonUser/..%3b
漏洞分析
-
获取请求路径
PathMatchingFilterChainResolver.getChian(...):
public static String getPathWithinApplication(HttpServletRequest request) { return normalize(removeSemicolon(getServletPath(request) + getPathInfo(request))); }
去除分号还是和以前一样,去除分号后面全部内容,包括分号
normalize()中能规范化“/../"但是不能规范化尾部的”/.."
-
匹配过滤链
根据我们的配置,我们必然是匹配到 map.put("/file/anonUser/**","anon");所以不用任何验证,但是由于InvalidRequestFilter作为
“Global Filter”会被添加到每个过滤链中,所以还要经过它://核心算法 //InvalidRequestFilter:: @Override protected boolean isAccessAllowed(ServletRequest req, ServletResponse response, Object mappedValue) throws Exception { HttpServletRequest request = WebUtils.toHttp(req); // check the original and decoded values return isValid(request.getRequestURI()) // user request string (not decoded) && isValid(request.getServletPath()) // decoded servlet part && isValid(request.getPathInfo()); // decoded path info (may be null) } private boolean isValid(String uri) { return !StringUtils.hasText(uri) || ( !containsSemicolon(uri) && !containsBackslash(uri) && !containsNonAsciiCharacters(uri)) && !containsTraversal(uri); }
从以上我们的配置已知,blockSemicolon == false,所以不用担心containsSemicolon(uri),我们的payload中没有反斜杠,也没有非Ascii字符,所以看containsTraversal(uri)
//指的是“/” private static final List<String> FORWARDSLASH = Collections.unmodifiableList(Arrays.asList("%2f", "%2F")); //“.” private static final List<String> PERIOD = Collections.unmodifiableList(Arrays.asList("%2e", "%2E")); private boolean containsTraversal(String uri) { if (isBlockTraversal()) { return !(isNormalized(uri) && PERIOD.stream().noneMatch(uri::contains) && FORWARDSLASH.stream().noneMatch(uri::contains)); } return false; } private boolean isNormalized(String path) { if (path == null) { return true; } for (int i = path.length(); i > 0;) { int slashIndex = path.lastIndexOf('/', i - 1); int gap = i - slashIndex; if (gap == 2 && path.charAt(slashIndex + 1) == '.') { return false; // ".", "/./" or "/." } if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') { return false; } i = slashIndex; } return true; }
因为PERIOD是编码后的点号,而payload中是明文,故不匹配,且payload中也没有"%2f",从payload("/file/anonUser/..%3b")可知,第一轮的gap== 6所以最终同样绕过。
所以最终到达资源点,由响应可知我们实际访问的目录是“/fileRoot",这相当于发出一个url == ”/file"的请求才能访问,但是现在因为路径穿越导致未经验证就访问到了,符合漏洞描述
“/file"需要验证:
补充
request.getServletPath()的数据是怎么来的?
定位到路径处理函数:org.apache.catalina.connector.CoyoteAdapter.postParseRequest(...)
只看关键代码:
-
第一个标红断点处,req.decodedURI(),获取的是从二进制数据解码后的明文数据,这时还没有url解码
-
到了标蓝处,这里是解析路径参数(path param,比如
http://example.com/users/42;action=edit;view=summary
)的,也就是在这里处理分号的,但如果对分号进行url编码,则可以绕过;protected void parsePathParameters(org.apache.coyote.Request req, Request request) { // Process in bytes (this is default format so this is normally a NO-OP req.decodedURI().toBytes(); ByteChunk uriBC = req.decodedURI().getByteChunk(); //如果用%3b代替“;”则semicolon == -1 int semicolon = uriBC.indexOf(';', 0); // Performance optimisation. Return as soon as it is known there are no // path parameters; if (semicolon == -1) { return; } // What encoding to use? Some platforms, eg z/os, use a default // encoding that doesn't give the expected result so be explicit Charset charset = connector.getURICharset(); .... while (semicolon > -1) { // Parse path param, and extract it from the decoded request URI int start = uriBC.getStart(); int end = uriBC.getEnd(); int pathParamStart = semicolon + 1; int pathParamEnd = ByteChunk.findBytes(uriBC.getBuffer(), start + pathParamStart, end, new byte[] {';', '/'}); String pv = null; if (pathParamEnd >= 0) { if (charset != null) { pv = new String(uriBC.getBuffer(), start + pathParamStart, pathParamEnd - pathParamStart, charset); } // Extract path param from decoded request URI byte[] buf = uriBC.getBuffer(); for (int i = 0; i < end - start - pathParamEnd; i++) { buf[start + semicolon + i] = buf[start + i + pathParamEnd]; } uriBC.setBytes(buf, start, end - start - pathParamEnd + semicolon); } else { if (charset != null) { pv = new String(uriBC.getBuffer(), start + pathParamStart, (end - start) - pathParamStart, charset); } uriBC.setEnd(start + semicolon); } .... if (pv != null) { int equals = pv.indexOf('='); if (equals > -1) { String name = pv.substring(0, equals); String value = pv.substring(equals + 1); request.addPathParameter(name, value); ..... } } semicolon = uriBC.indexOf(';', semicolon); } }
-
到了try语句块,由注释可知,这里才开始url解码
-
到了Normalization,就是对路径进行规范化:以下是方法注释,源码太长就不展开了
This method normalizes "\", "//", "/./" and "/../"
因此当路径带有 "/..%3b" 时,由于分号被url编码,所以绕过对分号的处理,然后解码后,变成”/..;"但是到规范化(Normalization)时”/..;"并不匹配上述注释中的任何一个,因此最终保留下来
漏洞修复
The InvalidRequestFilter is more flexible · apache/shiro@3b80f5c[2]
Reference
[1] Security Reports | Apache Shiro
[2] The InvalidRequestFilter is more flexible · apache/shiro@3b80f5c
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)