APKHunt Tutorial: Static Android APK Security Analysis

Deepak Prasad
Tested on Kali GNU/Linux Rolling 2026.2
Package golang-go 2:1.26~1
jadx 1.5.6-0kali1
dex2jar 2.1~nightly-28-0kali2
apktool 2.7.0 (independent manifest check)
APKHunt commit 386a0d5 (Cyber-Buddy/APKHunt)
Applies to Linux; tested here on Kali Linux. Upstream states the tool was tested on Linux only.
Privilege Normal user for clone and scans; sudo for apt installs
Input InsecureShop v1.0 training APK (archived Optiv release)
Scope Static APK scan with APKHunt plus manual validation of findings. No emulator required. Does not cover MobSF dynamic analysis, Frida, or runtime instrumentation.
Related guides Frida Android tutorial
Android Studio AVD lab
Genymotion Desktop lab
Short on time?Beta Get the gist of this 17-minute read in seconds, then ask follow-up questions

APKHunt is a command-line static analyzer for Android APK files. This tutorial installs it on Kali, scans the intentionally vulnerable InsecureShop APK, and then manually checks which findings are real, noisy, or missed by the scanner.

Static analysis means the tool reads the APK without running it on a phone or emulator. APKHunt is focused on CLI-based static analysis, while MobSF provides a broader web-based static and dynamic workflow. Use APKHunt for a fast first pass; open MobSF when you want a UI or runtime testing. When you move to dynamic work, use either the free Android Studio AVD lab or Genymotion on the host before Frida or MobSF Dynamic Analyzer.

IMPORTANT
Run APKHunt only on APKs you own or have explicit permission to assess. Use intentionally vulnerable training apps in an isolated lab. Document scope in your rules of engagement before you touch client packages.

What APKHunt does and its limitations

APKHunt is a terminal tool for Android APK files. Upstream documents Linux as the tested platform; this article runs it on Kali. You give it an APK path. Behind the scenes it:

  1. Converts the app bytecode with dex2jar
  2. Decompiles readable Java-like sources with jadx
  3. Searches those files and the app manifest for known risky patterns

Those patterns are labeled with older OWASP mobile checklist IDs (MASVS v1.5 / MSTG names such as MSTG-STORAGE-2). Treat those IDs as scanner references, not as proof that the app currently fails MASVS.

Keep these limits in mind before you trust a hit:

  • APKHunt mainly performs pattern-based checks over decompiled files; it does not perform complete interprocedural data-flow analysis.
  • A matching line means "this pattern appeared here," not "I proved an exploit."
  • The tool does not grade findings as Critical / High / Medium. You decide severity after you read the code.
  • There is no web dashboard. Output stays in the terminal (and optional log files next to the APK).
  • APKHunt still runs on current Kali as a lightweight CLI triage tool, but its public rule taxonomy remains based on MASVS v1.5.0 / MSTG-era identifiers. Use it for static leads rather than current MASVS compliance or runtime validation.

Later sections show those limits in practice: noisy library hits, generic advice text, and a wrong "zero exported activities" count.


APKHunt vs MobSF

Choose the tool that matches the job. For the broader static-plus-dynamic workflow, use the MobSF Android pentesting guide.

Dimension APKHunt MobSF
Role Focused CLI static scanning and reporting Broader static and dynamic platform
Interface CLI only Web UI (+ REST API)
Analysis model Decompile + pattern checks on disk Static modules plus Dynamic Analyzer on a device/emulator
Output Terminal + TXT (and a browser-viewable HTML log with -l) Interactive reports, scores, screenshots, runtime data
Best when Fast MASVS v1.5-style SAST, batch folders, file-path evidence Interactive review, Frida, traffic, runtime proof
Not a substitute for Emulator dynamic tests Lightweight one-shot CLI SAST in a script

Start with APKHunt when you only need static leads. Switch to MobSF when you need runtime proof or a richer dashboard.


Install APKHunt on Kali Linux

APKHunt needs three helpers: Go (to run the tool), jadx (to decompile), and dex2jar (to convert DEX to JAR). Install them with apt:

bash
sudo apt-get update && sudo apt-get install -y jadx dex2jar golang-go git

Confirm Go is available:

bash
go version

Sample output:

output
go version go1.26.7 linux/amd64

This lab used Go 1.26.7. If APKHunt fails to build on a different Go release, check the current upstream repository before troubleshooting the scanner itself. Confirm jadx next:

bash
jadx --version

Sample output:

output
1.5.6

jadx is installed and on PATH. Download APKHunt from GitHub into your home directory:

bash
git clone --depth 1 https://github.com/Cyber-Buddy/APKHunt.git "${HOME}/APKHunt"

The clone creates ${HOME}/APKHunt with the scanner source inside. Record the short commit hash so findings stay tied to a known revision:

bash
git -C "${HOME}/APKHunt" rev-parse --short HEAD

Sample output:

output
386a0d5

This article's implementation notes refer to that commit. Show the built-in help. The first run compiles briefly; the entrypoint file is lowercase apkhunt.go:

bash
cd "${HOME}/APKHunt" && go run apkhunt.go -h

Sample output (banner trimmed):

output
    OWASP MASVS Static Analyzer

[+] APKHunt - a comprehensive static code analysis tool for Android apps
[+] Based on: OWASP MASVS - https://mobile-security.gitbook.io/masvs/

    APKHunt Usage:
	  go run APKHunt.go [options] {.apk file}

    Options:
	 -h     For help
	 -p     Provide a single apk file-path
	 -m     Provide the folder-path for multiple apk scanning
	 -l     For logging (.txt file)

    Note:
	 - Tested on linux only!
	 - Keep tools such as jadx, dex2jar, go, grep, etc.! installed

Read three flags from that help: -p for one APK, -m for a folder of APKs, and -l for log files. The built-in help still shows APKHunt.go, but the current repository entrypoint is lowercase apkhunt.go. On a case-sensitive Linux filesystem, use the lowercase filename. Upstream documents Linux as the tested platform.


Scan an Android APK with APKHunt

Use a practice (intentionally vulnerable) APK so findings are expected and safe. This walkthrough uses the archived InsecureShop v1.0 release because that file stays the same every time. Current development lives at hax0rgb/InsecureShop.

Create a lab folder and download the release APK with curl. -fL fails on HTTP errors and follows redirects. The official v1.0 release asset is InsecureShop.apk (capital I and S); save it locally under a simple lowercase path:

bash
mkdir -p "${HOME}/apk-lab" && curl -fL -o "${HOME}/apk-lab/insecureshop.apk" "https://github.com/optiv/InsecureShop/releases/download/v1.0/InsecureShop.apk"

Confirm you have the same training APK this lab used by checking the SHA-256 digest:

bash
sha256sum "${HOME}/apk-lab/insecureshop.apk"

Sample output:

output
a83298ae4a37fcab8101e8b41e513dd2199af71a94ea537d556a318e07d4d1bd  /home/kali/apk-lab/insecureshop.apk

That hash matches the APKHunt scan output later in this article. Start the scan with -p (path) and -l (write logs). Expect several minutes while decompilation runs:

bash
cd "${HOME}/APKHunt" && go run apkhunt.go -p "${HOME}/apk-lab/insecureshop.apk" -l

Trimmed transcript (jadx progress lines omitted):

output
[+] Scan has been started at: 2026-09-09 12:15:57.565815168 -0400 EDT m=+0.000502779
[+] APK Base: insecureshop.apk
[+] APK Size: 4.5341797 MB
[+] APK Directory: /home/kali/apk-lab/
[+] APK Static Analysis Path: /home/kali/apk-lab/insecureshop_SAST/
[+] APK Hash: MD5: c5d872355e43322f1692288e2c4e6f00
[+] APK Hash: SHA256: a83298ae4a37fcab8101e8b41e513dd2199af71a94ea537d556a318e07d4d1bd

[+] d2j-dex2jar has started converting APK to Java JAR file
    dex2jar /home/kali/apk-lab/insecureshop.apk -> /home/kali/apk-lab/insecureshop.jar

[+] Jadx has started decompiling the application
exit status 3
INFO  - loading ...
INFO  - processing ...
ERROR - finished with errors, count: 11

