描述
Spring Framework, version 5.1, versions 5.0.x prior to 5.0.10, versions 4.3.x prior to 4.3.20, and older unsupported versions on the 4.2.x branch provide support for range requests when serving static resources through the ResourceHttpRequestHandler
, or starting in 5.0 when an annotated controller returns an org.springframework.core.io.Resource
. A malicious user (or attacker) can add a range header with a high number of ranges, or with wide ranges that overlap, or both, for a denial of service attack.
此漏洞会影响依赖于spring-webmvc或spring-webflux的应用程序。此类应用程序还必须具有服务静态资源的注册(例如JS,CSS,图像和其他),或者具有返回的注释控制器org.springframework.core.io.Resource
。
依赖于spring-boot-starter-web或spring-boot-starter-webflux的Spring Boot应用程序已准备好提供开箱即用的静态资源,因此容易受到攻击。
触发条件
Note the following when evaluating the impact:
Support for Range requests was introduced in version 4.2. Therefore versions prior to 4.2 are not affected by this issue.
Support for returning an
org.springfamework.core.io.Resource
from an annotated controller was introduced in 5.0. Therefore versions prior to 5.0 can only be impacted through a registration to serve static resources.
必备背景知识
- 问题表达得很清楚,range这个Header头在4.2版本之后被引入,主要负责做大文件断点续传。用户可以使用以下header头进行相应工作以节省计算成本(试下载指定部分文件)。
> GET /big_buck_bunny_1080p_surround.avi HTTP/1.1
> Range: bytes=0-9,1000-1009
如果目标服务器支持范围请求,则它响应206 Partial Content
< HTTP/1.1 206 Partial Content
< Last-Modified: Tue, 06 May 2008 11:21:35 GMT
< ETag: "8000089-375a6422-44c8e0d0f0dc0"
< Accept-Ranges: bytes
< Content-Length: 100
< Content-Range: bytes 0-99/928670754
注意此处Range后面参数个数是攻击者可以自定义的,那么攻击者可以写很大数量的Range,甚至可以有重叠,把服务端资源耗光导致DoS.
> GET /big_buck_bunny_1080p_surround.avi HTTP/1.1
> Range: bytes=0-1009,1-1009,2-1009,3-1009
...
HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
ranges = headers.getRange();
...
if (ranges.size() == 1) {
//略
}
else {
String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
response.setContentType("multipart/byteranges; boundary=" + boundaryString);
ServletOutputStream out = response.getOutputStream();
for (HttpRange range : ranges) {
long start = range.getRangeStart(length);
long end = range.getRangeEnd(length);
InputStream in = resource.getInputStream();
// Writing MIME header.
out.println();
out.println("--" + boundaryString);
if (contentType != null) {
out.println("Content-Type: " + contentType);
}
out.println("Content-Range: bytes " + start + "-" + end + "/" + length);
out.println();
// Printing content
copyRange(in, out, start, end);
}
out.println();
out.print("--" + boundaryString + "--");
}
从上述代码看出,主要关键在headers.getRange()处有没有做Range的个数校验,headers的类名是HttpHeaders,跟进去可以看,具体又是包装HttpRange.java这个类的方法。
public List<HttpRange> getRange() {
String value = getFirst(RANGE);
return HttpRange.parseRanges(value);
}
对比HttpRange这个类的parseRanges方法代码最近做的变更,大概就是限制Range个数为100个以内(详见:https://github.com/spring-projects/spring-framework/blob/423aa28ed584b4ff6e5bad218c09beef5e91951e/spring-web/src/main/java/org/springframework/http/HttpRange.java是修复后代码,修复前的可以点击History)
那么我猜攻击payload就是发送Range后面大量重复的bytes呗(大于100个,譬如1000个)
Range: bytes=0-9,1000-1009
两二个特征(条件):
1. 使用了ResourceHttpRequestHandler + 支持Range,
2. 使用Controller注解返回了org.springfamework.core.io.Resource.
待补充
- 实际Demo环境调试结果。
问题关键词
ResourceHttpRequestHandler。
org.springframework.core.io.Resource。