我们不造轮子,但需要学会修复轮子!
对于一个开源项目,多数程序员会点 Star,少数程序员会 Fork,只有极少数的程序员会点 Watch。而我一般会点 Star 和 Watch。
我点 Watch 的原因是,观察这个框架最新的改动,提交了哪些新代码,为什么提交这些代码等等。关注这些内容,往往对我有非常大的惊喜,我从中学到不少知识,同时也会第一时间获得开源框架的漏洞修复情况。
昨天周五下班时间,客户转发了一封邮件给我。邮件显示 Spring 有一个漏洞需要修复。
看这个漏洞编号就知道,它是一个 2015 年就报告的一个漏洞。
关于这个漏洞的说明和修复,网上的资料都很少,就像是刚被发现的一样。通过搜索查找得知,这个漏洞的发现与复现来自于一篇论文(https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/reflected-file-download-a-new-web-attack-vector/
),而之所以不受重视是因为谷歌和 Spring 团队认为这个漏洞影响并不大。
CVE-2015-5211 就是一个我们常见的 RFD 漏洞。RFD,即Reflected File Download
反射型文件下载漏洞,是一个 2014 年来自 BlackHat 的漏洞。这个漏洞在原理上类似 XSS,在危害上类似 DDE:攻击者可以通过一个 URL 地址使用户下载一个恶意文件,从而危害用户的终端 PC。
这个漏洞很罕见,大多数公司会认为它是一个需要结合社工的低危漏洞,但谷歌,微软,雅虎,eBay,PayPal 和其他许多公司认为这是一个中危漏洞。
也就是说这个漏洞伤害性不大,屈辱性也不强。通过这个漏洞能够打开我们的计算器,关闭电脑等。
## 批处理执行时会打开计算器
http://www.google.com/finance/info;setup.bat?q=ELI:ALTR&callback=calc
## 批处理执行时windows会注销
http://www.google.com/finance/info;setup.bat?q=ELI:ALTR&callback=logoff
所以,对该问题只是简单通报,对再现和验证也不够重视,对可见的修改方案就更不要奢望了。后来,官方在升级其他漏洞和功能时,也对这个漏洞进行了一并修复。
- 修改 CVE-2015-5211 补丁提交记录 Protect against RFD exploits(
https://github.com/spring-projects/spring-framework/commit/2bd1da
) - 修改 CVE-2015-5211 参考补丁提交记录Avoid unnecessary parsing of path params(
https://github.com/spring-projects/spring-framework/commit/899761f0a8890353dc01f10d2af96ae192f3e655
)
就是因为不够重视,官方在修复 CVE-2015-5211
完成后。在 2020 年,又被发现,当年修复的 CVE-2015-5211 漏洞,引出了新的漏洞:CVE-2020-5421
。
拔起萝卜带起泥。CVE-2020-5421
的漏洞是在修复CVE-2015-5211
时,留下的一个漏洞。在对 url 做过滤查找文件名称前,先针对性的处理了;jsessionid=xxxx;
。在发现;jsessionId=
开始到下一个分号结束的部分内不检查是否存在文件名称,而漏洞就可以通过;jsessionid=ssddfeff&setup.bat
这样的方式存在了。
不好理解,我们来看一个例子。
当我们在浏览器执行:http://localhost:8080/spring/;jsessionid=/input.bat?input=calc
。原本的打开计算器被修复了,现在变成了下载名为input.bat
的可执行文件。
复现过程超级简单,主要代码我整理成了一个 demo,一起来看一下。
先使用包含漏洞的版本,基于 SpringBoot-2.1.7.RELEASE、Spring-xxx-5.1.9.RELEASE 进行测试。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
复现配置:
spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.contentnegotiation.favor-path-extension=true
复现代码:
@Controller
@RequestMapping(value = "spring")
public class cve20205421 {
// localhost:8080/spring/input?input=hello
@RequestMapping("input")
@ResponseBody
public String input(String input){
return input;
}
}
运行以上代码,在 url 中添加;jsessionid=
。如http://localhost:8080/spring/;jsessionid=/.bat?input=calc
,就会下载名为.bat
的可执行文件。
OK,前面扯了那么多,都是没用了。最主要的还是修复方案。
修复方案一共有 3 种。方案一:把上面的复现配置全部改为 false。如果这一步你做不到,那就采用方案二。
方案二:升级 jar 包。
现在 Spring 的版本基本上都被其他第三方的 jar 强依赖。不建议进行大版本的升级,所以,建议还是,5.x 的升级 5.x 的最新版;4.x 升级到 4.x 的最新版;3.x 也升级到 3.x 的最新版。低于 3.x 版本的,那只能改代码重新编译了,因为 3.x 以下的版本,官方都不在维护了。
Spring 版本升级,根据 Maven 的依赖最短路径优先原则,分分钟搞定。如有不会的可以加我微信:codedq,免费教学。
修复方案三:添加安全过滤器。
public class SpringJsessionidRdfFilter implements Filter {
private final Set<String> safeExtensions = new HashSet<>();
/* Extensions associated with the built-in message converters */
private static final Set<String> WHITELISTED_EXTENSIONS = new HashSet<>(Arrays.asList(
"txt", "text", "yml", "properties", "csv",
"json", "xml", "atom", "rss",
"png", "jpe", "jpeg", "jpg", "gif", "wbmp", "bmp"));
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
String contentDisposition = response.getHeader(HttpHeaders.CONTENT_DISPOSITION);
if (!"".equals(contentDisposition)&&null != contentDisposition) {
return;
}
try {
int status = response.getStatus();
if (status < 200 || status > 299) {
return;
}
}
catch (Throwable ex) {
// ignore
}
String requestUri = request.getRequestURI();
System.out.println(requestUri);
if(requestUri.contains(";jsessionid=")){
int index = requestUri.lastIndexOf('/') + 1;
String filename = requestUri.substring(index);
String pathParams = "";
index = filename.indexOf(';');
if (index != -1) {
pathParams = filename.substring(index);
filename = filename.substring(0, index);
}
UrlPathHelper decodingUrlPathHelper = new UrlPathHelper();
filename = decodingUrlPathHelper.decodeRequestString(request, filename);
String ext = StringUtils.getFilenameExtension(filename);
pathParams = decodingUrlPathHelper.decodeRequestString(request, pathParams);
String extInPathParams = StringUtils.getFilenameExtension(pathParams);
if (!safeExtension(request, ext) || !safeExtension(request, extInPathParams)) {
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=f.txt");
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
private boolean safeExtension(HttpServletRequest request, @Nullable String extension) {
if (!StringUtils.hasText(extension)) {
return true;
}
extension = extension.toLowerCase(Locale.ENGLISH);
this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
if (this.safeExtensions.contains(extension)) {
return true;
}
String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (pattern != null && pattern.endsWith("." + extension)) {
return true;
}
if (extension.equals("html")) {
String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(name);
if (!CollectionUtils.isEmpty(mediaTypes) && mediaTypes.contains(MediaType.TEXT_HTML)) {
return true;
}
}
return false;
}
}
核心原理就是:解析 url中的最后一节(最后一个'/'之后的内容'),如果存在文件名称,取得其扩展名。如果不是如下的白名单中的类型,就在头信息中设置
:
"Content-Disposition: inline;filename=f.txt"
让其以固定的文件名称 f.txt 下载,以避免出现不受控制的文件类型。
限制文件类型白名单为:”txt”, “text”, “json”, “xml”, “atom”, “rss”, “png”, “jpe”, “jpeg”, “jpg”, “gif”, “wbmp”, “bmp”,不在白名单内的就被拦截!
: » Spring连环漏洞CVE-2015-5211和CVE-2020-5421修复升级教程!
原创文章,作者:1402239773,如若转载,请注明出处:https://blog.ytso.com/252294.html