[+] Capturing the data from the AndroidManifest file
...
[+] Scan has been finished at: 2026-09-09 12:24:45.925463564 -0400 EDT m=+528.360151157

[+] Total time taken for hunting: 8m48.359665809s

On this Kali VM the full run took about 8m48s (convert + decompile + rules). Your time will differ with CPU, disk, and APK size.

Notice exit status 3 and "finished with errors, count: 11" from jadx. That means some classes failed to decompile cleanly, but APKHunt still continued. Treat missing findings as inconclusive until you confirm the file you care about exists under insecureshop_SAST.

-l writes log files next to the APK. Upstream help still says TXT only, but this tested run also wrote HTML. The HTML is a simple browser view of the log, not a MobSF-style dashboard. Prefer the TXT file when you search. List what appeared:

bash
ls -lh "${HOME}/apk-lab"/APKHunt_insecureshop_*.txt "${HOME}/apk-lab"/APKHunt_insecureshop_*.html "${HOME}/apk-lab"/insecureshop_SAST

Sample output:

output
-rw-r--r-- 1 kali kali 178K Sep  9 12:24 /home/kali/apk-lab/APKHunt_insecureshop_2026-9-9_12-15-57.html
-rw-r--r-- 1 kali kali 178K Sep  9 12:24 /home/kali/apk-lab/APKHunt_insecureshop_2026-9-9_12-15-57.txt
drwxr-xr-x 4 kali kali 4.0K Sep  9 12:16 /home/kali/apk-lab/insecureshop_SAST

You should see a .txt log, an .html log, and the insecureshop_SAST folder. That folder holds the decompiled sources you will open by hand next.


How to read APKHunt findings

Every finding block in the log looks roughly the same. Learn the pieces once:

Field What it means (plain English)
Category header (==>> …) Which old MASVS v1.5 topic APKHunt is checking
File path + line Where to look in the decompiled tree
Matched snippet The exact text that matched
QuickNote Generic fix advice from the tool (not always a perfect fit)
Reference Old MSTG-style ID plus a CWE number

APKHunt does not prove severity. Copy category, path, rule ID, and one evidence line into your notes. Decide severity only after you open the file.

Save the newest TXT log path in a shell variable so later greps stay short:

bash
REPORT=$(ls -1t "${HOME}/apk-lab"/APKHunt_insecureshop_*.txt | head -1)

Search the log for the cleartext-traffic finding:

bash
grep -A8 'allowing cleartext traffic' "${REPORT}"

Sample output (ANSI color codes omitted):

output
==>>  The app is allowing cleartext traffic...

/home/kali/apk-lab/insecureshop_SAST/resources/AndroidManifest.xml
27:        android:usesCleartextTraffic="true"

[!] QuickNote:
    - It is recommended to set android:usesCleartextTraffic or cleartextTrafficPermitted to false. Please note that, Sensitive information should be sent over secure channels only.
[*] Reference:
    - OWASP MASVS: MSTG-NETWORK-2 | CWE-319: Cleartext Transmission of Sensitive Information

The important evidence line is android:usesCleartextTraffic="true" in the manifest. The QuickNote is generic advice; the Reference ID is the scanner's label.

The same report also flags backup and debug settings (android:allowBackup="true", android:debuggable="true"). Next, find SharedPreferences hits and keep only the app's own package path:

bash
grep -A12 'Shared Preferences related instances' "${REPORT}" | grep -E '==>>|insecureshop/'

Sample output:

output
==>> The Shared Preferences related instances...
/home/kali/apk-lab/insecureshop_SAST/sources/com/insecureshop/util/Prefs.java

Prefs.java under com/insecureshop/ is the useful hit. Ignore AndroidX library paths when you triage. WebView JavaScript findings appear later under platform checks and point into the shop WebView activities.


Manually validate APKHunt findings

Scanner output is a lead list. Open the cited files and decide: real issue, needs a phone test, or noise.

Confirm cleartext, debug, and backup manifest flags

The Android manifest is the app's configuration file. Grep the three flags from the report inside the decompiled copy:

bash
grep -E 'usesCleartextTraffic|allowBackup|debuggable' "${HOME}/apk-lab/insecureshop_SAST/resources/AndroidManifest.xml"

Sample output:

