查看原文
其他

原创 | 绕过后缀安全检查进行文件上传-2

tkswifty SecIN技术平台 2022-06-18

点击上方蓝字 关注我吧


引言

一般针对文件上传业务,主要判断是否有检查后缀名,同时要查看配置文件是否有设置白名单或者黑名单,如果没有的话,那么攻击者利用该缺陷上传类似webshell等恶意文件。前面分享了通过报错的方式来绕过后缀安全检查,传送门:https://sec-in.com/article/647。


实际业务中又发现的一处绕过后缀安全检查进行文件上传的实例,当前漏洞已经修复完毕 。提取关键的的漏洞代码做下复盘。



业务场景

上传业务接口是基于servlet实现的,主要用于图片的上传,首先判断当前request请求是否是multipart上传请求,是的话调用 processFileUpload方法:

public class UploadService extends HttpServlet implements Servlet {    private boolean isAllowed;
private String[] allowedExtName=new String[]{ "jpg","jpeg","bmp","gif","png"//图片 }; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); }
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { boolean isMultipart = ServletFileUpload.isMultipartContent(request); if (isMultipart){ processFileUpload(request,response); } } ......}

查看 processFileUpload的具体实现,使用 DiskFileItemFactory进行上传,UploadListener应该是用作监听的,估计是有个进度条的展示。同时还设置了文件上传的最大值,不得超过100MB:

private void processFileUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); ...... String msg = ""; //建立工厂对象和文件上传对象 DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1048576); ServletFileUpload upload = new ServletFileUpload((FileItemFactory)factory); //建立上传监听器和设置监听器 UploadListener listener = new UploadListener(); session.setAttribute("LISTENER", listener); upload.setProgressListener((ProgressListener)listener); //给上传的文件设一个最大值,这里是不得超过100MB int maxSize=100*1024*1024; String filePath = ""; try{ List<FileItem> items = upload.parseRequest(request); for (int i=0;i<items.size();i++){ FileItem item = items.get(i); if (!item.isFormField() && item.getName().length() > 0) { long upFileSize = item.getSize(); String name = item.getName(); //获取文件后缀名 String[] splitName=name.split("\\."); String extName=splitName[splitName.length-1]; //检查文件后缀名 for(String allowed:allowedExtName) { if(allowed.equalsIgnoreCase(extName)) { isAllowed=true; } } if (!this.isAllowed) { msg = "file content illegal"; break; } if (upFileSize > maxSize) { msg = "上传文件太大了,请上传不超过100MB的文件!"; break; } String fileName = FileUtil.getFileName(item); File uploadedFile = new File(String.valueOf(FileUtil.getRealPath(getServletContext())) + File.separator + fileName); item.write(uploadedFile); }else{ msg="没选择上传文件!"; } } }catch (Exception e){ msg = "上传失败:"+e.getMessage(); }

具体的上传逻辑如下,首先通过item获取文件的后缀名,然后遍历allowedExtName的内容依次比对,如果相等设置isAllowed为true,在后续逻辑中,如果isAllowed不为true,则无法完成文件的上传操作, 同时在文件写入时通过 FileUtil.getFileName 对文件名进行了重命名操作:

