Apache Commons Fileupload Dos漏洞分析

说明

Apache Commons Fileupload是一个用于处理文件上传的库。本文就分析Apache Commons Fileupload的两个老洞,包括CVE2014-0050和CVE-2016-3092。两个漏洞都是由同一个地方导致的,都是由于对boundary的处理的逻辑不够严谨造成的。CVE2014-0050是由于对boundary的处理没有校验出现无限循环而导致的Dos漏洞;CVE-2016-3092则是赋值操作存在问题,程序会不断的以boundary的长度来开辟内存空间进而导致内存的耗尽。CVE-2014-0050是在1.3.1之前的版本,CVE-2016-3092出现在1.3.2之前的版本,

环境搭建

搭建一个简单的SpringMVC的项目,使用gradle导入Apache Commons Fileupload的版本1.3版本,如下:

1
2
3
4
5
6
dependencies {
/**
* omit other dependencies
*/
compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.3'
}

在路由中添加一个对于文件上传处理的路由,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RequestMapping(value="/fileupload")
public String uploadFileHandler(HttpServletRequest request) {
boolean flag = false;
//判断是否是文件上传请求
if(ServletFileUpload.isMultipartContent(request)){
// 创建文件上传处理器
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
//限制单个上传文件的大小
upload.setFileSizeMax(1L<<24);
try {
List<FileItem> list = upload.parseRequest(request);
for (FileItem item : list) {
// 普通表单项
if (item.isFormField()) {
String name = item.getFieldName();
String value = item.getString("UTF-8");
System.out.println(name + " : " + value);
} else {// 文件表单项
// 文件名
String fileName = item.getName();
// 生成唯一文件名
fileName = UUID.randomUUID().toString() + "#" + fileName;
// 获取上传路径:项目目录下的upload文件夹(先创建upload文件夹)
String basePath = request.getServletContext().getRealPath("/upload");
// 创建文件对象
File file = new File(basePath, fileName);
// 写文件(保存)
item.write(file);
// 删除临时文件
item.delete();
}
}
} catch (FileUploadException e) {
System.out.println("上传文件过大");
} catch (IOException e) {
System.out.println("文件读取出现问题");
} catch (Exception e) {
e.printStackTrace();
}
}
return flag? "success":"error";
}

配合一个简单的文件上传的页面,如下:

1
2
3
4
5
6
<form method="post" action="/fileupload" enctype="multipart/form-data">
选择一个文件:
<input type="file" name="uploadFile" />
<br/><br/>
<input type="submit" value="上传" />
</form>

至此,整个漏洞的环境搭建完毕。

CVE2014-0050漏洞分析

在通过CVE2014-0050的修复commit分析发现,其中的修复关键地方是对org.apache.commons.fileupload.MultipartStream.java进行了如下的修改:

增加代码的含义是对this.boundaryLength进行了限制,如果超过了bufSize则抛出异常。bufSize的长度的定义是DEFAULT_BUFSIZE = 4096,默认是是4096的长度。分析下org.apache.commons.fileupload.MultipartStream.java::MultipartStream()代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MultipartStream(InputStream input,
byte[] boundary,
int bufSize,
ProgressNotifier pNotifier) {
this.input = input;
this.bufSize = bufSize;
this.buffer = new byte[bufSize];
this.notifier = pNotifier;

// We prepend CR/LF to the boundary to chop trailing CR/LF from
// body-data tokens.
this.boundary = new byte[boundary.length + BOUNDARY_PREFIX.length];
this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;
this.keepRegion = this.boundary.length;
System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
BOUNDARY_PREFIX.length);
System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
boundary.length);

head = 0;
tail = 0;
}

其中的this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;就是boundary的长度加上默认的BOUNDARY_PREFIX.length(值为4)。最终程序运行至org.apache.commons.fileupload.MultipartStream::makeAvailable(),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private int makeAvailable() throws IOException {
if (pos != -1) {
return 0;
}

// Move the data to the beginning of the buffer.
total += tail - head - pad;
System.arraycopy(buffer, tail - pad, buffer, 0, pad);

// Refill buffer with new data.
head = 0;
tail = pad;

for (;;) {
int bytesRead = input.read(buffer, tail, bufSize - tail);
if (bytesRead == -1) {
// The last pad amount is left in the buffer.
// Boundary can't be in there so signal an error
// condition.
final String msg = "Stream ended unexpectedly";
throw new MalformedStreamException(msg);
}
if (notifier != null) {
notifier.noteBytesRead(bytesRead);
}
tail += bytesRead;

findSeparator();
int av = available();

if (av > 0 || pos != -1) {
return av;
}
}
}

根据input.read(buffer, tail, bufSize - tail)返回的结果决定是否退出。但是当我们的自定义的boundary的长度超过了4096之后,int bytesRead = input.read(buffer, tail, bufSize - tail)中的bytesRead永远不会返回-1,最终的结果如下所示:

如上图所示,最终bytesRead返回的结果永远是0,导致for循环就无法退出了,从而形成了Dos攻击。当我们多开几个线程请求时,CPU都会飙升。如下所示:
)

漏洞的修复就是通过增加对boundary的判断实现了,禁止boundary的长度过长,否则就会抛出异常。如下:

CVE-2016-3092漏洞分析

首先分析一下的commit信息,如下所示:

将赋值操作放到了判断boundary的长度之后,同时通过this.bufSize = Math.max(bufSize, boundaryLength*2);增加了bufSize的长度。之所以进行这样调整的原因是在于进行判断之前就会进行大量地赋值操作,将数据放到内存当中,增加了系统的开销,这个漏洞如果要触发就需要发送大量地请求,设置boundary恰好满足条件,即为4090。这样就会大量地消耗系统的内存。

根据Apache Commons Fileupload 1.3.1 DOS(CVE-2016-3092)的提示,构造一个boundary大小为1000000字节的数据包,循环发送500次请求对1.3.1的版本进行测试就会出现内存大量消耗的问题;

总结

看似简单的代码很有可能会存在安全问题,这也告诫我们不能随意地将用户的输入放入到数据库中进行查询或者是放置到内存中。