CVE-2024-21633 (published 2024-01-03T17:15Z)

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.

CVSS v3 base score: 7.8, HIGH (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: 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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/iBotPeaches/Apktool
commit_details: repo_url: https://github.com/iBotPeaches/Apktool
author: Connor Tumbleson
author_date: 2024-01-02 06:11:03-05:00
msg: 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
repository: repo_url: https://github.com/iBotPeaches/Apktool
repo_name: iBotPeaches/Apktool
description: A tool for reverse engineering Android apk files
repo_language: Java
owner: iBotPeaches
file_changes: filename: ApkBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -480,7 +480,7 @@ public class ApkBuilder {
             File inputFile;
 
             try {
-                inputFile = new File(unknownFileDir, BrutIO.sanitizeUnknownFile(unknownFileDir, unknownFileInfo.getKey()));
+                inputFile = new File(unknownFileDir, BrutIO.sanitizeFilepath(unknownFileDir, unknownFileInfo.getKey()));
             } catch (RootUnknownFileException | InvalidUnknownFileException | TraversalUnknownFileException exception) {
                 LOGGER.warning(String.format("Skipping file %s (%s)", unknownFileInfo.getKey(), exception.getMessage()));
                 continue;

programming_language: Java
filename: ResourcesDecoder.java
change_type: ModificationType.MODIFY
diff: @@ -154,12 +154,12 @@ public class ResourcesDecoder {
         decoders.setDecoder("xml", new XmlPullStreamDecoder(axmlParser, getResXmlSerializer()));
 
         ResFileDecoder fileDecoder = new ResFileDecoder(decoders);
-        Directory in, out;
+        Directory in, out, outRes;
 
         try {
             out = new FileDirectory(outDir);
             in = mApkInfo.getApkFile().getDirectory();
-            out = out.createDir("res");
+            outRes = out.createDir("res");
         } catch (DirectoryException ex) {
             throw new AndrolibException(ex);
         }
@@ -169,14 +169,14 @@ public class ResourcesDecoder {
 
             LOGGER.info("Decoding file-resources...");
             for (ResResource res : pkg.listFiles()) {
-                fileDecoder.decode(res, in, out, mResFileMapping);
+                fileDecoder.decode(res, in, outRes, mResFileMapping);
             }
 
             LOGGER.info("Decoding values */* XMLs...");
             for (ResValuesFile valuesFile : pkg.listValuesFiles()) {
-                generateValuesFile(valuesFile, out, xmlSerializer);
+                generateValuesFile(valuesFile, outRes, xmlSerializer);
             }
-            generatePublicXml(pkg, out, xmlSerializer);
+            generatePublicXml(pkg, outRes, xmlSerializer);
         }
 
         AndrolibException decodeError = axmlParser.getFirstError();

programming_language: Java
filename: ResFileDecoder.java
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@ import brut.androlib.res.data.value.ResFileValue;
 import brut.directory.DirUtil;
 import brut.directory.Directory;
 import brut.directory.DirectoryException;
+import brut.util.BrutIO;
 
 import java.io.*;
 import java.util.Map;
@@ -44,8 +45,15 @@ public class ResFileDecoder {
         ResFileValue fileValue = (ResFileValue) res.getValue();
         String inFilePath = fileValue.toString();
         String inFileName = fileValue.getStrippedPath();
-        String outResName = res.getFilePath();
         String typeName = res.getResSpec().getType().getName();
+        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
+            ));
+        }
 
         String ext = null;
         String outFileName;

programming_language: Java
filename: ResourceDirectoryTraversalTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,65 @@
+/*
+ *  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());
+    }
+}

programming_language: Java
filename: UnknownDirectoryTraversalTest.java
change_type: ModificationType.MODIFY
diff: @@ -51,7 +51,7 @@ public class UnknownDirectoryTraversalTest extends BaseTest {
 
     @Test
     public void validFileTest() throws IOException, BrutException {
-        String validFilename = BrutIO.sanitizeUnknownFile(sTmpDir, "file");
+        String validFilename = BrutIO.sanitizeFilepath(sTmpDir, "file");
         assertEquals(validFilename, "file");
 
         File validFile = new File(sTmpDir, validFilename);
@@ -60,18 +60,18 @@ public class UnknownDirectoryTraversalTest extends BaseTest {
 
     @Test(expected = TraversalUnknownFileException.class)
     public void invalidBackwardFileTest() throws IOException, BrutException {
-        BrutIO.sanitizeUnknownFile(sTmpDir, "../file");
+        BrutIO.sanitizeFilepath(sTmpDir, "../file");
     }
 
     @Test(expected = RootUnknownFileException.class)
     public void invalidRootFileTest() throws IOException, BrutException {
         String rootLocation = OSDetection.isWindows() ? "C:/" : File.separator;
-        BrutIO.sanitizeUnknownFile(sTmpDir, rootLocation + "file");
+        BrutIO.sanitizeFilepath(sTmpDir, rootLocation + "file");
     }
 
     @Test(expected = InvalidUnknownFileException.class)
     public void noFilePassedTest() throws IOException, BrutException {
-        BrutIO.sanitizeUnknownFile(sTmpDir, "");
+        BrutIO.sanitizeFilepath(sTmpDir, "");
     }
 
     @Test(expected = TraversalUnknownFileException.class)
@@ -83,12 +83,12 @@ public class UnknownDirectoryTraversalTest extends BaseTest {
             invalidPath = "..\\..\\app.exe";
         }
 
-        BrutIO.sanitizeUnknownFile(sTmpDir, invalidPath);
+        BrutIO.sanitizeFilepath(sTmpDir, invalidPath);
     }
 
     @Test
     public void validDirectoryFileTest() throws IOException, BrutException {
-        String validFilename = BrutIO.sanitizeUnknownFile(sTmpDir, "dir" + File.separator + "file");
+        String validFilename = BrutIO.sanitizeFilepath(sTmpDir, "dir" + File.separator + "file");
         assertEquals("dir" + File.separator + "file", validFilename);
     }
 }

programming_language: Java
filename: DirUtil.java
change_type: ModificationType.MODIFY
diff: @@ -89,7 +89,7 @@ public class DirUtil {
             } else if (!in.containsDir(fileName) && !in.containsFile(fileName)) {
                 // Skip copies of directories/files not found.
             } else {
-                String cleanedFilename = BrutIO.sanitizeUnknownFile(out, fileName);
+                String cleanedFilename = BrutIO.sanitizeFilepath(out, fileName);
                 if (! cleanedFilename.isEmpty()) {
                     File outFile = new File(out, cleanedFilename);
                     //noinspection ResultOfMethodCallIgnored

programming_language: Java
filename: FileDirectory.java
change_type: ModificationType.MODIFY
diff: @@ -119,7 +119,7 @@ public class FileDirectory extends AbstractDirectory {
         }
     }
 
-    private File getDir() {
+    public File getDir() {
         return mDir;
     }
 }

programming_language: Java
filename: ZipUtils.java
change_type: ModificationType.MODIFY
diff: @@ -57,8 +57,8 @@ public class ZipUtils {
             throws BrutException, IOException {
         for (final File file : folder.listFiles()) {
             if (file.isFile()) {
-                final String cleanedPath = BrutIO.sanitizeUnknownFile(folder, file.getPath().substring(prefixLength));
-                final ZipEntry zipEntry = new ZipEntry(BrutIO.normalizePath(cleanedPath));
+                final String cleanedPath = BrutIO.sanitizeFilepath(folder, file.getPath().substring(prefixLength));
+                final ZipEntry zipEntry = new ZipEntry(BrutIO.adaptSeparatorToUnix(cleanedPath));
 
                 // aapt binary by default takes in parameters via -0 arsc to list extensions that shouldn't be
                 // compressed. We will replicate that behavior

programming_language: Java
filename: BrutIO.java
change_type: ModificationType.MODIFY
diff: @@ -74,8 +74,8 @@ public class BrutIO {
         return crc;
     }
 
-    public static String sanitizeUnknownFile(final File directory, final String entry) throws IOException, BrutException {
-        if (entry.length() == 0) {
+    public static String sanitizeFilepath(final File directory, final String entry) throws IOException, BrutException {
+        if (entry.isEmpty()) {
             throw new InvalidUnknownFileException("Invalid Unknown File");
         }
 
@@ -94,7 +94,14 @@ public class BrutIO {
         return canonicalEntryPath.substring(canonicalDirPath.length());
     }
 
-    public static String normalizePath(String path) {
+    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) {
         char separator = File.separatorChar;
 
         if (separator != '/') {

programming_language: Java