Spring连环漏洞CVE-2015-5211和CVE-2020-5421修复升级教程!

JAVA herman 233浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:codedq,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:codedq,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

我们不造轮子,但需要学会修复轮子!

对于一个开源项目,多数程序员会点 Star,少数程序员会 Fork,只有极少数的程序员会点 Watch。而我一般会点 Star 和 Watch。

Spring开源代码
Spring开源代码

我点 Watch 的原因是,观察这个框架最新的改动,提交了哪些新代码,为什么提交这些代码等等。关注这些内容,往往对我有非常大的惊喜,我从中学到不少知识,同时也会第一时间获得开源框架的漏洞修复情况。

昨天周五下班时间,客户转发了一封邮件给我。邮件显示 Spring 有一个漏洞需要修复。

CVE-2015-5211
CVE-2015-5211

看这个漏洞编号就知道,它是一个 2015 年就报告的一个漏洞。

CVE-2015-5211
CVE-2015-5211

关于这个漏洞的说明和修复,网上的资料都很少,就像是刚被发现的一样。通过搜索查找得知,这个漏洞的发现与复现来自于一篇论文(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 和其他许多公司认为这是一个中危漏洞。

RFD 漏洞
RFD 漏洞

也就是说这个漏洞伤害性不大,屈辱性也不强。通过这个漏洞能够打开我们的计算器,关闭电脑等。

## 批处理执行时会打开计算器
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 完成后。在 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包
Spring升级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”,不在白名单内的就被拦截!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号1:xmtxtt(5000人已满),微信号2:xttblog(5000人已满),微信号3:codedq(超3800)。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » Spring连环漏洞CVE-2015-5211和CVE-2020-5421修复升级教程!