0


漏洞分析 | Spring Framework路径遍历漏洞(CVE-2024-38816)

漏洞概述

VMware Spring Framework是美国威睿(VMware)公司的一套开源的Java、JavaEE应用程序框架。该框架可帮助开发人员构建高质量的应用。

近期,网宿安全演武实验室监测到Spring Framework在特定条件下,存在目录遍历漏洞(网宿评分:高危、CVSS 3.1 评分:7.5):

当同时满足使用 RouterFunctions 和 FileSystemResource 来处理和提供静态文件时,攻击者可构造恶意请求遍历读取系统上的文件。

目前该漏洞POC状态已在互联网公开,建议客户尽快做好自查及防护。

受影响版本

Spring Framework 5.3.0 - 5.3.39

Spring Framework 6.0.0 - 6.0.23

Spring Framework 6.1.0 - 6.1.12

其他更旧或者官方已不支持的版本

漏洞分析

根据漏洞描述(https://spring.io/security/cve-2024-38816)可知,关键变更在于如何处理静态资源路径。

https://github.com/spring-projects/spring-framework/commit/d86bf8b2056429edf5494456cffcb2b243331c49#diff-25869a3e3b3d4960cb59b02235d71d192fdc4e02ef81530dd6a660802d4f8707R4

这里改了两处,分别是:

webflux --> org.springframework.web.reactive.function.server.PathResourceLookupFunction

webmvc --> org.springframework.web.servlet.function.PathResourceLookupFunction

它们都旨在为 Web 应用程序提供静态内容的访问。

以webmvc --> org.springframework.web.servlet.function.PathResourceLookupFunction为例,展开分析。

先是动态处理了资源请求,确保只返回有效并且可访问的资源。

  1. @Override
  2. public Optional<Resource> apply(ServerRequest request) {
  3. PathContainer pathContainer = request.requestPath().pathWithinApplication();
  4. if (!this.pattern.matches(pathContainer)) {
  5. return Optional.empty();
  6. }
  7. pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
  8. String path = processPath(pathContainer.value());
  9. if (path.contains("%")) {
  10. path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
  11. }
  12. if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
  13. return Optional.empty();
  14. }
  15. try {
  16. Resource resource = this.location.createRelative(path);
  17. if (resource.isReadable() && isResourceUnderLocation(resource)) {
  18. return Optional.of(resource);
  19. }
  20. else {
  21. return Optional.empty();
  22. }
  23. }
  24. catch (IOException ex) {
  25. throw new UncheckedIOException(ex);
  26. }
  27. }

接着对路径字符串进行规范化处理,确保返回的路径格式是有效的。

  1. private String processPath(String path) {
  2. boolean slash = false;
  3. for (int i = 0; i < path.length(); i++) {
  4. if (path.charAt(i) == '/') {
  5. slash = true;
  6. }
  7. else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
  8. if (i == 0 || (i == 1 && slash)) {
  9. return path;
  10. }
  11. path = slash ? "/" + path.substring(i) : path.substring(i);
  12. return path;
  13. }
  14. }
  15. return (slash ? "/" : "");
  16. }

最后从安全角度,确保路径不指向敏感目录,并且避免出现路径穿越的情况。

  1. private boolean isInvalidPath(String path) {
  2. if (path.contains("WEB-INF") || path.contains("META-INF")) {
  3. return true;
  4. }
  5. if (path.contains(":/")) {
  6. String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
  7. if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
  8. return true;
  9. }
  10. }
  11. return path.contains("..") && StringUtils.cleanPath(path).contains("../");
  12. }

简单阐明以后,不难发现上述代码做了敏感目录检查、url检查、路径穿越检查等操作,暂时没发现可疑点,所以我们需要进一步跟进

org.springframework.web.servlet.function.PathResourceLookupFunction#isInvalidPath()

