[하루한줄] CVE-2024-22263: Spring Cloud Dataflow의 Path Traversal으로 인한 임의 파일 쓰기 취약점

URL

https://blog.securelayer7.net/spring-cloud-data-flow-exploit/

Target

  • Spring Cloud Dataflow < 2.11.3

Explain

Cloud Foundry/Kubernetes에서 스트리밍 및 데이터 처리 파이프라인 구축을 위한 마이크로서비스 기반 툴킷인 Spring Cloud Dataflow 에서 임의 파일 쓰기 취약점이 발견되어 세부 정보가 공개되었습니다.

취약점은 패키지 업로드 요청을 처리하는 Skipper 서버 구성 요소인 **PackageService.java 모듈의 메서드에 존재합니다.

Skipper는 여러 클라우드 플랫폼에서 Spring Boot 애플리케이션의 라이프사이클을 관리할 수 있는 툴임.

/api/package/upload 엔드포인트의 Skipper 서버 API를 통해 임의의 경로를 대상으로 하는 업로드 요청을 보낼 수 있습니다.

@Transactional

public PackageMetadata upload(UploadRequest uploadRequest) {

    validateUploadRequest(uploadRequest);

upload 메서드는 공격자의 요청에 포함된 UploadRequestvalidateUploadRequest을 호출해 검증합니다.

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...
}

validateUploadRequest 메서드는 패키지 파일이 비어있는지 확인하는 null 검사를 수행합니다.

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

validation 이후 upload 메서드에서는 공격자의 입력을 사용해 packgeFile 경로를 구성해 해당 경로에 패키지파일을 구성합니다.

위 과정에서 다음과 같은 검증 부족이 존재합니다.

  1. validateUploadRequest(uploadReqeust) 검증 과정에서, uploadRequest의 실제 경로가 파일시스템에 생성되기 전에 검증을 진행하므로 실제 파일 경로를 검증할 수 없음
  2. validateUploadRequest 메서드는 null 패키지 검사 외 path traversal check와 같은 검증 로직이 존재하지 않음
  3. Path packageFile 구성에서 유저 인풋인 uploadRequest에 대한 검증 없이 구성함.

공격자는 name 필드에 path traversal 페이로드를 포함하는 uploadRequest 요청을 보내 임의의 경로에 파일을 쓸 수 있습니다.

uploadRequest = {
    "repoName": repoName,
    "name": "../../poc",
    "version": version,
    "extension": "zip",
    "packageFileAsBytes": packageFileAsBytes
}

취약점의 패치는 다음과 같이 이루어졌습니다.

  1. validateUploadRequest(uploadReqeust) 호출 전 업로드 프로세스 중 사용될 파일 경로를 생성함.

    @Transactional
    
    public PackageMetadata upload(UploadRequest uploadRequest) {
    
        Path packageDirPath = TempFileUtils.createTempDirectory("skipperUpload");
    
        validateUploadRequest(packageDirPath, uploadRequest);
  2. validateUploadRequest 메서드에서는 실제 파일 경로를 확인하고, canonicalDestinationFilepath가 디렉토리의 표준 경로인 canonicalDestinationDirPath 로 시작하는지 확인하는 것으로 ../../ 와 같은 path traversal 페이로드를 필터링함.

    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());
        }
    }
  3. 요청 경로를 trim()을 통해 sanitization하는 것으로 기타 파일 경로 조작 등을 방지함.

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