CVE-2026-39973: Apktool Path Traversal via Stripped Sanitization Call
A security regression in Apktool 3.0.0–3.0.1 removed BrutIO.sanitizePath(), allowing ../sequences in resources.arsc to escape the output directory and write arbitrary files, enabling RCE.
# When a Popular Android Tool Becomes a Security Risk
Apktool is a program that lets software developers and security researchers take apart Android apps to see how they work. Think of it like opening up a toy to understand the gears inside. In versions 3.0.0 and 3.0.1, researchers discovered a critical flaw that lets malicious apps do something dangerous during this process.
Here's the problem: when Apktool breaks down an app file, it needs to extract files and store them on your computer. The developers recently removed some safety checks that verified where files were being placed. This is like removing the bouncer from a nightclub — suddenly, anyone can go anywhere, even the places they shouldn't.
A hacker can create a specially crafted malicious app that, when someone tries to analyze it with Apktool, writes harmful files anywhere on their computer. Those files could install spyware, steal passwords, or take control of your machine. The attacker doesn't need your permission or for you to actually run the app — just opening it in Apktool is enough.
Who should worry? Primarily security researchers and developers who regularly analyze unknown apps. If you're someone who downloads APK files from untrusted sources and examines them with Apktool, you're at risk. The average person using their phone normally isn't affected.
What you can do: First, if you use Apktool, update to version 3.1.0 or later, which fixes this problem. Second, only analyze APK files from sources you trust. Third, consider using a sandboxed environment — a separate, isolated computer or virtual machine — if you regularly examine unknown apps. This contains any damage if something goes wrong.
Want the full technical analysis? Click "Technical" above.
CVE-2026-39973 is a path traversal vulnerability in Apktool 3.0.0 and 3.0.1 introduced by a single-line regression in commit e10a045 (PR #4041, December 12, 2025). During standard APK decoding (apktool d), resource file output paths derived from the resources.arsc Type String Pool are written to disk without sanitization. An attacker who distributes a crafted APK can write arbitrary files to the analyst's filesystem — including SSH configs, shell startup scripts, and Windows Startup folder binaries — escalating a passive reverse-engineering action to remote code execution.
CVSS 7.1 (HIGH) reflects the user-interaction requirement (analyst must decode the APK) offset against the full OS-level write primitive and reliable RCE escalation path.
Root cause: Commit e10a045 removed the BrutIO.sanitizePath() guard from ResFileDecoder.java, allowing attacker-controlled ../ sequences in resources.arsc Type String Pool entries to escape the configured output directory during apktool d.
Affected Component
The vulnerable code lives in brut/androlib/res/decoder/ResFileDecoder.java, specifically in the method responsible for resolving the destination path for each decoded resource file. The Type String Pool in resources.arsc supplies the filename strings — these are attacker-controlled in a crafted APK.
Affected versions: 3.0.0, 3.0.1. Fixed in 3.0.2.
Root Cause Analysis
Prior to e10a045, ResFileDecoder.decode() called BrutIO.sanitizePath() on the resolved output path before writing. The commit removed this call, ostensibly as cleanup. Below is the reconstructed vulnerable logic (Java, matching the structure from the advisory):
// ResFileDecoder.java — versions 3.0.0 and 3.0.1
// Reconstructed from patch diff and advisory source material
public void decode(ResResource res, File outDir, String fileName)
throws AndrolibException {
// fileName originates from resources.arsc Type String Pool
// e.g., "res/drawable/icon.png" — but attacker controls this string
File outFile = new File(outDir, fileName);
// BUG: BrutIO.sanitizePath() call was removed in commit e10a045
// Previously: outFile = new File(outDir, BrutIO.sanitizePath(outDir, fileName));
// Now: no traversal check — "../../../.ssh/authorized_keys" passes through
InputStream in = res.getFileValue().open();
try {
BrutIO.copyAndClose(in, new FileOutputStream(outFile)); // arbitrary write
} catch (IOException ex) {
throw new AndrolibException("Could not decode file: " + fileName, ex);
}
}
The sanitizePath() function that was removed performs canonical path comparison:
// BrutIO.java — sanitizePath() (present in 3.0.2, absent in 3.0.0–3.0.1)
public static String sanitizePath(File outDir, String fileName)
throws BrutException {
// Resolve canonical paths to collapse ../ sequences
String canonicalOutDir = outDir.getCanonicalPath();
String canonicalOutFile = new File(outDir, fileName).getCanonicalPath();
// BUG GUARD: if resolved path doesn't start with the output dir, reject it
if (!canonicalOutFile.startsWith(canonicalOutDir + File.separator)) {
throw new BrutException(
"Refusing path traversal attempt: " + fileName
);
}
return fileName;
}
Without sanitizePath(), a fileName of ../../../home/user/.ssh/authorized_keys resolves outside outDir and FileOutputStream happily creates or truncates the target file with attacker-supplied bytes.
Exploitation Mechanics
Crafting the malicious APK requires injecting a controlled string into the resources.arsc Type String Pool. The pool encodes filenames used to name resource file entries — these map directly to the fileName parameter above.
#!/usr/bin/env python3
# Minimal resources.arsc Type String Pool injection sketch
# Overwrites analyst's ~/.bashrc on apktool d
import struct, zipfile, io
TRAVERSAL_PATH = b"../../../home/analyst/.bashrc\x00"
PAYLOAD = b'\nexport PATH="$HOME/.local/malware:$PATH"\n'
# String pool header (simplified — real format per AOSP chunk spec)
# StringPool_header: type=0x0001, headerSize=0x1C, chunkSize, stringCount ...
STRING_COUNT = 1
STRING_OFFSETS = struct.pack("
EXPLOIT CHAIN:
1. Attacker crafts resources.arsc with Type String Pool entry:
"res/raw/../../../../home/analyst/.bashrc"
pointing to a resource file containing shell payload.
2. Attacker packages this into a valid-looking APK and distributes it
(malware sample, bug bounty submission, CTF challenge, etc.).
3. Analyst runs: apktool d malicious.apk -o ./output
4. ResFileDecoder.decode() is called per resource entry.
fileName = "res/raw/../../../../home/analyst/.bashrc"
outFile = new File("./output", fileName)
→ resolves to /home/analyst/.bashrc (traversal succeeds)
5. BrutIO.copyAndClose() writes attacker payload to /home/analyst/.bashrc
— no exception, no warning, Apktool exits cleanly.
6. On next shell login (or sourced session), .bashrc executes payload.
Alternatively: ~/.ssh/config → redirect connections through attacker proxy
~/.ssh/authorized_keys → persistent SSH backdoor
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\*.bat
→ immediate RCE on next Windows login
Memory Layout
This is a logic/path traversal class vulnerability rather than a memory corruption bug, so there is no heap corruption to diagram. The relevant "layout" is the filesystem write primitive and the path resolution chain:
The fix in 3.0.2 re-introduces the single BrutIO.sanitizePath() call that e10a045 removed. The diff is minimal but decisive:
// BEFORE — ResFileDecoder.java (3.0.0 / 3.0.1, commit e10a045):
public void decode(ResResource res, File outDir, String fileName)
throws AndrolibException {
File outFile = new File(outDir, fileName);
// No path validation — fileName goes straight to FileOutputStream
InputStream in = res.getFileValue().open();
BrutIO.copyAndClose(in, new FileOutputStream(outFile));
}
// AFTER — ResFileDecoder.java (3.0.2, fix commit):
public void decode(ResResource res, File outDir, String fileName)
throws AndrolibException {
// Re-introduced: reject paths that escape outDir via canonical resolution
String sanitized = BrutIO.sanitizePath(outDir, fileName);
File outFile = new File(outDir, sanitized);
InputStream in = res.getFileValue().open();
BrutIO.copyAndClose(in, new FileOutputStream(outFile));
}
// BrutIO.sanitizePath() — throws BrutException on traversal attempt:
public static String sanitizePath(File outDir, String fileName)
throws BrutException {
if (!new File(outDir, fileName)
.getCanonicalPath()
.startsWith(outDir.getCanonicalPath() + File.separator)) {
throw new BrutException("Refusing path traversal: " + fileName);
}
return fileName;
}
One interesting detail: the fix uses getCanonicalPath() which resolves symlinks. This means a symlink inside outDir pointing outside it would also be caught — a subtlety that pure getAbsolutePath() comparison would miss.
Detection and Indicators
Detection is straightforward at the APK layer before execution:
STATIC DETECTION — scan resources.arsc String Pool before decoding:
grep for "../" sequences in raw arsc bytes:
$ python3 -c "
import sys, re
data = open(sys.argv[1],'rb').read()
for m in re.finditer(rb'\.\./', data):
ctx = data[m.start()-16:m.end()+48]
print(f'[!] Traversal candidate @ 0x{m.start():08x}: {ctx}')
" suspicious.apk/resources.arsc
BEHAVIORAL DETECTION:
- Apktool write syscalls targeting paths outside the specified -o directory
- strace: openat() calls with O_WRONLY|O_CREAT to ~/ or system paths
- auditd rule: -w /home -p w -k apktool_traversal
YARA:
rule apktool_path_traversal_arsc {
strings:
$dotdot_unix = { 2E 2E 2F 2E 2E 2F } // ../../
$dotdot_win = { 2E 2E 5C 2E 2E 5C } // ..\..\
condition:
uint32(0) == 0x52455300 // "RES\0" arsc magic
and any of them
}
Remediation
Update immediately to Apktool 3.0.2 or later. The fix is a one-line reintroduction of an existing sanitization call.
If upgrading is not immediately possible: wrap apktool d invocations in a sandboxed environment (Docker with --read-only home mount, or a dedicated throwaway VM) before decoding untrusted APKs.
Analysts handling untrusted samples should treat any APK from an unverified source as potentially weaponized against tooling, not just against end-users.
For CI/CD pipelines running automated APK analysis: pin Apktool to 3.0.2+, validate the jar checksum, and restrict the process's filesystem write scope via seccomp or AppArmor profiles targeting apktool's working directory.
This class of regression — removing a security-critical call during refactoring — is particularly dangerous because it produces no functional difference in benign inputs. The code works correctly on legitimate APKs; only a maliciously crafted resources.arsc exposes the missing guard. Standard regression testing will not catch it. Security-specific test cases covering path traversal inputs in String Pool entries should be part of Apktool's test suite going forward.