
Reproducción de la vulnerabilidad de seguridad Spring CVE-2020-5421
CVE-2020-5421 puede eludir la protección contra ataques RFD mediante el parámetro de ruta jsessionid. La protección anterior contra RFD se agregó para abordar CVE-2015-5211.
¿Qué es RFD?
La vulnerabilidad de descarga de archivos reflejada (RFD) es una técnica de ataque en la que, al simular la descarga de un archivo desde un dominio de confianza, el atacante puede obtener acceso completo a la computadora de la víctima.
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
Dirección de GitHub: https://github.com/pandaMingx/CVE-2020-5421
Probado con 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>
@Controller
@RequestMapping(value = "spring")
public class cve20205421 {
// localhost:8080/spring/input?input=hello
@RequestMapping("input")
@ResponseBody
public String input(String input){
return input;
}
}
Configuración adicional
spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.contentnegotiation.favor-path-extension=true
Agregue ;jsessionid= en la URL, por ejemplo: http://localhost:8080/spring/;jsessionid=/input.bat?input=calc, se descargará un archivo ejecutable llamado input.bat.
CVE-2020-5421 es una evasión de la corrección de CVE-2015-5211. Localice el código de corrección de CVE-2015-5211: 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");
}
}
Siga el método rawUrlPathHelper.getOriginatingRequestUri, hasta llegar a org.springframework.web.util.UrlPathHelper.removeJsessionid, donde la cadena ;jsessionid= en la URL de la solicitud se trunca (o antes del siguiente punto y coma).
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;
}
Debido a este código que elimina ;jsessionid=, después de eliminar ;jsessionid=, el código de defensa posterior de CVE-2015-5211 no puede obtener la extensión real del archivo de la solicitud, evadiendo así la defensa RDF.
Durante la replicación de la vulnerabilidad, se agregaron dos parámetros en application.properties: spring.mvc.pathmatch.use-suffix-pattern=true, spring.mvc.contentnegotiation.favor-path-extension=true (por defecto en SpringBoot es false).
Por lo tanto, la condición de explotación de CVE-2020-5421 es que deben estar habilitados el modo de coincidencia de sufijos y el mecanismo de negociación de contenido. Si el proyecto SpringBoot no tiene estos dos modos habilitados, no existe condición de explotación y no es necesario tratarlo.
Si existen condiciones de explotación, se proporcionan dos soluciones, de las cuales la solución dos es adecuada para proyectos con alto riesgo de actualizar la versión de Spring.
Spring Framework 5.2.9
Spring Framework 5.1.18
Spring Framework 5.0.19
Spring Framework 4.3.29
La solución dos verificará si la extensión de la URL que contiene ;jsessionid= es una extensión segura. Si no lo es, establecerá Content-Disposition=inline;filename=f.txt, forzando a que el contenido de la respuesta se descargue en un archivo llamado f.txt. (El método es consistente con el mecanismo de defensa RDF de Spring).
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;
}
}