List<FileItem> items = upload.parseRequest(request); for (int i=0;i<items.size();i++){ FileItem item = items.get(i); if (!item.isFormField() && item.getName().length() > 0) { long upFileSize = item.getSize(); String name = item.getName(); //获取文件后缀名 String[] splitName=name.split("\\."); String extName=splitName[splitName.length-1]; //检查文件后缀名 for(String allowed:allowedExtName) { if(allowed.equalsIgnoreCase(extName)) { isAllowed=true; } } if (!this.isAllowed) { msg = "file content illegal"; break; } if (upFileSize > maxSize) { msg = "上传文件太大了,请上传不超过100MB的文件!"; break; } String fileName = FileUtil.getFileName(item); File uploadedFile = new File(String.valueOf(FileUtil.getRealPath(getServletContext())) + File.separator + fileName); //文件写入 item.write(uploadedFile); }else{ msg="没选择上传文件!"; } } }catch (Exception e){ msg = "上传失败:"+e.getMessage(); }

可以看到allowedExtName是servlet中定义的属性,对应的是图片相关的白名单

private String[] allowedExtName=new String[]{ "jpg","jpeg","bmp","gif","png"//图片 };

简单总结一下当前上传接口所做的安全措施:

  • 通过后缀白名单的方式限制非法文件的上传

  • 文件名随机命名,不允许用户自定义

查看接口的具体效果:

尝试上传jsp文件,提示上传失败:

尝试上传png图片,成功上传:

可以看到设置的白名单“的确”起到了一定的拦截作用。


绕过过程

根据上面的分析,可以知道关键的后缀安全检查逻辑主要在isAllowed属性,只有当isAllowed的值为true时,才能完成文件上传 。

isAllowed作为servlet类UploadService的属性(boolean的默认值为false),在文件上传方法processFileUpload中值可进行改变(当上传后缀符合白名单时改变为true):

public class UploadService extends HttpServlet implements Servlet { private boolean isAllowed; ...... private void processFileUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ...... for(String allowed:allowedExtName) { if(allowed.equalsIgnoreCase(extName)) { isAllowed=true; } } } .....}

在JavaWeb中,当浏览器发送了一次请求到服务器时,servlet容器会根据请求的url-pattern找到对应的Servlet类,执行对应的doPost或doGet方法,再将响应信息返回给浏览器。

那么如果每次请求都会重新实例化(new)一次对应的servlet class(这里是UploadService),在这个前提下,每个请求的isAllowed属性都是独立的,互不影响。isAllowed属性可以很好的完成它自己的任务。但是实际上在servlet中可能并不是这样的。

首先简单介绍下单例模式:

  • 单例模式

单例模式中一个流程中只有一个对象存在,同理单例类的属性也是类似的。

那么对于servlet来说,如果其也是单例的话,那么isAllowed在调用时会被共享,存在线程安全问题 。

查阅相关的资料,在Servlet规范中,对于Servlet单例与多例定义如下:

“Deployment Descriptor”, controls how the servlet container provides instances of the servlet.For a servlet not hosted in a distributed environment (the default), the servlet container must use only one instance per servlet declaration. However, for a servlet implementing the SingleThreadModel interface, the servlet container may instantiate multiple instances to handle a heavy request load and serialize requests to a particular instance.

大概意思是,如果一个Servlet没有被部署在分布式的环境中,一般web.xml中声明的一个Servlet只对应一个实例。而如果一个Servlet实现了SingleThreadModel接口,就会被初始化多个实例。

在业务代码中,UploadService是通过web.xml完成映射的:

<servlet> <servlet-name>UploadPhotoServlet</servlet-name> <servlet-class>UploadService</servlet-class></servlet>

<servlet-mapping> <servlet-name>UploadPhotoServlet</servlet-name> <url-pattern>/uploadPhoto</url-pattern></servlet-mapping>

那么也就是说UploadService是单例的,isAllowed属性可能存在线程安全问题。

在上传一个正常的png图片后,此时isAllowed为true,因为UploadService是单例的,在下一次上传时isAllowed并没有改变(仍为true),那么在上传恶意jsp时((即使后缀不在白名单内,现有的代码逻辑isAllowed也不会重新设置为false)),那么就可以绕过后缀检查完成上传了:

if(allowed.equalsIgnoreCase(extName)){ isAllowed=true;}

验证上面的猜想,对上传过程进行调试,可以看到在完成一次图片上传的过程后,即便当前上传的是白名单以外的 jsp 文件,此时isAllowed仍是true,也就是说当前 jsp 可以正常完成上传 :

在实际环境中进行测试,在上传一张图片后,成功绕过后缀安全检查上传了jsp:

访问对应的文件路径,对应的jsp内容也成功解析:

拓展与延伸(二次绕过)

经过上面的分析,研发进行了代码的修改,这里增加了一个判断,如果后缀名不在白名单内,则将isAllowed设置为false,避免上述缺陷导致的白名单绕过:

//检查文件后缀名for (String allowed : allowedExtName) { if (allowed.equalsIgnoreCase(extName)) { isAllowed = true; } else if (!allowed.equalsIgnoreCase(extName)) { isAllowed = false; }}

这么修改以后,因为每一次检查都会改变isAllowed的值,那么在成功上传png后,再次上传jsp时,因为jsp不符合白名单要求,那么isAllowed会改变为false,此时上传失败:

实际上上面的修复方式也是存在缺陷的,并没有解决isAllowed的线程安全问题 。

同理,因为isAllowed在多个请求中共享,那么可以通过条件竞争的方式来绕过后缀安全检查 。

验证猜想,首先并发上传正常的png图片:

然后另一边同时并发上传jsp文件(注意上传图片的频次要比jsp的快),可以看到通过条件竞争,仍可成功绕过后缀检查,完成恶意jsp文件的上传:

最终的修复方法也很简单,不再引入isAllowed来判断,当遇到白名单以外后缀文件上传时,直接抛出异常不再往下执行。避免了线程安全问题。

在进行黑盒测试时,通过上面的方式尝试绕过后缀安全检查进行文件上传也是一种不错的思路。白盒审计中也需要额外关注。

相关推荐




原创 | java安全-java类加载器
原创 | java安全-java反射
原创 | java安全-java RMI你要的分享、在看与点赞都在这儿~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存