查看一下它检查相对路径时,StringUtils.cleanPath()做了哪些操作。

  1. public static String cleanPath(String path) {
  2. if (!hasLength(path)) {
  3. return path;
  4. }
  5. String normalizedPath;
  6. // Optimize when there is no backslash
  7. if (path.indexOf('\\') != -1) {
  8. normalizedPath = replace(path, DOUBLE_BACKSLASHES, FOLDER_SEPARATOR);
  9. normalizedPath = replace(normalizedPath, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
  10. }
  11. else {
  12. normalizedPath = path;
  13. }
  14. String pathToUse = normalizedPath;
  15. // Shortcut if there is no work to do
  16. if (pathToUse.indexOf('.') == -1) {
  17. return pathToUse;
  18. }
  19. // Strip prefix from path to analyze, to not treat it as part of the
  20. // first path element. This is necessary to correctly parse paths like
  21. // "file:core/../core/io/Resource.class", where the ".." should just
  22. // strip the first "core" directory while keeping the "file:" prefix.
  23. int prefixIndex = pathToUse.indexOf(':');
  24. String prefix = "";
  25. if (prefixIndex != -1) {
  26. prefix = pathToUse.substring(0, prefixIndex + 1);
  27. if (prefix.contains(FOLDER_SEPARATOR)) {
  28. prefix = "";
  29. }
  30. else {
  31. pathToUse = pathToUse.substring(prefixIndex + 1);
  32. }
  33. }
  34. if (pathToUse.startsWith(FOLDER_SEPARATOR)) {
  35. prefix = prefix + FOLDER_SEPARATOR;
  36. pathToUse = pathToUse.substring(1);
  37. }
  38. String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
  39. // we never require more elements than pathArray and in the common case the same number
  40. Deque<String> pathElements = new ArrayDeque<>(pathArray.length);
  41. int tops = 0;
  42. for (int i = pathArray.length - 1; i >= 0; i--) {
  43. String element = pathArray[i];
  44. if (CURRENT_PATH.equals(element)) {
  45. // Points to current directory - drop it.
  46. }
  47. else if (TOP_PATH.equals(element)) {
  48. // Registering top path found.
  49. tops++;
  50. }
  51. else {
  52. if (tops > 0) {
  53. // Merging path element with element corresponding to top path.
  54. tops--;
  55. }
  56. else {
  57. // Normal path element found.
  58. pathElements.addFirst(element);
  59. }
  60. }
  61. }
  62. // All path elements stayed the same - shortcut
  63. if (pathArray.length == pathElements.size()) {
  64. return normalizedPath;
  65. }
  66. // Remaining top paths need to be retained.
  67. for (int i = 0; i < tops; i++) {
  68. pathElements.addFirst(TOP_PATH);
  69. }
  70. // If nothing else left, at least explicitly point to current path.
  71. if (pathElements.size() == 1 && pathElements.getLast().isEmpty() && !prefix.endsWith(FOLDER_SEPARATOR)) {
  72. pathElements.addFirst(CURRENT_PATH);
  73. }
  74. final String joined = collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
  75. // avoid string concatenation with empty prefix
  76. return prefix.isEmpty() ? joined : prefix + joined;
  77. }

这个方法主要对用户输入路径做了规范化处理,具体包括长度检查、不同操作系统下的路径分隔符处理等。看起来也做了严格的处理,但这一步存在问题。

  1. String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);

具体来说,它是允许空元素存在的,假设路径字符串形如:

String pathToUse = "/static///../../Windows/win.ini";

那么调用 delimitedListToStringArray 方法以后,pathArray即为

["static", "", "", "..", "..", "Windows", "win.ini"]

而pathElements即为

再来看这一串:String pathToUse = "/static/../../Windows/win.ini";

显然,pathArray中存在空元素会影响上级目录的处理,导致返回不同的结果,即存在安全隐患。

漏洞复现

实现目录穿越需要用到"../",结合上述分析,可通过这种方式实现。

package org.example.demo;

import org.springframework.util.ResourceUtils;

import org.springframework.util.StringUtils;

public class test {

public static void main(String[] args) {

String path = "/static///../../Windows/win.ini";

System.out.println(isInvalidPath(path));

}

private static boolean isInvalidPath(String path) {

if (path.contains("WEB-INF") || path.contains("META-INF")) {

return true;

}

if (path.contains(":/")) {

String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);

if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {

return true;

}

}

return path.contains("..") && StringUtils.cleanPath(path).contains("../");

}

}

但还需要结合上下文,继续构造payload。首先路径以斜杠开头时,StringUtils.cleanPath()方法会去掉路径的第⼀个斜杠。

  1. if (pathToUse.startsWith(FOLDER_SEPARATOR)) {
  2. prefix = prefix + FOLDER_SEPARATOR;
  3. pathToUse = pathToUse.substring(1);
  4. }

那就需要多写一条"/",构造"///../"跳一级目录。

而在最初的org.springframework.web.servlet.function.PathResourceLookupFunction#apply()中,对路径做了规范化处理,即去掉连续的"/"

  1. pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
  2. String path = processPath(pathContainer.value());

所以需要将多余的"/"变为"",再借助StringUtils.cleanPath()方法重新转换回来。

  1. normalizedPath = replace(normalizedPath, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);

修复方案

目前官方已有可更新版本,建议受影响用户升级至最新版本:

https://github.com/spring-projects/spring-framework/tags

产品支持

网宿全站防护-WAF已支持对该漏洞利用攻击的防护,并持续挖掘分析其他变种攻击方式和各类组件漏洞,第一时间上线防护规则,缩短防护“空窗期”。


本文转载自: https://blog.csdn.net/WangsuSecurity/article/details/143590051
版权归原作者 网宿安全演武实验室 所有, 如有侵权,请联系我们删除。

“漏洞分析 | Spring Framework路径遍历漏洞(CVE-2024-38816)”的评论:

还没有评论