浅谈URI中的任意文件下载 – 作者:SecIN技术社区

原文来自SecIN社区—作者:tkswifty

引言

文件下载是比较常见的业务。常见的接口格式为/download?fileName=xxx.png,整个过程若没过滤目录穿越符号…/或者未对下载的路径进行处理限制。当传入的filename参数为../../etc/passwd即可穿越路径达到任意文件下载的效果。
有些接口在尝试获取某一文件时,路径是/file/test.jpg,通过解析URI中的内容来得到对应的文件名test.jpg,然后完成相关的下载操作。此时如果尝试访问/file/../../test.jpg,相关的中间件或者开发框架在解析路由时会做相关的处理。那么此时是否可以进行目录穿越的实际利用呢?
以下是实际项目中遇到的URI中任意文件下载实例:

URI中任意文件下载实例

自定义Servlet

相关业务为用户上传文件的下载,主要通过Servlet进行交互,以下是相关的实现。

servlet的具体映射:

<servlet-mapping>
	  	<servlet-name>UserfilesDownloadServlet</servlet-name>
	  	<url-pattern>/userfiles/*</url-pattern>
</servlet-mapping>

下载业务具体实现如下:

public class UserfilesDownloadServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;
	private Logger logger = LoggerFactory.getLogger(getClass());

	public void fileOutputStream(HttpServletRequest req, HttpServletResponse resp) 
			throws ServletException, IOException {
		String filepath = req.getRequestURI();
		int index = filepath.indexOf(Global.USERFILES_BASE_URL);
		if(index >= 0) {
			filepath = filepath.substring(index + Global.USERFILES_BASE_URL.length());
		}
		try {
			filepath = UriUtils.decode(filepath, "UTF-8");
		} catch (UnsupportedEncodingException e1) {
			logger.error(String.format("解释文件路径失败,URL地址为%s", filepath), e1);
		}
		File file = new File(Global.getUserfilesBaseDir() + Global.USERFILES_BASE_URL + filepath);
		try {
			FileCopyUtils.copy(new FileInputStream(file), resp.getOutputStream());
			resp.setHeader("Content-Type", "application/octet-stream");
			return;
		} catch (FileNotFoundException e) {
			req.setAttribute("exception", new FileNotFoundException("请求的文件不存在"));
			req.getRequestDispatcher("/WEB-INF/views/error/404.jsp").forward(req, resp);
		}
	}

......
}

这里使用了springframework的FileCopyUtils.copy()方法,具体实现如下,主要是对File对象进行相应的处理,将读取的文件内容复制到response内容中:

public static int copy(File in, File out) throws IOException {
        Assert.notNull(in, "No input File specified");
        Assert.notNull(out, "No output File specified");
        return copy((InputStream)(new BufferedInputStream(new FileInputStream(in))), (OutputStream)(new BufferedOutputStream(new FileOutputStream(out))));
    }

最后通过设置response header返回文件内容,具体效果如下:

图片[1]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

其中下载的文件名filepath是通过req.getRequestURI()来获取的,该方法是不会对URI中的../或者;等特殊字符进行规范化处理的。同时在获取到对应的filepath后直接进行路径拼接然后进行文件读取,整个过程未限定下载的文件目录范围,也并未过滤..//等敏感关键字,存在任意文件下载风险。但是这里场景比较特殊,相关参数的获取是在URI中获取的,能否深入利用还有待商榷。

当前项目的upload目录位置如下:

图片[2]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

这里尝试访问../WEB-INF/web.xml(回到upload上级目录WebContent),成功完成目录穿越读取到对应的web.xml配置信息:

图片[3]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

中间件在进行解析时,会对URI中的../进行相关处理从而得到相关的servlet,tomcat解析时已经对../进行处理了处理,上面的访问方式其实跟直接访问/userfiles/WEB-INF/web.xml是一样的:

图片[4]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

若此时如果想读取/etc/passwd,就需要写入更多的目录穿越符../,此时tomcat处理完../后已经不在/userfiles/*这个servlet映射范围内了,那么此时会抛出相关的异常,并不能进一步获取更多的敏感信息。

尝试扩大漏洞的危害,读取更多的敏感文件。首先要让tomcat不处理../../

filepath = UriUtils.decode(filepath, "UTF-8");

尝试进行编码请求,发现触发400 Invalid URI错误,因为tomcat会对URI路径信息进行解码并进行检测,当遇到斜杠的URL(即%2F)时,出于安全的考虑会返回400状态码:

else if (metaChar == '%')
      {
        char res = (char)Integer.parseInt(str
          .substring(strPos + 1, strPos + 3), 16);
        if ((noSlash) && (res == '/')) {
          throw new IllegalArgumentException(sm.getString("uDecoder.noSlash"));
        }
        dec.append(res);
        strPos += 3;
      }
    }
    return dec.toString();

图片[5]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

在URL中有一个保留字符分号;,主要作为参数分隔符进行使用,有时候是请求中传递的参数太多了,所以使用分号;将参数对(key=value)连接起来作为一个请求参数进行传递。tomcat在对;进行处理时,同样的也会对;进行截断并当成参数处理,在URI编码的基础上加上;再次访问,经过一系列处理获取到的路径为/upload/;/../../../../../../../../etc/passwd,此时返回的是404状态码,应该是在进行文件读取的时候找不到名为;的目录,触发了FileNotFoundException异常:

try {
			FileCopyUtils.copy(new FileInputStream(file), resp.getOutputStream());
			resp.setHeader("Content-Type", "application/octet-stream");
			return;
		} catch (FileNotFoundException e) {
			req.setAttribute("exception", new FileNotFoundException("请求的文件不存在"));
			req.getRequestDispatcher("/WEB-INF/views/error/404.jsp").forward(req, resp);
		}

图片[6]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

刚好结合网站的其他业务可以进行目录创建,在upload下创建名为;的目录名,此时访问/userfiles/upload/;%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd即可获取到相关的敏感文件:

图片[7]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

@PathVariable注解

@PathVariable注解是spring3.0的一个新功能,可以接收请求路径中占位符的值并将URL中占位符参数{xxx} 绑定到控制器处理方法的入参中。该功能在SpringMVC向REST 、目标挺进发展过程中具有十分重要的意义。例如下面的例子:

  • 获取id = 1的订单信息
@RequestMapping("/Order/{id}")
public String getOrderDetails(@PathVariable("id") Integer id)
{
   //......
}

除此之外,@PathVariable同样可以用于实现文件的下载,例如下面的例子:

@RequestMapping("/file/get/{filename:.+}")
	public ResponseEntity<byte[]> download(HttpServletRequest request,
			@PathVariable(value = "filename") String fileName) throws IOException {
		String uploadRoot = "/var/work/download";
		File file = new File(uploadRoot + URLDecoder.decode(File.separator + fileName, "UTF-8"));
		byte[] body = null;
		InputStream is = new FileInputStream(file);
		body = new byte[is.available()];
		is.read(body);
		HttpHeaders headers = new HttpHeaders();
		headers.add("Content-Disposition", "attchement;filename=" + file.getName());
		HttpStatus statusCode = HttpStatus.OK;
		ResponseEntity<byte[]> entity = new ResponseEntity<>(body, headers, statusCode);
		return entity;
	}

具体效果如下,例如这里尝试获取上传的test.png图片:

图片[8]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

这里将URL中占位符参数filename跟初始路径/var/work/download进行拼接,然后下载对应的文件。这里对应的文件名用户可控,未限定下载的文件目录范围,同时并未过滤..//等敏感关键字,存在任意文件下载风险。

但是这里参数的位置在URI中,能否利用还需要进一步探究。

正常来说,直接使用../../../尝试目录穿越即可下载/etc/passwd等敏感文件,实际上中间件在进行解析时,会对URI中的../行相关处理从而得到相关的servlet,以tomcat为例,实际上解析时已经对../进行处理了,占位符参数filename并不能获取到../来进行目录穿越。这里因为处理后URI为/file/etc/passwd没找到对应的映射,所以返回了404状态码:

图片[9]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

同样的,尝试对/进行URL编码,tomcat源码会对URI路径信息进行解码并进行检测,当遇到斜杠的URL(即%2F)时,出于安全的考虑会返回400状态码(抛出Invalid URI: noSlash异常):

tomcat源码中相关处理流程如下:

else if (metaChar == '%')
      {
        char res = (char)Integer.parseInt(str
          .substring(strPos + 1, strPos + 3), 16);
        if ((noSlash) && (res == '/')) {
          throw new IllegalArgumentException(sm.getString("uDecoder.noSlash"));
        }
        dec.append(res);
        strPos += 3;
      }
    }
    return dec.toString();

图片[10]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

这里可以使用双重URL编码绕过中间件的处理,对/进行URL双重编码后返回404状态码,说明已经绕过noSlash异常了:

图片[11]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

中间件后就是Spring的处理过程,查看Spring的解析过程,主要通过getPathWithinServletMapping方法获取路由:

public String getPathWithinServletMapping(HttpServletRequest request) {
        String pathWithinApp = this.getPathWithinApplication(request);
        String servletPath = this.getServletPath(request);
        String sanitizedPathWithinApp = this.getSanitizedPath(pathWithinApp);

getPathWithinApplication方法中会使用 getRequestUri 来获取对应路由:

public String getRequestUri(HttpServletRequest request) {
        String uri =
(String)request.getAttribute("javax.servlet.include.request_uri");
        if (uri == null) {
            uri = request.getRequestURI();
        }
        return this.decodeAndCleanUriString(request, uri);
    }

随即在decodeAndCleanUriString对URI进行格式化处理,首先对分号进行处理,然后进行URI解码,最后进行返回:

private String decodeAndCleanUriString(HttpServletRequest request, String uri)
{
        uri = this.removeSemicolonContent(uri);
        uri = this.decodeRequestString(request, uri);
        uri = this.getSanitizedPath(uri);
        return uri;
}

也就是说,Spring本身会对URI进行一次解码处理。例如/file/get/..%252fetc/passwd经过tomcat+spring处理后会变成/file/get%2fetc/passwd

再回到案例代码,这里应该是考虑到了中文传输的问题,在前端用js对URL进行编码后再发送请求,这时候开发可能忽略了Spring自身的一次解码操作,在对应的接口再次进行了一次解码:

File file = new File(uploadRoot + URLDecoder.decode(File.separator + fileName, "UTF-8"));

那么也就是说使用双重URL编码的方式处理../进行请求,即可在Spring以及接口自身的URLDecode获得漏洞利用需要的../../,从而成功进行目录穿越读取到/etc/passwd内容:

图片[12]-浅谈URI中的任意文件下载 – 作者:SecIN技术社区-安全小百科

拓展延伸

因为涉及到URI部分的解析,参与的有中间件、spring、人工的解码等一系列处理。那么如果在上面案例的基础上集成shiro框架。那相关的利用poc又是如何呢?
案例一的;可能需要编码成%3b。这里就有点CVE-2020-13933的味道了,但是高版本shiro对编码的;进行了处理,怎么继续利用还是值得思考的。

来源:freebuf.com 2020-11-19 11:18:30 by: SecIN技术社区

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论