Skip to content

Arbitrary file write on Decoding

High
iBotPeaches published GHSA-2hqv-2xv4-5h5w Jan 3, 2024

Package

No package listed

Affected versions

<=2.9.1

Patched versions

2.9.2

Description

Summary

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

Details

  • Apktool infers resource files' output path according to their resource names ([output-dir]/res/[type]/[resource-name]+[ext of (resource-file)] )

  • E.g. a resource named "foo" with path of "res/raw/bar", is extracted to res/raw/foo

  • But resource name is never sanitized, therefore altering the resource name from "foo" to "../../../../../../../../../../../../tmp/poc" will end up placing "res/raw/bar" file to /tmp/poc in linux systems, but vulnerability exists in windows as well, didn't check for macOS but it seems generic

PoC

  • Generate Payload

apk

  • Run

apk

Impact

  • Attacker may write/overwrite any file that user has write access (e.g. under certain conditions* shell init files or authorized keys can be overwritten)
    *Either user name is known or cwd is under user folder (note that an apk may contain 0xFFFF (65535) raw resources, this allows attacker to brute-force his target file)

Vulnerable code

source-code

Patch

  • I suggest use of original path as destination path while extracting resources
From 50f90be4de46781e2015eac671f1fa8355bb8b87 Mon Sep 17 00:00:00 2001
From: 0x33c0unt <26827438+0x33c0unt@users.noreply.github.com>
Date: Sun, 17 Dec 2023 22:26:32 +0100
Subject: [PATCH] Extract resource files according to their original path
 inside of the APK

---
 .../brut/androlib/res/ResourcesDecoder.java   |  8 ++--
 .../androlib/res/decoder/ResFileDecoder.java  | 47 +++++--------------
 2 files changed, 17 insertions(+), 38 deletions(-)

diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java
index a9507b6b..976e5885 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java
@@ -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);
         }
@@ -174,9 +174,9 @@ public class ResourcesDecoder {
 
             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();
diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
index 9bab7c98..d4956f6d 100644
--- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
+++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java
@@ -42,92 +42,71 @@ public class ResFileDecoder {
             throws AndrolibException {
 
         ResFileValue fileValue = (ResFileValue) res.getValue();
-        String inFilePath = fileValue.toString();
+        String filePath = fileValue.toString();
         String inFileName = fileValue.getStrippedPath();
-        String outResName = res.getFilePath();
         String typeName = res.getResSpec().getType().getName();
-
         String ext = null;
-        String outFileName;
         int extPos = inFileName.lastIndexOf(".");
-        if (extPos == -1) {
-            outFileName = outResName;
-        } else {
+        if (extPos != -1) {
             ext = inFileName.substring(extPos).toLowerCase();
-            outFileName = outResName + ext;
-        }
-
-        String outFilePath = "res/" + outFileName;
-        if (!inFilePath.equals(outFilePath)) {
-            resFileMapping.put(inFilePath, outFilePath);
         }
-
-        LOGGER.fine("Decoding file " + inFilePath + " to " + outFilePath);
+        LOGGER.fine("Decoding file " + filePath);
 
         try {
             if (typeName.equals("raw")) {
-                decode(inDir, inFilePath, outDir, outFileName, "raw");
+                decode(inDir, filePath, outDir, filePath, "raw");
                 return;
             }
             if (typeName.equals("font") && !".xml".equals(ext)) {
-                decode(inDir, inFilePath, outDir, outFileName, "raw");
+                decode(inDir, filePath, outDir, filePath, "raw");
                 return;
             }
             if (typeName.equals("drawable") || typeName.equals("mipmap")) {
                 if (inFileName.toLowerCase().endsWith(".9" + ext)) {
-                    outFileName = outResName + ".9" + ext;
-
-                    // check for htc .r.9.png
-                    if (inFileName.toLowerCase().endsWith(".r.9" + ext)) {
-                        outFileName = outResName + ".r.9" + ext;
-                    }
-
                     // check for raw 9patch images
                     for (String extension : RAW_9PATCH_IMAGE_EXTENSIONS) {
                         if (inFileName.toLowerCase().endsWith("." + extension)) {
-                            copyRaw(inDir, outDir, inFilePath, outFileName);
+                            copyRaw(inDir, outDir, filePath, filePath);
                             return;
                         }
                     }
-
                     // check for xml 9 patches which are just xml files
                     if (inFileName.toLowerCase().endsWith(".xml")) {
-                        decode(inDir, inFilePath, outDir, outFileName, "xml");
+                        decode(inDir, filePath, outDir, filePath, "xml");
                         return;
                     }
 
                     try {
-                        decode(inDir, inFilePath, outDir, outFileName, "9patch");
+                        decode(inDir, filePath, outDir, filePath, "9patch");
                         return;
                     } catch (CantFind9PatchChunkException ex) {
                         LOGGER.log(Level.WARNING, String.format(
                             "Cant find 9patch chunk in file: \"%s\". Renaming it to *.png.", inFileName
                         ), ex);
-                        outDir.removeFile(outFileName);
-                        outFileName = outResName + ext;
+                        outDir.removeFile(filePath);
                     }
                 }
 
                 // check for raw image
                 for (String extension : RAW_IMAGE_EXTENSIONS) {
                     if (inFileName.toLowerCase().endsWith("." + extension)) {
-                        copyRaw(inDir, outDir, inFilePath, outFileName);
+                        copyRaw(inDir, outDir, filePath, filePath);
                         return;
                     }
                 }
 
                 if (!".xml".equals(ext)) {
-                    decode(inDir, inFilePath, outDir, outFileName, "raw");
+                    decode(inDir, filePath, outDir, filePath, "raw");
                     return;
                 }
             }
 
-            decode(inDir, inFilePath, outDir, outFileName, "xml");
+            decode(inDir, filePath, outDir, filePath, "xml");
         } catch (RawXmlEncounteredException ex) {
             // If we got an error to decode XML, lets assume the file is in raw format.
             // This is a large assumption, that might increase runtime, but will save us for situations where
             // XSD files are AXML`d on aapt1, but left in plaintext in aapt2.
-            decode(inDir, inFilePath, outDir, outFileName, "raw");
+            decode(inDir, filePath, outDir, filePath, "raw");
         } catch (AndrolibException ex) {
             LOGGER.log(Level.SEVERE, String.format(
                 "Could not decode file, replacing by FALSE value: %s",
-- 
2.39.3 (Apple Git-145)

Credits

  • Denuvo, although it is not part of my job to dig for vulnerabilities, this vulnerability was spot by coincidence during my shift.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CVE ID

CVE-2024-21633

Weaknesses

Credits