Apache Commons FileUpload是Java中用来解析文件上传请求的基础库。在Servlet3.0之前的版本中,Servlet规范中并未定义获取multipart/form-data请求文件内容的API,因此一般会使用Commons FileUpload来解析上传的文件。从Servlet 3.0开始提供了getPart方法来获取上传的文件,在Tomcat7.0及以上版本对getPart的实现中(org.apache.catalina.connector.Request#parseParts),也是使用了Apache Commons FileUpload(修改Package后复制到Tomcat的项目中使用)。
Apache Commons FileUpload在解析上传文件时,为了防止出现DOS漏洞,已经支持通过org.apache.commons.fileupload.FileUploadBase#setSizeMax方法限制Request body总大小。也支持通过org.apache.commons.fileupload.FileUploadBase#setFileSizeMax方法限制上传的单个文件大小。
但因为Commons FileUpload把上传请求解析为FileItem对象时,会额外占用不少的内存(例如DiskFileItem中的dfos里会创建一个1024大小的ByteArrayOutputStream)。所以上传空文件会十多倍的放大内存占用,并且可以在sizeMax内(Spring boot下默认10M)上传大量空文件(构造上传请求时因filename、Content-Disposition等原因,一个空文件需要74Byte,10M可以上传14万空文件),Commons FileUpload对大量空文件循环解析时需要较多的时间,会耗费很多内存(10M的请求需要服务端几百M的内存)。当发送多个攻击请求时,会占满内存且GC无法释放,导致拒绝服务。
解压下载的zip包后,把pom.xml中spring-boot-starter-parent的版本修改为2.7.7(该版本依赖的Tomcat是未修复漏洞的9.0.70。还可以不修改spring boot版本,直接在pom.xml中添加9.0.70版本Tomcat的依赖)
在IDEA中打开该项目即可。
为方便复现,在启动Spring boot应用时,先配置下启动时的VM options,添加-Xmx512M,限制该应用最多使用512M内存
接着启动Spring boot应用。因为没有任何业务代码,所以启动后只有Spring security的登陆页面。并且因为有Spring security的认证,访问任意url时,都会被重定向到/login页面
此时使用代码构造上传14万空文件的请求(boundary设置为单个字符,节省空间。Content-Disposition、name、filename、Content-Type均不能省略),5个线程并发向8080端口的任意路径发送该上传请求,在请求中不需要携带登录后的cookie,几分钟后发现Spring boot应用出现异常
此时刷新页面时,一直转圈,没有响应
成功复现漏洞
1、漏洞分析 – 向一个不存在的接口上传文件,为什么Spring boot应用会解析上传文件导致拒绝服务?
在常规的Servlet应用中,若要在Servlet中调用getPart方法获取到上传的文件,需要先给该Servlet配置multipart-config(也可以通过MultipartConfig注解),所以若业务的Servlet中没有上传接口时,无法使用该漏洞进行攻击。
但在Spring boot应用中,会把所有的请求通过DispatcherServlet进行处理,在该Servlet中再根据请求URL的不同转发到不同的Controller。并且Spring为了在业务的Mapping接口中支持上传文件,自动给DispatcherServlet配置了multipart-config。也就是经过Spring boot包装后,每一个Spring boot业务从Servlet层面看来,都有一个ServletMapping为/的DispatcherServlet,并且该Servlet还配置了multipart-config。因此向任意url上传文件时都会解析上传的文件触发漏洞。
以下是相关代码的截图及说明:
下图是在spring-boot-autoconfigure中的MultipartAutoConfiguration,初始化了multipartConfigElement bean。
下图是在spring-boot-autoconfigure中的DispatcherServletAutoConfiguration
,会创建DispatcherServlet
,并通过DispatcherServletRegistrationBean
(extends ServletRegistrationBean)来自动把该Servlet注册到Servlet容器中。并且在创建registration时,还会把上面的multipartConfig设置到当前Servlet上
2、漏洞分析 – 明明已经有Spring security的认证鉴权了,为什么进行攻击时依然不需要认证就可以成功攻击
在FileUploadBase的parseRequest方法中下断点后,再次发送攻击请求,通过断点的调用栈可以看到是Spring security的CsrfFilter中调用getParameter时触发解析上传的文件
在org.apache.catalina.connector.Request#parseParts
中可以看到,当Servlet中没有配置MultipartConfigElement
时,如果allowCasualMultipartParsing
配置为true,依然会生成新的mce来解析上传文件。否则就抛异常或者返回emptyList了。
在CsrfFilter
中,查看filterChain的内容,可以发现CsrfFilter
是在认证鉴权的FilterSecurityInterceptor
之前的,因此在认证鉴权之前,CsrfFilter
在getParameter时就触发了文件上传的解析。
综上,只要在认证鉴权逻辑处理之前,有Filter调用到了getParameter方法,就会触发文件解析操作,最终造成拒绝服务漏洞。
3、漏洞分析 – 为什么上传大量空文件会导致拒绝服务?
先讲空间原因,在恶意构造上传空文件时,单个空文件最小可以构造成如下内容,花费的请求流量为74Byte--brnContent-Disposition:form-data;name=f;filename=arnContent-Type:arnrnrn
而Commons FileUpload把上述内容解析为DiskFileItem
对象时,dfos属性的类型是DeferredFileOutputStream
,而DeferredFileOutputStream
中有一个大小为1024的比特数组,也就是说一个DiskFileItem
对象至少占用1KB。
因此可以把请求流量至少放大14倍(1024/74)用来占用服务端的内存,1个10M大小的上传空文件请求,服务端在解析成List<FileItem>
时需要耗费至少140M内存。
如果只是临时的new一些byte数组,快速使用后又被GC回收掉的话,也无法造成拒绝服务(各种GC算法的吞吐量都不会低)。但叠加时间原因后,就会真正造成拒绝服务了。
因为一个10M上传空文件的请求中可以包含14万个空文件。Commons FileUpload在解析时会循环14万次来创建DiskFileItem对象,一般也需要0.X秒才能完成。
在这0.X秒解析过程中,前面已经生成的DiskFileItem对象因后续还需使用(add到List中作为结果返回到外面),无法被GC回收。当并发的多个请求都在循环中new byte数组来创建DiskFileItem对象时,内存就不够了,并且此时多次Full GC也无法释放出内存。因此导致JVM抛出 “Java heap space”、“GC overhead limit exceeded”等异常。最终导致Tomcat服务挂掉。
从业务角度来看该漏洞的修复
1:无论是外置还是内置的Tomcat,均需要升级到无漏洞版本(9.0.71及以上、8.5.85及以上、10.1.5及以上)
2:若依赖了Apache Commons FileUpload(无论主动还是被动),需要升级到1.5版本
3:若主动依赖了Apache Commons FileUpload,并且通过ServletFileUpload/FileUpload/FileUploadBase/PortletFileUpload/DiskFileUpload或其他继承了FileUploadBase的类来调用parseRequest/parseParameterMap/getItemIterator方法,则需要在调用这些方法之前先调用setFileCountMax方法来限制文件数量(调用isMultipartContent方法不会触发漏洞)
4:若主动依赖了Apache Commons FileUpload,并把multipartResolver bean配置为Spring的CommonsMultipartResolve,则在Spring中会使用Apache Commons FileUpload进行上传文件的解析(Spring的multipartResolver默认值为StandardServletMultipartResolver,会使用Servlet容器提供的方法去解析上传文件),因CommonsMultipartResolve并未提供方法/配置来给FileUpload配置fileSizeMax,因此要么弃用CommonsMultipartResolve,要么继承CommonsMultipartResolve后重写相关方法来调用setFileCountMax。
5:若因二方件或三方件引入了Apache Commons FileUpload,需要根据二方件/三方件使用Apache Commons FileUpload的代码来决定修复方式,总原则和上面的第三点一样,调用parseRequest/parseParameterMap/getItemIterator方法前需要调用setFileCountMax方法。已知的一些三方件的情况如下:
Struts:Struts中默认使用Commons FileUpload来解析上传的文件(struts.multipart.parser=jakarta
),且不支持调用setFileCountMax
方法。可以通过实现org.apache.struts2.dispatcher.multipart.MultiPartRequest
接口来自定义上传文件的解析逻辑。
[1]:Apache Commons FileUpload官方修复:(https://github.com/apache/commons-fileupload/commit/e20c04990f7420ca917e96a84cec58b13a1b3d17)
[2]: Apache Tomcat 9 修复:(https://github.com/apache/tomcat/commit/cf77cc545de0488fb89e24294151504a7432df74)
本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
原文始发于微信公众号(华为安全应急响应中心):CVE-2023-24998 Apache Commons FileUpload 拒绝服务漏洞分析