Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
도구/GitHubGitHub/pandamingx/cve-2020-5421
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & Education
GitHubpandamingx/cve-2020-5421

CVE-2020-5421

Spring 보안 취약점 CVE-2020-5421 재현

저장소 보기
35년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Spring 보안 취약점 CVE-2020-5421 재현

취약점 개요

CVE-2020-5421은 jsessionid 경로 매개변수를 통해 RFD 공격을 방어하는 보호를 우회할 수 있습니다. 이전의 RFD 방어는 CVE-2015-5211에 대응하여 추가되었습니다.
RFD란 무엇인가

반사형 파일 다운로드 취약점(RFD)은 신뢰할 수 있는 도메인에서 파일을 가상으로 다운로드하게 하여 공격자가 피해자 컴퓨터에 대한 완전한 접근 권한을 얻을 수 있는 공격 기술입니다.

영향받는 버전

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

취약점 재현

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

버전

SpringBoot-2.1.7.RELEASE, Spring-xxx-5.1.9.RELEASE 기반으로 테스트했습니다.

root@kitploit:~
   <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>

재현 코드

root@kitploit:~
@Controller
@RequestMapping(value = "spring")
public class cve20205421 {

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

추가 설정

root@kitploit:~
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 와 같이 요청할 경우 input.bat이라는 이름의 실행 파일이 다운로드됩니다.

취약점 분석

CVE-2020-5421은 CVE-2015-5211 수정 방식에 대한 우회입니다. CVE-2015-5211의 수정 코드인 org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.addContentDispositionHeader를 확인합니다.

root@kitploit:~
/**
	 * 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");
		}
	}

rawUrlPathHelper.getOriginatingRequestUri 메서드를 따라가다 보면, org.springframework.web.util.UrlPathHelper.removeJsessionid 메서드에서 요청 URL의 ;jsessionid= 문자열부터 (또는 다음 ; 앞까지) 잘라낸다는 것을 확인할 수 있습니다.

root@kitploit:~
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의 후속 방어 코드가 요청의 실제 확장자 파일명을 가져올 수 없게 되며, 이를 통해 RFD 방어 코드를 우회하게 됩니다.

수정 권장 사항

취약점을 재현하는 과정에서 applcation.properties에 두 개의 매개변수를 추가했습니다: spring.mvc.pathmatch.use-suffix-pattern=true, spring.mvc.contentnegotiation.favor-path-extension=true (SpringBoot에서는 기본값이 false입니다). 따라서 CVE-2020-5421의 이용 조건은 반드시 접미사 매칭 모드와 콘텐츠 협상 메커니즘을 활성화해야 한다는 것입니다. SpringBoot 프로젝트에서 이 두 모드를 활성화하지 않았다면 취약점 이용 조건이 성립하지 않으므로 처리하지 않아도 됩니다.
취약점 이용 조건이 존재한다면 두 가지 방안을 제공합니다. 그중 방안 2는 Spring 버전 업그레이드 시 위험이 큰 프로젝트에 적합합니다.

방안 1: Spring 버전을 안전한 버전으로 업그레이드:

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

방안 2: 보안 필터 추가

방안 2는 **;jsessionid=**를 포함하는 URL의 접미사가 안전한 접미사인지 검증합니다. 안전하지 않다면 Content-Disposition=inline;filename=f.txt를 설정하여 응답 콘텐츠를 f.txt라는 파일로 강제 다운로드하게 합니다. (spring의 RFD 방어 메커니즘과 동일한 방식입니다)

root@kitploit:~
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
도구 다운로드