浅谈SpringWeb URI中的任意文件下载
2023-5-25 09:32:13 Author: 星冥安全(查看原文) 阅读量:13 收藏

在实际业务中经常会存在把文件的名称作为URI路径中的一部分去处理,在某些情况下是存在风险的。

文件下载是十分常见的业务。如果文件名参数可控,且系统未对参数进行过滤或者过滤不全的话,可能会导致任意文件下载的风险。一般来说文件名参数主要来源有两个,一个是request Parameter,另外一种是把文件名作为路径的一部分进行处理,通过解析URI中的内容来得到对应的文件名,例如下面的例子:

下面根据看看Spring Web中是如何处理的,看看有没有利用的可能。

首先看看在Spring中,常见的获取URI Path有哪几种方式,简单看下具体的原理:

1.1 HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性

以spring-webmvc-5.3.26为例,在Spring中,会通过getHandlerInternal方法从request对象中获取请求的path并根据path找到handlerMethod,首先调用initLookupPath方法初始化请求映射的路径:

获取到路径后,调用lookupHandlerMethod方法,首先直接根据路径获取对应的Mapping,获取不到的话调用addMatchingMappings遍历所有的ReuqestMappingInfo对象并进行匹配:

匹配到后,这里有一个处理,把最佳匹配的方法放进request对象对应的属性里面,然后调用handleMatch方法:

在handleMacth方法中,将前面initLookupPath方法调用后返回的请求映射的路径放到request的HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性中去:

也就是说,在Controller层可以通过request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)的方式获取到请求的全路径,然后再通过AntPathMatcher的extractPathWithinPattern方法提取出动态匹配的路径:

  @RequestMapping("/file/download/**")
public void fileDownload(HttpServletResponse response,HttpServletRequest request) throws IOException {
String reqPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
String path = new AntPathMatcher().extractPathWithinPattern(bestMatchPattern, reqPath);
......
}

例如上面的例子,当请求/file/download/../../etc/passwd时,最终path参数的值为../../etc/passwd

1.2 {pathVariable:正则表达式(可选)}

通过如下方式同样也可以获取URI Path内容,同时由于Spring版本的迭代,存在两个解析器AntPathMatcher和PathPattern,分别查看具体的实现:

@RequestMapping("/file/download/{path:.*}")  
public void fileDownload(@PathVariable("path") String path,HttpServletResponse response) throws IOException {
......
}

1.2.1 AntPathMatcher解析

具体是通过org.springframework.util.AntPathMatcher#doMatch方法进行解析。

首先调用tokenizePattern()方法将pattern分割成了String数组,如果是全路径并且区分大小写,那么就通过简单的字符串检查,看看path是否有潜在匹配的可能,没有的话返回false。

然后调用tokenizePath()方法将需要匹配的path分割成string数组,主要是通过java.util 里面的StringTokenizer来处理字符串:

分割过程是基于/进行分割的,也就是说并不能直接获取到/

1.2.2 PathPattern解析

PathPattern首先会根据/将URL拆分成多个PathElement对象,然后根据PathPattern的链式节点中对应的PathElement的matches方法逐个进行匹配。其中负责解析{pathVariable:正则表达式(可选)}主要是org.springframework.web.util.pattern.CaptureVariablePathElement

这里主要是通过java.util.regex.compile#matcher处理匹配到的内容:

根据前面的分析,会从matchingContext中获取pathElement的值进行匹配,而matchingContext的初始化如下:

主要的值是从pathContainer中获取的,而pathContainer会根据/进行分隔,创建对应的Element:

举个例子,/admin/../最后对应的pathContainer如下:

那么如果此时Controller对应使用/admin/{path:.*}进行匹配,PathPattern在对{path:.*}使用CaptureVariablePathElement进行匹配时,此时value为..同样的并不能直接获取到/

1.3 PathPattern新增的语法支持

PathPattern在保持其匹配规则的基础上,新增了{*spring}的语法支持。

