
Spring 安全漏洞 CVE-2020-5421复现
CVE-2020-5421 permet de contourner la protection contre les attaques RFD via le paramètre de chemin jsessionid. La protection précédente contre RFD a été ajoutée pour faire face à CVE-2015-5211.
Qu'est-ce que RFD
La vulnérabilité de téléchargement de fichier réflexif (RFD) est une technique d'attaque qui permet à un attaquant d'obtenir un accès complet à l'ordinateur de la victime en téléchargeant virtuellement un fichier à partir d'un domaine de confiance.
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
Adresse GitHub : https://github.com/pandaMingx/CVE-2020-5421
Testé avec SpringBoot-2.1.7.RELEASE et 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;
}
}
Configuration supplémentaire
spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.contentnegotiation.favor-path-extension=true
Ajoutez ;jsessionid= dans l'URL, par exemple http://localhost:8080/spring/;jsessionid=/input.bat?input=calc, et un fichier exécutable nommé input.bat sera téléchargé.
CVE-2020-5421 est un contournement du correctif de CVE-2015-5211. Localisez le code de correction 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");
}
}
En raison de ce code qui supprime ;jsessionid=, après la suppression de ;jsessionid=, le code de défense ultérieur de CVE-2015-5211 ne peut pas obtenir le véritable suffixe du fichier de la requête, contournant ainsi le code de défense RFD.
Lors de la reproduction de la vulnérabilité, deux paramètres ont été ajoutés dans application.properties : spring.mvc.pathmatch.use-suffix-pattern=true et spring.mvc.contentnegotiation.favor-path-extension=true (par défaut false dans SpringBoot).
Ainsi, la condition d'exploitation de CVE-2020-5421 est que le mode de correspondance des suffixes et le mécanisme de négociation de contenu doivent être activés. Si ces deux modes ne sont pas activés dans un projet SpringBoot, il n'y a pas de condition d'exploitation et aucune action n'est nécessaire.
Si les conditions d'exploitation existent, deux solutions sont proposées ici. La solution 2 est adaptée aux projets où la mise à niveau de la version Spring présente un risque élevé.
Spring Framework 5.2.9
Spring Framework 5.1.18
Spring Framework 5.0.19
Spring Framework 4.3.29
La solution 2 vérifie si le suffixe de l'URL contenant ;jsessionid= est un suffixe sûr. Si ce n'est pas le cas, elle définit Content-Disposition=inline;filename=f.txt, forçant le contenu de la réponse à être téléchargé dans un fichier nommé f.txt. (La méthode est cohérente avec le mécanisme de défense RFD 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;
}
}