output
        android:debuggable="true"
        android:allowBackup="true"
        android:usesCleartextTraffic="true"

All three flags are really present. Record them as confirmed configuration findings, then judge impact separately:

  • usesCleartextTraffic="true" permits the app to use cleartext network traffic where the platform and network-security configuration allow it; it does not prove that the app actually sends sensitive data over HTTP. On Android 7.0 / API 24 and later, if the app defines android:networkSecurityConfig, inspect that configuration because it determines the effective cleartext policy.
  • debuggable="true" is bad for a store release, but normal on many training builds.
  • allowBackup="true" is only a signal. Whether data can be copied off the device depends on Android version, backup rules, and device policy. Do not infer practical data extraction from the flag alone; validate the app's backup rules and behavior on the target Android version.

Validate password storage and logging separately

Open the SharedPreferences helper APKHunt named:

bash
sed -n '40,105p' "${HOME}/apk-lab/insecureshop_SAST/sources/com/insecureshop/util/Prefs.java"

Sample output (trimmed):

output
            SharedPreferences sharedPreferences = context.getSharedPreferences("Prefs", 0);
...
    public final String getPassword() {
...
        return sharedPreferences.getString("password", "");
...
    public final void setPassword(String value) {
...
        sharedPreferences.edit().putString("password", value).apply();

Finding A: password stored as a plaintext value in SharedPreferences. The key line is putString("password", value). Mode 0 means MODE_PRIVATE (not world-readable). So the scanner's generic "world-readable preferences" QuickNote does not describe this call. MODE_PRIVATE prevents ordinary cross-app reads, but it does not make the stored credential encrypted or safe against backup, rooted-device access, or local forensic access.

Finding B: password written to device logs. That is a different problem. Check the login screen code:

bash
grep -n 'Log.d' "${HOME}/apk-lab/insecureshop_SAST/sources/com/insecureshop/LoginActivity.java"

Sample output:

output
87:        Log.d("userName", username);
88:        Log.d("password", password);

Lines 87–88 write username and password to Logcat. Keep that as a separate finding from SharedPreferences storage. Modern Android restricts access to other apps' Logcat output, so the practical exposure depends on device privileges, debugging context, Android version, and who has access to the device or logging interface.

Trace WebView input to loadUrl

Do not stop at "JavaScript is enabled." Open the WebView activity and see where the URL comes from:

bash
sed -n '55,115p' "${HOME}/apk-lab/insecureshop_SAST/sources/com/insecureshop/WebViewActivity.java"

Sample output (trimmed):

output
        settings.setJavaScriptEnabled(true);
...
        Uri uri = intent.getData();
        if (uri != null) {
...
                    String queryParameter = data2.getQueryParameter("url");
...
            webview.loadUrl(data);

In plain steps, the code does this:

text
  Deep link / Intent arrives (path /web or /webview)
           │
           ▼
  Read query parameter "url" into variable data
           │
           ▼
  webview.loadUrl(data)

WebViewActivity also has a BROWSABLE deep-link filter (insecureshop://com.insecureshop/...), so the activity is externally invokable through that intent filter. Call this a confirmed externally reachable path from a deep link to WebView.loadUrl(). That proves attacker-controlled navigation may reach the WebView code path; it does not by itself prove useful JavaScript execution, file access, or sensitive-session impact. WebView2Activity can load URLs from even more Intent fields. JavaScript-on alone is not enough for a vulnerability claim.

APKHunt false negative: exported activities reported as 0

This is the most important scanner-validation result in this test. Ask the TXT log what APKHunt counted for exported components:

bash
grep -A5 'APK Component Summary' "${REPORT}"

Sample output:

output
==>> APK Component Summary
[+] --------------------------------
    Exported Activities: 0
    Exported Content Providers: 0
    Exported Broadcast Receivers: 0
    Exported Services: 0

APKHunt claims Exported Activities: 0. Check the JADX-produced manifest yourself:

bash
grep -n 'android:exported\|AboutUsActivity\|InsecureShopProvider' "${HOME}/apk-lab/insecureshop_SAST/resources/AndroidManifest.xml"

Sample output:

output
51:            android:name="com.insecureshop.AboutUsActivity"
52:            android:exported="true"/>
86:            android:name="com.insecureshop.contentProvider.InsecureShopProvider"
88:            android:exported="true"

Confirm the same fact with a second parser that does not reuse APKHunt's JADX tree. Install apktool if it is not already on the host:

bash
sudo apt-get install -y apktool

Decode the original APK into a fresh directory:

bash
apktool d -f -o "${HOME}/apk-lab/insecureshop-apktool" "${HOME}/apk-lab/insecureshop.apk"

Search the decoded manifest for AboutUsActivity:

bash
grep -n 'AboutUsActivity' "${HOME}/apk-lab/insecureshop-apktool/AndroidManifest.xml"

Sample output:

output
25:        <activity android:exported="true" android:name="com.insecureshop.AboutUsActivity"/>

apktool also reports android:exported="true" for AboutUsActivity, so the miss is not a JADX-only artifact.

Source AboutUsActivity export
APKHunt component summary Exported Activities: 0
JADX manifest (APKHunt tree) android:exported="true" on a following line
apktool decoded manifest android:exported="true" on the same <activity> line
Conclusion Two independent parsers show the exported activity; APKHunt's summary under-reported it

AboutUsActivity alone proves the activity count is wrong. Other android:exported="true" rows may be providers or different components, so classify each one. Do not treat every true as another missed activity.

Why did APKHunt miss it? In the APKHunt revision tested here (386a0d5), the component-count logic uses a line-oriented grep pipeline: it greps lines containing <activity, then pipes those lines into a second grep for android:exported="true". In the JADX-formatted manifest used by the scanner, the activity name and android:exported attribute often sit on separate lines, so the second grep never sees the attribute on the same line as <activity. That under-count matches this lab. Treat it as an implementation miss on multiline declarations in this revision, not as a claim that every future APKHunt release will fail the same way.

You can reproduce the empty pipeline against the JADX tree:

bash
bash -c "grep -ne '<activity' \"${HOME}/apk-lab/insecureshop_SAST/resources/AndroidManifest.xml\" | grep -e 'android:exported=\"true\"'" || true

No matching lines print. That is the same style of check APKHunt uses before it prints Exported Activities: 0.

After each check, tag the finding in your notes: confirmed, needs runtime, or noise / library.


Common APKHunt false positives, false negatives, and blind spots

Static tools miss context. Watch for these patterns:

  • Library noise. Matches inside AndroidX or Glide are often less interesting than com/insecureshop/ code.
  • Generic QuickNotes. The advice text is a template. Always read the matched line.
  • Under-reporting. Exported components can exist even when the summary says zero.
  • Pattern matching without data flow. "JavaScript enabled" is not proof of WebView injection until you see where the URL comes from.
  • Jadx errors. Partial decompile can hide issues. Missing finding ≠ safe app.
  • Obfuscation. Hardened release builds give weaker static results.
  • Old checklist IDs. Remap MSTG-era labels to current MASVS language in the client report.

For every APKHunt hit you might put in a report, open the class and nearby code in *_SAST. If you cannot name the class and the user impact, it is not ready.


Troubleshooting APKHunt

Symptom Likely cause Fix
go: command not found / jadx: not found Missing packages Install golang-go, jadx, and dex2jar; confirm with go version and jadx --version
APKHunt.go: no such file Filename case Current repository entrypoint is lowercase apkhunt.go
Jadx exit status 3 / finished with errors Partial decompile Confirm *_SAST/sources and resources/AndroidManifest.xml exist; inspect JADX errors before trusting empty categories
Empty or tiny report Wrong -p path, non-APK file, or scan aborted early Confirm ls on the APK; watch for dex2jar/jadx failures at the start of the log
Almost only library hits Broad greps Filter to the app package directory under *_SAST/sources/
"Exported Activities: 0" but you expect exports Tool under-count Grep android:exported yourself; confirm with a second manifest parser on client work
Heavy obfuscation / empty useful sources Release hardening Expect weaker SAST; rely more on MobSF dynamic and manual reverse engineering
Multi-APK folder scan skips files Non-.apk names or nested folders Keep APKs directly in the -m directory
HTML log looks incomplete or messy in a browser Current upstream HTML logging closes the HTML wrapper before the scan body is written Use the TXT log as the primary scanner output for searching and note-taking; validate important findings against the manifest and decompiled source

Summary

APKHunt gives you a Linux command-line static pass over Android APKs. It decompiles with dex2jar and jadx, then pattern-matches older MASVS v1.5 / MSTG-style checks. On this Kali host the lab installed the packages, cloned Cyber-Buddy/APKHunt at commit 386a0d5, and scanned InsecureShop v1.0 with -p and -l in about 8m48s.

The scanner found useful leads: cleartext, backup, and debug manifest flags; plaintext password storage in SharedPreferences; password logging to Logcat; and WebView-related code worth investigating. Manual tracing then confirmed an externally reachable deep-link path into loadUrl. These are confirmed static code and configuration observations; severity and exploit impact still require context and, where relevant, runtime validation.

Manual review also showed the tool can be wrong. The exported-component summary said zero exports, but both the JADX tree and an independent apktool decode show AboutUsActivity as exported. That miss is why every APKHunt engagement still needs a pass through the manifest and sources before anything goes into a client report.

In practice:

  • Use APKHunt for quick CLI static leads
  • Use manual review to validate impact and catch misses
  • Use MobSF when you need dynamic or runtime work

Remap scanner IDs to current MASVS language yourself, and always read the code behind the hit.


References

  • Cyber-Buddy/APKHunt — upstream project, usage, and MASVS positioning
  • OWASP MASVS — current Mobile Application Security Verification Standard
  • OWASP MASTG — mobile application security testing guide
  • Android Developers — <application> element — allowBackup, debuggable, usesCleartextTraffic, and related attributes
  • Android Developers — App backup — backup behavior across platform versions
  • optiv/InsecureShop — archived v1.0 training APK used in this lab
  • hax0rgb/InsecureShop — current InsecureShop development repository
  • jadx — Dex to Java decompiler used by the pipeline
  • apktool — independent APK decode used to confirm exported components

Frequently Asked Questions

Is APKHunt a replacement for MobSF?

No. APKHunt is a focused CLI static scanner with MASVS v1.5-era reporting. MobSF is a broader platform with static and dynamic modules, a web UI, and runtime analysis. Use APKHunt for lightweight SAST; use MobSF when you need interactive or dynamic coverage.

Does a finding with an MSTG ID mean the app fails current MASVS?

Not by itself. APKHunt's taxonomy is based on OWASP MASVS v1.5.0 and older MSTG identifiers. Treat those IDs as scanner references, confirm the issue in source or at runtime, then map the risk to your engagement's current MASVS language.

Why did jadx print errors but APKHunt still finished?

Jadx can exit with a non-zero status and still write a partial *_SAST tree. APKHunt continues grepping whatever decompiled files exist. Confirm that resources/AndroidManifest.xml and the app package under sources/ are present, and treat missing findings as inconclusive when decompilation was incomplete.

Does APKHunt assign severity to findings?

No. It prints categories, evidence paths, QuickNotes, and MSTG-era references. Assign severity only after you validate the hit and understand impact for the target app and Android version.

Why does APKHunt report 0 exported activities when the APK contains exported components?

In the APKHunt revision tested for this article (386a0d5), the grep-based component detection can miss multiline manifest declarations where <activity and android:exported="true" appear on separate lines. In the InsecureShop test it printed Exported Activities: 0, while both the JADX tree and an independent apktool decode showed AboutUsActivity with android:exported="true". Treat scanner summaries as leads and verify the manifest independently.

Should every SharedPreferences match go into the report?

No. Prefer matches under the application package that touch credentials or PII, then read the mode and call sites. Library noise and generic QuickNotes are common; confirm sensitivity before you assign severity. Treat Logcat leaks of the same secret as a separate finding.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Kennedy Muthii

Information Security Analyst

Accomplished professional proficient in Python, ethical hacking, Linux, cybersecurity, and OSINT. With a track record including winning a national cybersecurity contest, launching a startup in Kenya, and holding a degree in information science, he is currently engaged in cutting-edge research in ethical hacking.

  • Python (programming language)
  • Certified Ethical Hacker
  • White Hat (Computer Security)
  • Linux
  • Penetration Testing