![8e10f258e5247fd3da28fb6b4bc418e2.gif](https://i-blog.csdnimg.cn/blog_migrate/d324cbf0571ec385371cd1990da127a2.gif)
![b05f4a470183417dcd8ce1d2b4a7f6a7.gif](https://i-blog.csdnimg.cn/blog_migrate/2bc17815ccadce7715f6d8c453772776.gif)
一般可以通过相关的业务关键字(例如download、filename等)或者相关的操作类(例如InputStream、File等)快速定位相关的模块进行审计挖掘。
前段时间审计某项目时发现一处任意文件下载的绕过。系统基于Springboot进行开发,首先是发现任意文件下载接口:
@Value("${CERTIFICATE.PATH}")private String uploadPath;@GetMapping("/downloadFile.do")public void uploadFileDownload(HttpServletResponse response,String fileName) throws IOException { ServletOutputStream outputStream =response.getOutputStream(); String contentType ="application/octet-stream"; response.setContentType(contentType); String filePath = uploadPath+fileName; System.out.println(filePath); outputStream.write(FileUtils.readFileToByteArray(filePath)); outputStream.flush(); outputStream.close();}
相关的接口是下载用户上传的凭证材料的,整个下载过程从配置文件中获取uploadPath,然后与用户输入的fileName进行拼接,最后调用工具类FileUtils的readFileToByteArray()方法进行下载。查看工具类readFileToByteArray()方法的具体实现:
public static byte[] readFileToByteArray(String path) { StringBuffer buf = new StringBuffer(); try { File file = new File(path); BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8")); String row; while ((row = br.readLine()) != null) { buf.append(row); } } catch (IOException e) { e.printStackTrace(); } if (buf.length() == 0) { buf.append(""); } return buf.toString().getBytes();}
可以看到相关的工具类方法直接将传入的地址进行文件内容读取,封装后进行返回,整个文件下载的过程是没有过滤掉"./"、"…/"、"/"等特殊字符,防止用户进行目录回溯的,同时也没有限制下载目录。初步判断是存在任意文件下载风险的。
这里尝试去测试环境访问对应的下载接口进行复现,发现失败了:
![b05f4a470183417dcd8ce1d2b4a7f6a7.gif](https://i-blog.csdnimg.cn/blog_migrate/2bc17815ccadce7715f6d8c453772776.gif)
一般来说审计过滤器缺陷主要分以下几个点:
获取数据的方式是否覆盖全面
过滤的规则内容
过滤器的顺序
@WebFilter(filterName = "SecurityFilter", urlPatterns = "/*" )public class SecurityFilter implements Filter { FilterConfig filterConfig = null; @Override public void destroy() { this.filterConfig = null; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } @Override public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; }}
具体实现是通过HttpServletRequestWrapper来获取request的内容,然后通过重写getParameterValues()、getParameter()方法进行处理的。
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { public XssHttpServletRequestWrapper(HttpServletRequest servletRequest) { super(servletRequest); } @Override public String[] getParameterValues(String parameter) { String[] values = super.getParameterValues(parameter); if (values == null) { return null; } int count = values.length; String[] encodedValues = new String[count]; for (int i = 0; i < count; i++) { System.out.println("before:"+values[i]); encodedValues[i] = JsoupUtil.clean(cleanAnyFileRead((String) values[i])); System.out.println("after:"+encodedValues[i]); } return encodedValues; }
这里存在第一种绕过方式,因为对于multipart/form-data这种数据类型上述过滤器明显也是无法获取request提交内容的,那么可以尝试转换multipart的方式进行提交,尝试绕过相关的过滤器:
比较可惜的是,这里对任意文件下载接口并不适用,因为其是用@GetMapping("/downloadFile.do")进行注解的,限制了请求方法为GET。获取数据的方式虽然不全面,但是仅针对/downloadFile.do接口来说的话是足够的了,那么继续往下看相关的过滤规则是否存在缺陷。
这里存在两层过滤,一层是通过cleanAnyFileRead()过滤任意文件上传的敏感输入的,第二层是调用工具类JsoupUtil.clean()方法进行XSS输入过滤。
@Overridepublic String getParameter(String parameter) { String value = super.getParameter(parameter); if (value == null) { return null; } return JsoupUtil.clean(cleanAnyFileRead((String) value));}
首先看任意文件下载的过滤,这里是简单的使用正则进行过滤,出现一次或者多次的.且以/结尾的内容都进行过滤,例如…/、./等,这也就解释了为什么前面测试时输入…/…/…/etc/passwd失败了,过滤后返回的内容为/etc/passwd,拼接uploadPath后不存在相关的文件,路径穿越失败:
private static String cleanAnyFileRead(String value) { value = value.replaceAll("\\.+/", ""); return value;}
继续往下看xss的过滤,JsoupUtil.clean()方法的具体实现如下:
public class JsoupUtil { /** * 配置白名单为基本使用的标签再加上img标签 */ private static final Whitelist WHITELIST = Whitelist.basicWithImages(); /** * 配置过滤的参数,不对代码格式化 */ private static final Document.OutputSettings OUTPUT_SETTINGS = new Document.OutputSettings().prettyPrint(false); static { //富文本编辑时一些样式是使用style来进行实现的 比如红色字体 style="color:red;" 所以需要给所有标签添加style属性 WHITELIST.addAttributes(":all","style"); } public static String clean(String content){ return Jsoup.clean(content,"",WHITELIST,OUTPUT_SETTINGS); }}
比较简单粗暴,直接使用jsoup组件进行了过滤。这里简单介绍下jsoup的api:使用Jsoup的clean方法进行清除HTML标签操作,该方法会清除在你所指定的白名单whitelist中的所有HTML标签。默认的Jsoup提供了5种Whitelist的API
- none()
该API会清除所有HTML标签,仅保留文本节点。 - impleTest()
该API仅会保留b, em, i, strong, u 标签,除此之外的所有HTML标签都会被清除。 - basic()
该API会保留 a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul 和其适当的属性标签,除此之外的所有HTML标签都会被清除,且该API不允许出现图片(img tag)。另外该API中允许出现的超链接中可以允许其指定http, https, ftp, mailto 且在超链接中强制追加rel=nofollow属性。 - basicWithImages()
该API在保留basic()中允许出现的标签的同时也允许出现图片(img tag)和img的相关适当属性,且其src允许其指定 http 或 https。 - relaxed()
该API仅会保留 a, b, blockquote, br, caption, cite, code, col, colgroup, dd, div, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, span, strike, strong, sub, sup, table, tbody, td, tfoot, th, thead, tr, u, ul 标签,除此之外的所有HTML标签都会被清除,且在超链接中不会强制追加rel=nofollow属性。 - clean()
此外Jsoup的Whitelist提供了addTags方法,利用该方法可以对whitelist进行自定义扩展:
public Whitelist addTags(String... tags)
所以JsoupUtil.clean()方法的效果应该是输入