
Riproduzione della vulnerabilità di sicurezza Spring CVE-2020-5421
CVE-2020-5421 può aggirare la protezione contro gli attacchi RFD tramite il parametro di percorso jsessionid. La protezione precedente contro RFD è stata aggiunta per far fronte a CVE-2015-5211.
Cos'è RFD
La vulnerabilità di download riflesso di file (RFD) è una tecnica di attacco che, scaricando virtualmente un file da un dominio attendibile, consente all'attaccante di ottenere pieno accesso al computer della vittima.
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
Indirizzo GitHub: https://github.com/pandaMingx/CVE-2020-5421
Testato 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;
}
}
Configurazione aggiuntiva
spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.contentnegotiation.favor-path-extension=true
Aggiungendo ;jsessionid= all'URL, ad esempio http://localhost:8080/spring/;jsessionid=/input.bat?input=calc, verrà scaricato un file eseguibile chiamato input.bat.
CVE-2020-5421 è un bypass della correzione di CVE-2015-5211, che porta al codice di correzione di 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");
}
}
Seguendo il metodo rawUrlPathHelper.getOriginatingRequestUri, si individua il metodo org.springframework.web.util.UrlPathHelper.removeJsessionid, che tronca la stringa a partire da ;jsessionid= nella URL della richiesta (o prima del successivo ;).
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;
}
A causa di questo codice che rimuove ;jsessionid=, dopo la rimozione di ;jsessionid= il codice di difesa successivo di CVE-2015-5211 non riesce più a ottenere il vero suffisso del file della richiesta, aggirando così il codice di difesa RFD.
Durante la riproduzione della vulnerabilità, in applcation.properties sono stati aggiunti due parametri: spring.mvc.pathmatch.use-suffix-pattern=true, spring.mvc.contentnegotiation.favor-path-extension=true (per impostazione predefinita false in SpringBoot).
È evidente che la condizione di sfruttamento di CVE-2020-5421 richiede l'attivazione della modalità di corrispondenza dei suffissi e del meccanismo di negoziazione dei contenuti. Se nel progetto SpringBoot queste due modalità non sono abilitate, non sussistono condizioni di sfruttamento e non è necessario intervenire.
Se sussistono condizioni di sfruttamento, ecco due soluzioni; la soluzione 2 è adatta ai progetti in cui l'aggiornamento della versione di Spring comporta rischi maggiori.
Spring Framework 5.2.9
Spring Framework 5.1.18
Spring Framework 5.0.19
Spring Framework 4.3.29
La soluzione 2 verifica se il suffisso delle URL contenenti ;jsessionid= è un suffisso sicuro; in caso contrario, imposta Content-Disposition=inline;filename=f.txt, forzando il download del contenuto della risposta in un file denominato f.txt. (L'approccio è coerente con il meccanismo di difesa RFD di 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;
}
}