Skip to content

Command injection via ZIP directory name in Android custom build setup (shell=True + string concatenation) #5477

Description

@sil3ntwizard

Summary

ClusterFuzz builds a shell command by string-concatenating an extracted ZIP
path into a subprocess.run(..., shell=True) call. A ZIP whose directory name
contains $() causes Bash to execute the injected command on the trusted host
process
before the fuzz target ever runs, bypassing the isolation boundary
between the untrusted runner and the host.

Affected versions

  • v2.36.2, v2.37.0, v2.37.1 (tested at revision 7758497)
  • Unpatched as of Aug 23, 2026

Root cause

In adb.py, get_package_name() constructs the aapt command by concatenation:

# Vulnerable pattern
aapt_command = '%s dump badging %s' % (aapt_path, apk_path)
subprocess.run(aapt_command, executable='/bin/bash', shell=True)

apk_path comes from an extracted ZIP. If the ZIP contains a directory named
payload$(touch /tmp/pwned), that name is preserved on disk and lands verbatim
in the shell command:

/resources/aapt dump badging /build/custom/payload$(touch /tmp/pwned)/app.apk

Bash evaluates $(touch /tmp/pwned) on the host while processing the word,
before aapt runs. The single quotes visible in write_command_line_file do not
protect against this because they are inside an outer double-quoted word.

Who can trigger this

Requires privileged ClusterFuzz project access, specifically the role that can
create or update fuzzing jobs and upload custom builds. Standard testcase
uploaders cannot trigger this.

Why the boundary matters

ClusterFuzz's security model isolates fuzz target code in an untrusted runner,
separate from the trusted host process that holds credentials, task metadata,
and cluster state. A project operator is permitted to supply code for the
untrusted runner. This bug lets that same operator run code in the trusted host
process, a boundary the design is supposed to prevent.

Proof of concept

The PoC extracts the exact vulnerable functions from the pinned source and runs
them with a harmless fake aapt. No Android device required.

docker build -f poc/Dockerfile -t clusterfuzz-cmdi-poc poc
docker run --rm -v "$(pwd)/clusterfuzz:/src:ro" clusterfuzz-cmdi-poc

Expected output:

CONTROL_MARKER_CREATED=False ← normal path, no injection
EXPLOIT_MARKER_CREATED=True ← $() in directory name executed on host
APP_PATH_CONTAINS_COMMAND_SUBSTITUTION=True

CONTROL_MARKER_CREATED=False confirms a normal path goes through the same
code without triggering anything. EXPLOIT_MARKER_CREATED=True confirms the
injected directory name causes Bash to execute on the host while package
inspection completes normally.

Fix

Replace string concatenation + shell=True with an argument list + shell=False:

## get_package_name() - safe version
result = subprocess.run(
    [aapt_path, 'dump', 'badging', apk_path],
    shell=False,
    capture_output=True,
    text=True
)

# write_command_line_file() - write via Python file I/O, not shell
with open(command_line_path, 'w') as f:
    f.write(apk_path)

apdk_path becomes a list element, never concatenated into a shell string.
Archive paths should be treated as untrusted regardless of uploader privilege level.

Disclosure timeline

  • Aug 23, 2026 - Reported to Google VRP
  • Aug 24, 2026 - Google confirmed the issue is real but below their security
    escalation threshold; suggested public disclosure on GitHub

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions