Spring 安全漏洞 CVE-2020-5421復(fù)現(xiàn)

漏洞概述

CVE-2020-5421 可通過jsessionid路徑參數(shù),繞過防御RFD攻擊的保護(hù)。先前針對RFD的防護(hù)是為應(yīng)對 CVE-2015-5211 添加的。
什么是RFD

反射型文件下載漏洞(RFD)是一種攻擊技術(shù),通過從受信任的域虛擬下載文件,攻擊者可以獲得對受害者計(jì)算機(jī)的完全訪問權(quán)限。

影響版本

Spring Framework 5.2.0 - 5.2.8
Spring Framework 5.1.0 - 5.1.17
Spring Framework 5.0.0 - 5.0.18
Spring Framework 4.3.0 - 4.3.28

漏洞復(fù)現(xiàn)

github地址:https://github.com/pandaMingx/CVE-2020-5421

版本

基于SpringBoot-2.1.7.RELEASE,Spring-xxx-5.1.9.RELEASE進(jìn)行測試。

   <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>

復(fù)現(xiàn)代碼

@Controller
@RequestMapping(value = "spring")
public class cve20205421 {

    // localhost:8080/spring/input?input=hello
    @RequestMapping("input")
    @ResponseBody
    public String input(String input){
        return input;
    }
}

額外配置

spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.contentnegotiation.favor-path-extension=true

在url中添加;jsessionid=,如http://localhost:8080/spring/;jsessionid=/input.bat?input=calc,就會(huì)下載名為input.bat的可執(zhí)行文件。

漏洞分析

CVE-2020-5421是針對CVE-2015-5211修復(fù)方式的繞過,定位到CVE-2015-5211的修復(fù)代碼
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor. addContentDispositionHeader

/**
     * Check if the path has a file extension and whether the extension is
     * either {@link #WHITELISTED_EXTENSIONS whitelisted} or explicitly
     * {@link ContentNegotiationManager#getAllFileExtensions() registered}.
     * If not, and the status is in the 2xx range, a 'Content-Disposition'
     * header with a safe attachment file name ("f.txt") is added to prevent
     * RFD exploits.
     */
    private void addContentDispositionHeader(ServletServerHttpRequest request, ServletServerHttpResponse response) {
        HttpHeaders headers = response.getHeaders();
        if (headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
            return;
        }

        try {
            int status = response.getServletResponse().getStatus();
            if (status < 200 || status > 299) {
                return;
            }
        }
        catch (Throwable ex) {
            // ignore
        }

        HttpServletRequest servletRequest = request.getServletRequest();
        String requestUri = rawUrlPathHelper.getOriginatingRequestUri(servletRequest);

        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);
        }

        filename = decodingUrlPathHelper.decodeRequestString(servletRequest, filename);
        String ext = StringUtils.getFilenameExtension(filename);

        pathParams = decodingUrlPathHelper.decodeRequestString(servletRequest, pathParams);
        String extInPathParams = StringUtils.getFilenameExtension(pathParams);

        if (!safeExtension(servletRequest, ext) || !safeExtension(servletRequest, extInPathParams)) {
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=f.txt");
        }
    }

跟進(jìn)rawUrlPathHelper.getOriginatingRequestUri方法,一路跟進(jìn)定位到org.springframework.web.util.UrlPathHelper.removeJsessionid方法中會(huì)將請求url中;jsessionid=字符串開始進(jìn)行截?cái)?或者下一個(gè);前)。

private String removeJsessionid(String requestUri) {
        int startIndex = requestUri.toLowerCase().indexOf(";jsessionid=");
        if (startIndex != -1) {
            int endIndex = requestUri.indexOf(59, startIndex + 12);
            String start = requestUri.substring(0, startIndex);
            requestUri = endIndex != -1 ? start + requestUri.substring(endIndex) : start;
        }

        return requestUri;
    }

由于這段刪除;jsessionid=的代碼,造成刪除;jsessionid=之后CVE-2015-5211的后續(xù)防御代碼即將獲取不到請求的真實(shí)后綴文件名,從而繞過RDF防御代碼。

修復(fù)建議

漏洞復(fù)現(xiàn)的過程中,在applcation.properties中添加了兩個(gè)參數(shù):spring.mvc.pathmatch.use-suffix-pattern=true,spring.mvc.contentnegotiation.favor-path-extension=true(SpringBoot中默認(rèn)為false)
可見,CVE-2020-5421的利用條件是必須要開啟后綴匹配模式和內(nèi)容協(xié)商機(jī)制。如果SpringBoot項(xiàng)目中沒有啟用這兩種模式則不存在漏洞利用條件,可不處理。
如果存在漏洞利用條件,這里提供兩個(gè)方案,其中方案二適用于升級Spring版本風(fēng)險(xiǎn)較大的項(xiàng)目。

方案一、升級Spring版本到安全版本:

Spring Framework 5.2.9
Spring Framework 5.1.18
Spring Framework 5.0.19
Spring Framework 4.3.29

方案二、添加安全過濾器

方案二將校驗(yàn)含有;jsessionid=的ULR的后綴是否為安全后綴,如果不是則設(shè)置Content-Disposition=inline;filename=f.txt,強(qiáng)制將響應(yīng)的內(nèi)容下載到名為f.txt的文件中。(做法和spring的RDF防御機(jī)制一致)

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;
    }

}

參考文檔

*https://www.xf1433.com/4595.html
*https://www.nsfocus.com.cn/html/2020/39_0921/976.html
*https://zhuanlan.zhihu.com/p/161166505
*https://github.com/spring-projects/spring-framework/commit/2281e421915627792a88acb64d0fea51ad138092

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容