{*path}表示匹配余下的path路径部分并将其赋值给名为path的变量(变量名可以根据实际情况随意命名,与@PathVariable名称对应即可)。{*path}是可以匹配剩余所有path的,类似/**,而且功能更强,可以获取到这部分动态匹配到的内容。

以spring-web-5.3.26为例,简单分析下具体的解析过程,PathPattern首先会根据/将URL拆分成多个PathElement对象,然后根据PathPattern的链式节点中对应的PathElement的matches方法逐个进行匹配。而负责解析{*path}的主要是org.springframework.web.util.pattern.CaptureTheRestPathElement

因为该模式只能在定义在尾部,所以这里其实是遍历pathElements的内容获取对应的内容:

然后调用matchingContext.set方法,设置对应的key-value关系,如果{*path},那么这里的key就是path:

value的获取只要是通过org.springframework.web.util.pattern.CaptureTheRestPathElement#pathToString方法实现的,这里就是把拆分的各个PathElement内容进行组合,包括/

也就是说{*path}是可以获取到/的,例如/file/{*path},如果请求URL为/file/../etc/passwd那么对应的path参数提取到的内容为../etc/passwd

跟Parameter传递参数的形式不一样,大多数情况下没办法直接使用../../进行利用,因为容器以及Spring本身解析时都存在一定的限制。

2.1 容器对特殊字符的处理

Spring Boot默认支持Tomcat,Jetty,和Undertow作为底层容器,无需再将应用打包成war即可部署。其中默认使用tomcat,只需要引入spring-boot-starter-web依赖,应用程序就默认引入了tomcat。看看tomcat是否有一些限制:

2.1.1 tomcat对%2f和%2F的处理

根据前面的分析,因为{pathVariable:正则表达式(可选)}的方式没办法获取到字符串/,那么很自然就想到通过URL编码的方式去请求。但是实际上tomcat对/的URL编码形态会有相关的处理。

以tomcat-embed-core为例。其对于url会对URL中的内容进行校验。具体方法在org.apache.tomcat.util.buf.UDecoder#convert(org.apache.tomcat.util.buf.ByteChunk, boolean, org.apache.tomcat.util.buf.EncodedSolidusHandling)

该方法主要是查找%的位置,然后进行对应的检查。例如%后面必须为16进制的数字或字符,否则会抛出异常:

private void convert(ByteChunk mb, boolean query, EncodedSolidusHandling encodedSolidusHandling) throws IOException {
int start = mb.getOffset();
byte[] buff = mb.getBytes();
int end = mb.getEnd();
int idx = ByteChunk.findByte(buff, start, end, (byte)37);
int idx2 = -1;
if (query) {
idx2 = ByteChunk.findByte(buff, start, idx >= 0 ? idx : end, (byte)43);
}

if (idx >= 0 || idx2 >= 0) {
if (idx2 >= 0 && idx2 < idx || idx < 0) {
idx = idx2;
}

for(int j = idx; j < end; ++idx) {
if (buff[j] == 43 && query) {
buff[idx] = 32;
} else if (buff[j] != 37) {
buff[idx] = buff[j];
} else {
if (j + 2 >= end) {
throw EXCEPTION_EOF;
}

byte b1 = buff[j + 1];
byte b2 = buff[j + 2];
if (!isHexDigit(b1) || !isHexDigit(b2)) {
throw EXCEPTION_NOT_HEX_DIGIT;
}

j += 2;
int res = x2c(b1, b2);
if (res == 47) {
switch(encodedSolidusHandling) {
case DECODE:
buff[idx] = (byte)res;
break;
case REJECT:
throw EXCEPTION_SLASH;
case PASS_THROUGH:
buff[idx++] = buff[j - 2];
buff[idx++] = buff[j - 1];
buff[idx] = buff[j];
}
} else {
buff[idx] = (byte)res;
}
}

++j;
}

mb.setEnd(idx);
}
}

这里有一个属性encodedSolidusHandling,根据这个属性会对URL编码后的/进行解码或者抛出异常的操作convert方法的encodedSolidusHandling入参来自于org.apache.catalina.connector.Connector#encodedSolidusHandling

UDecoder.ALLOW_ENCODED_SLASH属性默认为false,也就是说encodedSolidusHandling默认为EncodedSolidusHandling.REJECT:

那么也就是说在请求的url中,当/以%2f或者%2F形式存在时,tomcat会"REJECT"该请求:

2.1.2 tomcat对/../的跨目录处理

要利用目录穿越达到任意文件下载的效果,必然会用到路径穿越符/../,类似tomcat这类容器在处理请求时也会对其进行一定的处理。

Tomcat是在CoyoteAdapter.service()函数上对请求URL进行解析处理的,其会调用postParseRequest()函数来解析URL请求内容:

在该方法中会先后调用parsePathParameters()和normalize()函数对请求

内容进行解析处理,其中parsePathParameters主要是对;场景进行处理:

而normalize()主要是对请求URL进行标准化处理。如果返回flase,会返回400status:

查看具体的处理逻辑:

ascii码47代表/,92代表\\,如果不是以这两个开头的话,返回false。然后根据ALLOW_BACKSLASH的值选择性的对\\进行处理,决定是统一变换成/,还是返回false。并且当匹配到ASCII码0即空字符时,直接返回false:

然后通过循环判断是否有连续的/,删除掉多余的/

然后就是对./../目录穿越字符进行处理,找不到则直接返回true:

重点关注/../的处理逻辑,这里会解析路径穿越符并进行目录回溯,直到找不到返回true,但是当index==0时会返回false,此时返回400 status:

根据前面的分析,也就是说,在请求的URL中写入的路径穿越符个数是有限制的,跟当前请求的目录层数有关(当index==0也就是循环处理后的url为/../时会返回400 status):

2.2 Spring自身的处理

对于{pathVariable:正则表达式(可选)}方式,因为无论是PathPattern还是AntPathMatcher都会因为解析的方式,无法获取到请求Path中的/,那么很自然就会想到通过编码的方式进行获取,但是Spring自身会有一定的处理。

2.2.1 initLookupPath处理逻辑差异

Spring在处理请求时,会在initLookupPath方法中初始化请求映射的路径,主要会通过UrlPathHelper类进行路径的处理,这里还有一段逻辑,根据this.usesPathPatterns()的值会执行不同的逻辑(是否使用PathPattern)。

以spring-webmvc-5.3.26为例,简单对比具体的差别:

当使用PathPattern进行解析时,this.usesPathPatterns()为true,此时从request域中获取PATH_ATTRIBUTE属性的内容,然后使用defaultInstance对象进行处理,然后根据removeSemicolonContent的值(默认为true)确定是移除请求URI中的所有分号内容还是只移除jsessionid部分:

整个过程是没有URL解码操作的,那么也就是说,假设请求的url为/file/download/..%2f,最终得到的lookupPath也是不会进行URL解码的:

而在后面进行解析时,会从Element的valueToMatch属性中获取对应的值,此时得到的是解码后的/:

也就是说通过URL编码的方式可以解决PathPattern在{pathVariable:正则表达式(可选)}情况下获取不到/的问题

若使用AntPathMatcher解析的话,就会执行另外一处逻辑,此时会调用resolveAndCacheLookupPath方法进行处理:

这里实际上调用的是getPathWithinApplication方法进行获取:

在getRquestUri方法中,会调用decodeAndCleanUriString对请求的URI进行处理:

查看decodeAndCleanUriString方法的具体实现,主要有三个方法,看看具体的作用:

首先是removeSemicolonContent,对于当前处理的URI,如果设置了setRemoveSemicolonContent属性为true,则删除分号,否则删除Jsessionid。

然后是decodeRequestString,这里前面说过,如果设置了解码属性便进行对应的解码操作。

最后是getSanitizedPath方法,这个方法主要是将//替换为/

根据前面的分析,假设请求的url为/file/download/..%2f,最终得到的lookupPath是经过URL解码的:

也就是说,通过URL编码/的方式进行请求,使用AntPathMatcher解析的话也是没办法获取到请求路径中的/的。

2.2.2 alwaysUseFullPath的影响

alwaysUseFullPath主要用于判断是否使用servlet context中的全路径匹配处理器。前面提到通过HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性获取的方式,主要获取的是initLookupPath方法调用后返回的请求映射的路径。

这里会根据alwaysUseFullPath的值(在2.3.1及之后版本,在configurePathMatch方法中,通过实例化UrlPathHelper对象并调用对应的setAlwaysUseFullPath方法将alwaysUseFullPath属性设置为true),决定走哪个逻辑,getPathWithinServletMapping会对uri进行标准化处理(也就是说 Spring Boot 版本在小于等于2.3.0.RELEASE时,会对路径进行规范化处理),而getPathWithinApplication是通过request.getRequestURI()方法获取当前request中的URI/URL,并不会对获取到的内容进行规范化处理:

那么也就说低版本的话会因为解析了路径穿越符../导致找不到mapping的情况,那么此时也不会走到设置HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性的逻辑。

根据前面的分析,对于AntPathMatcher解析的场景限制太多,很难获取到请求路径中的/。所以下面主要探讨PathPattern以及HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性(高版本)场景下的利用。Spring Boot默认支持Tomcat,Jetty,和Undertow作为底层容器,简单看看各个场景下的利用方式:

3.1 Tomcat下的利用

根据前面的分析,tomcat会对%2f以及%2F进行处理,同时还会存在跨目录处理的问题。

  • {pathVariable:正则表达式(可选)}(PathPattern解析)

根据前面的分析直接请求/是无法直接获取的,需要通过URL编码的方式处理,但是因为tomcat默认会对%2f以及%2F进行处理抛出异常,所以只有当ALLOW_ENCODED_SLASH属性设置为true时才可以使用%2f进行请求:

static{
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH","true");
}

同时还需要考虑跨目录处理的问题,例如如下代码:

 @RequestMapping("/file/download/{path:.*}")
public void fileDownload(@PathVariable("path") String path,HttpServletResponse response) throws IOException {
File file = new File(resource + path);
FileInputStream fileInputStream = new FileInputStream(file);
InputStream fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
response.reset();
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(path, "UTF-8"));
response.addHeader("Content-Length", "" + file.length());
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
outputStream.write(buffer);
outputStream.flush();
}

假设resource为/tmp,此时目录层数为一层,小于/file/download的两层,那么此时path只需要为../etc/passwd即可进行利用。

若reource层数为三层,此时需要三个../才能进行利用,此时tomcat调用normalize()进行跨目录处理时index==0,会抛出异常。

  • HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性(高版本)

可以直接获取到../../,需要考虑tomcat对/../的跨目录处理:

  @RequestMapping("/file/download/**")
public void fileDownload(HttpServletResponse response,HttpServletRequest request) throws IOException {
String reqPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
String path = new AntPathMatcher().extractPathWithinPattern(bestMatchPattern, reqPath);
File file = new File(resource + path);
FileInputStream fileInputStream = new FileInputStream(file);
InputStream fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
response.reset();
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(path, "UTF-8"));
response.addHeader("Content-Length", "" + file.length());
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
outputStream.write(buffer);
outputStream.flush();
}
  • PathPattern新增的语法支持{*path}

同样的可以直接获取到../../,需要考虑tomcat对/../的跨目录处理。

@RequestMapping("/file/download/{*path}")
@ResponseBody
public void fileDownload(@PathVariable("path") String path, HttpServletResponse response) throws IOException {
File file = new File(resource + path);
FileInputStream fileInputStream = new FileInputStream(file);
InputStream fis = new BufferedInputStream(fileInputStream);
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();

response.reset();
response.setCharacterEncoding("UTF-8");
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(path, "UTF-8"));
response.addHeader("Content-Length", "" + file.length());
OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
outputStream.write(buffer);
outputStream.flush();
}

综上,在Tomcat的场景下,漏洞利用需要考虑请求URI的目录层级以及/../个数限制的关系。

3.2 Jetty下的利用

同样是上面的漏洞代码,看看Jetty下的场景如果突破限制进行利用。

使用Jetty的方式很简单,去除springboot 中默认的Tomcat 依赖后引入Jetty即可:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

默认情况下,Jetty也是会对/../进行跨目录的处理并返回400 status:

/进行URL编码也是不可以的:

主要的原因是在org.eclipse.jetty.http.HttpURI#parse方法中,首先会对请求的URI进行解码操作,然后调用org.eclipse.jetty.util.URIUtil#canonicalPath方法进行规范化处理,如果返回结果为null,说明是个Bad URI,会抛出异常:

查看canonicalPath的处理逻辑,首先获取请求uri的长度并赋值给end变量,然后进行for循环逐个遍历uri中的字符,如果是/的话让slash为true,.并且前一个字符是/则跳出循环,否则让slash为false,如果最终i的值等于end,返回请求的path:

如果.并且前一个字符是/则跳出循环,此时会对/./或/../形式的url进行转换:

首先获取canonical的值(根据前面跳出循环前i的位置决定,例如请求的uri为/file/download/../../../../etc/passwd,那么此时canonical的值为/file/download/

这里通过for循环继续遍历未遍历完的URI字符,通过dots变量记录.的个数,每遇到一次.则加一,当遇到/则调用doDotsSlash方法进入判断逻辑(可以简单的理解为满足./../的条件):

在doDotsSlash方法中会根据dots的值进行相应的处理,如果dots为0或1的话均会返回false,继续循环。如果dots为2,说明此时请求的URI存在目录穿越符,此时先判断canonical的长度是否小于2,然后进行跨目录处理,例如canonical的值原本为/file/download,处理后会变成/file/:

如此循环,如果请求的URI中路径穿越符足够多的话,例如请求的URI为/file/download/../../../../etc/passwd,那么canonical的值在循环遍历的过程中会变成/,此时length小于2,canonicalPath会返回null,说明是个Bad URI,会抛出异常。

实际上可以绕过这层限制,当dots为2时,此时说明需要处理路径穿越符,canonical的值会进行截断,例如/file/最终会被截断成/

若canonical的值为/file//的话,此时处理后的结果是/file/,那么也就是说只要每次处理时canonical最后多一个/,最终结length肯定不会小于2,也不会触发Bad URI异常。而当dots为0时,会在canonical末尾追加一个/:

什么时候dots会为0呢?每次处理完路径穿越符后,都会将dots置0然后重新遍历,而进入该逻辑的条件是对应的字符为/,所以只要以..//..//..//..//的方式进行请求即可绕过对应的限制:

根据前面的分析,验证猜想,对于{*path}以及HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性(高版本)的场景可以通过如下方式进行利用:

同理,{pathVariable:正则表达式(可选)}(PathPattern解析)只需要将/使

用URL编码后再请求即可:

3.3 undertow下的利用

使用udertow的方式很简单,去除springboot 中默认的Tomcat 依赖后引入undertow即可:

    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

udertow默认情况下貌似没有太多的限制,直接请求/../也是可以的,所以对于{*path}以及HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE属性(高版本)的场景直接利用即可:

而{pathVariable:正则表达式(可选)}(PathPattern解析)的话,根据前面的分析,只需要将/使用URL编码后再请求即可:

3.4 其他

除此之外,在Controller层还可能因为考虑到文件名是中文,再次进行URL解码的操作,那么此时很多限制也可以得到解决,包括类似AntPathPattern在某些场景下也可以进一步进行利用。

转载:https://forum.butian.net/share/2265作者:tkswifty欢迎大家去关注作者

欢迎师傅加入安全交流群(qq群:611901335),或者后台回复加群

如果想和我一起讨论,欢迎加入我的知识星球!!!

扫描下图加入freebuf知识大陆

师傅们点赞、转发、在看就是最大的支持

后台回复知识星球或者知识大陆也可获取加入链接(两个加其一即可)


文章来源: http://mp.weixin.qq.com/s?__biz=MzkxMDMwNDE2OQ==&mid=2247490157&idx=1&sn=4c7af8461bd839fc08d6c97030298688&chksm=c12c2cabf65ba5bd420dd345e8ab6ab0b3141a2493f2cb69e5e009bfe2300e70ebd940cb0cf7#rd
如有侵权请联系:admin#unsafe.sh