CVE-2024-21633 (Java, published 2024-01-03T17:15Z)
Description: value: Apktool is a tool for reverse engineering Android APK files. In versions 2.9.1 and prior, Apktool infers resource files' output path according to their resource names which can be manipulated by attacker to place files at desired location on the system Apktool runs on. Affected environments are those in which an attacker may write/overwrite any file that user has write access, and either user name is known or cwd is under user folder. Commit d348c43b24a9de350ff6e5bd610545a10c1fc712 contains a patch for this issue.
Severity: nan (CVSS v3 base score: 7.8)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Repository: https://github.com/iBotPeaches/Apktool
Files changed:
brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkBuilder.java
brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java
brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
brut.apktool/apktool-lib/src/test/java/brut/androlib/decode/ResourceDirectoryTraversalTest.java
brut.apktool/apktool-lib/src/test/java/brut/androlib/util/UnknownDirectoryTraversalTest.java
brut.j.dir/src/main/java/brut/directory/DirUtil.java
brut.j.dir/src/main/java/brut/directory/FileDirectory.java
brut.j.dir/src/main/java/brut/directory/ZipUtils.java
brut.j.util/src/main/java/brut/util/BrutIO.java
Commit message: Prevent arbitrary file writes with malicious resource names. (#3484)

* refactor: rename sanitize function

* fix: expose getDir

* fix: safe handling of untrusted resource names

 - fixes: GHSA-2hqv-2xv4-5h5w

* test: sample file for GHSA-2hqv-2xv4-5h5w

* refactor: avoid detection of absolute files for resource check

* chore: enable info mode on gradle

* test: skip test on windows

* chore: debug windows handling

* fix: normalize entry with file separators

* fix: normalize filepath after cleansing

* chore: Android paths are not OS specific

* refactor: use java.nio for path traversal checking

* chore: align path separator on Windows for Zip files

* chore: rework towards basic directory traversal

* chore: remove '--info' on build.yml

Vulnerable code:
                inputFile = new File(unknownFileDir, BrutIO.sanitizeUnknownFile(unknownFileDir, unknownFileInfo.getKey()));
        Directory in, out;
            out = out.createDir("res");
                fileDecoder.decode(res, in, out, mResFileMapping);
                generateValuesFile(valuesFile, out, xmlSerializer);
            generatePublicXml(pkg, out, xmlSerializer);
        String outResName = res.getFilePath();
        String validFilename = BrutIO.sanitizeUnknownFile(sTmpDir, "file");
        BrutIO.sanitizeUnknownFile(sTmpDir, "../file");
        BrutIO.sanitizeUnknownFile(sTmpDir, rootLocation + "file");
        BrutIO.sanitizeUnknownFile(sTmpDir, "");
        BrutIO.sanitizeUnknownFile(sTmpDir, invalidPath);
        String validFilename = BrutIO.sanitizeUnknownFile(sTmpDir, "dir" + File.separator + "file");
                String cleanedFilename = BrutIO.sanitizeUnknownFile(out, fileName);
    private File getDir() {
                final String cleanedPath = BrutIO.sanitizeUnknownFile(folder, file.getPath().substring(prefixLength));
                final ZipEntry zipEntry = new ZipEntry(BrutIO.normalizePath(cleanedPath));
    public static String sanitizeUnknownFile(final File directory, final String entry) throws IOException, BrutException {
        if (entry.length() == 0) {
    public static String normalizePath(String path) {

Fixed code:
                inputFile = new File(unknownFileDir, BrutIO.sanitizeFilepath(unknownFileDir, unknownFileInfo.getKey()));
        Directory in, out, outRes;
            outRes = out.createDir("res");
                fileDecoder.decode(res, in, outRes, mResFileMapping);
                generateValuesFile(valuesFile, outRes, xmlSerializer);
            generatePublicXml(pkg, outRes, xmlSerializer);
import brut.util.BrutIO;
        String outResName = res.getFilePath();

        if (BrutIO.detectPossibleDirectoryTraversal(outResName)) {
            outResName = inFileName;
            LOGGER.warning(String.format(
                "Potentially malicious file path: %s, using instead %s", res.getFilePath(), outResName
            ));
        }
/*
 *  Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
 *  Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package brut.androlib.decode;

import brut.androlib.ApkDecoder;
import brut.androlib.BaseTest;
import brut.androlib.Config;
import brut.androlib.TestUtils;
import brut.common.BrutException;
import brut.directory.ExtFile;
import brut.util.OS;
import brut.util.OSDetection;
import org.junit.AfterClass;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.File;
import java.io.IOException;

import static org.junit.Assert.assertTrue;

public class ResourceDirectoryTraversalTest extends BaseTest {

    @BeforeClass
    public static void beforeClass() throws Exception {
        TestUtils.cleanFrameworkFile();
        sTmpDir = new ExtFile(OS.createTempDirectory());
        TestUtils.copyResourceDir(ResourceDirectoryTraversalTest.class, "decode/arbitrary-write/", sTmpDir);
        Assume.assumeFalse(OSDetection.isWindows());
    }

    @AfterClass
    public static void afterClass() throws BrutException {
        OS.rmdir(sTmpDir);
    }

    @Test
    public void checkIfMaliciousRawFileIsDisassembledProperly() throws BrutException, IOException {
        String apk = "GHSA-2hqv-2xv4-5h5w.apk";

        Config config = Config.getDefaultConfig();
        config.forceDelete = true;
        ApkDecoder apkDecoder = new ApkDecoder(config, new File(sTmpDir + File.separator + apk));
        File outDir = new File(sTmpDir + File.separator + apk + ".out");
        apkDecoder.decode(outDir);

        File pocTestFile =  new File(outDir,"res/raw/poc");
        assertTrue(pocTestFile.exists());
    }
}
        String validFilename = BrutIO.sanitizeFilepath(sTmpDir, "file");
        BrutIO.sanitizeFilepath(sTmpDir, "../file");
        BrutIO.sanitizeFilepath(sTmpDir, rootLocation + "file");
        BrutIO.sanitizeFilepath(sTmpDir, "");
        BrutIO.sanitizeFilepath(sTmpDir, invalidPath);
        String validFilename = BrutIO.sanitizeFilepath(sTmpDir, "dir" + File.separator + "file");
                String cleanedFilename = BrutIO.sanitizeFilepath(out, fileName);
    public File getDir() {
                final String cleanedPath = BrutIO.sanitizeFilepath(folder, file.getPath().substring(prefixLength));
                final ZipEntry zipEntry = new ZipEntry(BrutIO.adaptSeparatorToUnix(cleanedPath));
    public static String sanitizeFilepath(final File directory, final String entry) throws IOException, BrutException {
        if (entry.isEmpty()) {
    public static boolean detectPossibleDirectoryTraversal(String entry) {
        if (OSDetection.isWindows()) {
            return entry.contains("..\\") || entry.contains("\\..");
        }
        return entry.contains("../") || entry.contains("/..");
    }

    public static String adaptSeparatorToUnix(String path) {