
Spring 安全漏洞 CVE-2020-5421复现
CVE-2020-5421 kann über den jsessionid-Pfadparameter den Schutz gegen RFD-Angriffe umgehen. Der vorherige Schutz gegen RFD wurde als Reaktion auf CVE-2015-5211 hinzugefügt.
Was ist RFD?
Reflective File Download (RFD) ist eine Angriffstechnik, bei der ein Angreifer durch das virtuelle Herunterladen einer Datei von einer vertrauenswürdigen Domäne vollständigen Zugriff auf den Computer des Opfers erlangen kann.
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
Getestet mit SpringBoot-2.1.7.RELEASE und 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;
}
}
Zusätzliche Konfiguration
spring.mvc.pathmatch.use-suffix-pattern=true
spring.mvc.contentnegotiation.favor-path-extension=true
Fügen Sie ;jsessionid= zur URL hinzu, z.B. http://localhost:8080/spring/;jsessionid=/input.bat?input=calc, dann wird eine ausführbare Datei namens input.bat heruntergeladen.
CVE-2020-5421 umgeht die Reparaturmethode von CVE-2015-5211. Die Reparatur von CVE-2015-5211 befindet sich in der Methode addContentDispositionHeader der Klasse AbstractMessageConverterMethodProcessor unter org.springframework.web.servlet.mvc.method.annotation.
/**
* 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");
}
}
Wenn man der Methode rawUrlPathHelper.getOriginatingRequestUri folgt, gelangt man zur Methode org.springframework.web.util.UrlPathHelper.removeJsessionid, die die Zeichenfolge ;jsessionid= in der Anfrage-URL abschneidet (oder vor dem nächsten ;).
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;
}
Da dieser Code ;jsessionid= entfernt, kann der nachfolgende Verteidigungscode von CVE-2015-5211 nach der Entfernung von ;jsessionid= den tatsächlichen Dateinamensuffix der Anfrage nicht mehr abrufen, wodurch der RFD-Abwehrcode umgangen wird.
Während der Reproduktion der Sicherheitslücke wurden in der Datei application.properties zwei Parameter hinzugefügt: spring.mvc.pathmatch.use-suffix-pattern=true, spring.mvc.contentnegotiation.favor-path-extension=true (Standardmäßig false in SpringBoot). Es ist ersichtlich, dass die Ausnutzungsbedingung von CVE-2020-5421 das Aktivieren des Suffix-Matching-Modus und des Content-Negotiation-Mechanismus erfordert. Wenn in einem SpringBoot-Projekt diese beiden Modi nicht aktiviert sind, besteht keine Ausnutzungsbedingung, und es muss nicht behandelt werden.
Falls die Ausnutzungsbedingungen vorliegen, werden hier zwei Lösungen angeboten, wobei Lösung 2 für Projekte geeignet ist, bei denen ein Upgrade der Spring-Version ein hohes Risiko darstellt.
Spring Framework 5.2.9
Spring Framework 5.1.18
Spring Framework 5.0.19
Spring Framework 4.3.29
Lösung 2 überprüft, ob der Suffix einer URL, die ;jsessionid= enthält, ein sicherer Suffix ist. Ist dies nicht der Fall, wird Content-Disposition=inline;filename=f.txt gesetzt, um den Inhalt der Antwort zwangsweise in eine Datei namens f.txt herunterzuladen. (Die Vorgehensweise entspricht dem RFD-Abwehrmechanismus von 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;
}
}