Mobix
Provides tools for Android application security testing, including installing apps, bypassing SSL pinning and root detection, capturing live traffic, and analyzing runtime behavior on a rooted device or emulator.
Allows switching the traffic interception proxy to Burp Suite, configuring the listener, and using Repeater to resend and manipulate requests for manual pentesting.
Allows fetching Android application packages (APK/APKS/XAPK) from Google Play links for automated installation and security analysis.
Mobix
An Android pentest lab you can run by hand, or hand off to an AI agent.
It covers the basics you'd expect from a Frida wrapper: SSL-pinning and root-detection bypass, live traffic capture, a web dashboard. What's different is the MCP side. The whole lab is exposed as a 36-tool control plane, so a Claude Code session can run the loop itself: scan an app, watch its traffic, flag likely IDORs, write up findings with evidence attached. The unattended path is locked to a read-only allowlist, so it can't do anything destructive on its own. Install is one line. Starting a scan is one command.
Synthetic demo app/traffic/findings throughout, no real target data. See Screenshots below for the full set, one per tab.
If you find this useful, a star helps other pentesters find it too.
Installing
One line on a fresh machine. It clones this repo to ~/mobile-pentest
(override with MP_ROOT=/some/path), then runs the installer below:
curl -fsSL https://raw.githubusercontent.com/blackfoxxx/Mobix/master/scripts/bootstrap.sh | bashAlready cloned it yourself? Run the installer directly instead:
scripts/install.sh # installs everything this lab depends on
mp doctor # verifies it actually works (not just "present")macOS: fully supported, the only tested target. Installs Homebrew (if missing), all formulae/casks (jadx, apktool, dex2jar, scrcpy, coreutils, fzf, jq, openssl, android-platform-tools, mitmproxy, Docker), pins
frida/frida-tools/objectionto the exact version this repo's bundledbin/frida-server-*-arm64needs, installscryptography(needed by any target-specific request-signer you write; see Re-signing custom request-integrity fields), and symlinksmpontoPATH. It does not install MuMu Player Pro itself, since that isn't distributed via Homebrew. Grab it from the vendor and root it first. Safe to re-run any time; every step is install-if-missing.Linux: partial install only. The portable tooling (adb, frida, jadx, apktool, mitmproxy, Docker) installs via apt/dnf/pacman, but MuMu Player Pro is macOS-only, and so is this lab's automation layer (
mp engage,creds.py front, the LaunchAgent keepalive, all built onosascriptandlaunchctl). On Linux you drive some other adb-reachable rooted device or emulator by hand withadb/frida/objection.mpitself runs, but anything that shells out to MuMu-specific automation will fail.Windows (native): not supported. The script exits with instructions to use WSL2 and take the Linux path from inside it. A real device or emulator is still needed, since MuMu won't run under WSL2 either.
mp doctor (scripts/selftest.sh) checks host tool versions, the frida
CLI-vs-device-server version match (and warns about shadow pipx installs that
could silently take over PATH), cryptography importability on whatever
python3 resolves to, live device root and /system writability, an actual
frida handshake, Docker/MobSF health, and MCP registration. Being present
doesn't mean it's working. Use mp doctor --quick to skip the
device-dependent checks when no emulator is booted.
Related MCP server: Mobile Pentest MCP Server — Extended
Starting a scan
mp scan is the one command to reach for. CLI and dashboard both funnel
through it now, so it's the same flow no matter where you start from:
mp scan # no target chosen -> opens the app picker
mp scan com.target.app # an already-installed app (fuzzy name OK)
mp scan app.apk # or a .apk/.xapk/.apkm/.apks file
mp scan https://host/app.apk # or a direct APK URL
mp scan 'play.google.com/...id=pkg' # or a Play Store linkEvery path converges on the same four numbered steps, always printed the same way:
[1/4] environment emulator up, frida-server running, CA installed
[2/4] target resolved package (installed / fuzzy-matched / installed-then-set)
[3/4] capture FRESH session, scope printed explicitly (or a warning if empty)
[4/4] app launched hooked - root checks blocked, SSL unpinning activeIt always starts clean: force-stops whatever was hooked before, never
silently reuses a stale capture. If the scope is empty it says so loudly
(⚠ no scope set -> capturing ALL hosts) instead of leaving that state
invisible. That ambiguity caused more than one mix-up early on. If a name
matches more than one installed app it lists every match and asks you to be
specific rather than guessing.
mp scan is a thin front door over lower-level building blocks (mp go,
mp switch, mp install, mp launch) that still exist for scripting or
finer control. See mp help, under BUILDING BLOCKS. You don't need them
for normal use.
Note: mp scan used to mean static APK analysis. That's now
mp static [apk]. The old name collided with what people actually meant
when they said "start a scan session," and untangling that collision is most
of what this redesign fixes.
Switching to Burp from the dashboard
The Services tab has a Burp Suite card mirroring mp burp/mp burp --mitm: a live status line (which process actually owns the proxy port,
mitmproxy, Burp, or something else, plus what the device is currently
pointed at), a port field, and Switch to Burp / Switch back to
mitmproxy buttons. Same prerequisite as the CLI: Burp's listener has to be
bound to All interfaces first, or the emulator can't reach it.
Switching to Burp stops whatever capture is currently running on that port as its first step, before it even checks whether Burp is up. Don't click it mid-capture unless you mean to end that session. Switching back to mitmproxy is safe to click anytime; it only updates the injected cert and doesn't touch a running capture.
Web dashboard
mp dash # http://127.0.0.1:8090Nine tabs:
Install: upload an APK/bundle or paste a link/Play Store URL. Runs static analysis and, unless passive, hooks it with a live capture. Log streams on the right.
Traffic: a master/detail inspector with a draggable splitter, plus a
host/path tree on the left to filter by API surface. Click any request to see
its headers, pretty-printed request and response bodies, decoded base64
payloads (requestBody / responseBody are unwrapped automatically), flags
and IDOR candidates. Free-text filter plus a per-app dropdown. You do not need
to open mitmweb for normal inspection.
Repeater: edit method/URL/headers/body and resend directly from the Mac,
no device hop. Auto-detects a macIn-style integrity field and offers to
re-sign it. Keeps the last 25 sends this session.
Login: stored per-app credential fields (text/password/TOTP with a live code). "Send" pushes a value into whatever field is focused in MuMu.
Bypass: a checkbox per module, with descriptions. Six Frida scripts
(proxy override, cert injection, unpinning, unpinning fallback, root detection,
anti-detection) and the nine anti-detection sub-modules (Build spoofing,
SystemProperties, File.exists, Debug, BufferedReader, Runtime.exec,
TelephonyManager, SensorManager). Toggles persist to bypass.json and apply on
the next launch; Relaunch target is right there. Turning a script off drops
its -l flag. Turning an anti-detection module off writes it into
ANTI_DETECT_SKIP in config.js.
Apps: emulator packages with running markers, click to set the target, plus scope entry and Pull APK.
Services: environment and service state, MobSF/Burp controls, and the remaining actions.
Claude Analysis: the live findings list from an mp engage run (interactive
or --auto), with a hero card for the latest finding and a live-activity feed.
See Claude Code engagements.
Sessions: every past capture session and Claude Code engagement, browsable read-only, forever. A new scan never deletes anything. See Sessions & resuming below.
The Emulator apps panel lists packages installed on the Android device
(pm list packages -3), marks which are running, and sets the target on click.
It never shows host/macOS applications; the lab only ever targets the emulator.
Bound to 127.0.0.1 only, deliberately. The dashboard can run mp
commands, so exposing it on a routable interface would hand the whole toolkit to
anyone who can reach it. Actions are a fixed whitelist and package/scope
arguments are regex-validated, but do not put it behind a tunnel or bind it to
0.0.0.0.
Screenshots
All of the below use a synthetic demo app (com.example.bankapp) with fabricated
traffic and findings. None of this is real target data.
Quick start: the mp command
Everything runs through mp (on PATH via /opt/homebrew/bin/mp). Pick a
target app once; every other command then applies to it.
mp apps # fzf picker over installed apps -> sets the target
mp scope api.target.com # optional: limit capture to these hosts
mp go # everything: boot emulator, hook target, start capture
# ... drive the app by hand ...
mp analyze # build the Claude-readable feed from the newest session
mp stop # end hooks + capturemp go needs nothing running beforehand. From a completely stopped
emulator it boots MuMu, waits for Android, starts frida-server, sets the proxy,
repairs the system CA if missing, launches the target with root + SSL bypasses,
and starts a scoped capture. About 40 seconds end to end. mp up does the same
without launching the app.
Optional extras with mp auto mobsf-on (starts MobSF as part of mp up).
mp status shows device, root, frida-server, proxy, CA, target, hooks, capture
and MobSF on one screen. mp help lists everything.
The target and scope persist in ~/mobile-pentest/config, so they survive
reboots and you only choose once per engagement.
Persistence
A LaunchAgent (com.aj.mobilepentest.keepalive) runs bin/mp-keepalive every
60 seconds. The CA lives on /system and survives on its own, but frida-server
and the device proxy setting do not survive an emulator restart. The agent
notices and restores both, logging to loot/keepalive.log. It deliberately does
not relaunch your target app; hooking an app is an explicit act.
To disable: launchctl unload ~/Library/LaunchAgents/com.aj.mobilepentest.keepalive.plist
Device
Serial |
|
Model / OS | SM-S9180, Android 12 (SDK 32) |
ABI |
|
Root |
|
SELinux | Permissive |
| mounted read-write, so system CA certs install directly |
Device IP |
|
Host address from the device: 10.0.2.2. The Mac's LAN IP (192.168.0.184) also
works but changes with the network; 10.0.2.2 does not. Everything is configured
against 10.0.2.2.
Layout
~/mobile-pentest/
├── scripts/ operator scripts (below)
├── frida-scripts/ httptoolkit unpinning suite + config.js (CA + proxy baked in)
├── bin/ mp / mp-dashboard / mp-mcp + frida-server (fetched by install.sh, not committed)
├── apks/ pulled APKs, one dir per package (gitignored - not source)
└── loot/ captured flows, static-analysis output, MobSF data (gitignored - not source)Underlying scripts
mp is a thin front end over scripts/. Use these directly when you want
finer control; each is independently runnable.
Each capture creates loot/session-<timestamp>/ containing both the raw flows
and an analysis feed built for Claude Code to read. See
Feeding traffic to Claude Code.
Scripts
Script | Purpose |
| Starts frida-server, sets the global proxy, verifies root + CA. Run after every emulator reboot. |
| Starts mitmproxy on |
| Converts a saved flow file into the analysis feed offline. |
| Toggles the device-wide HTTP proxy. Turn off before using WireGuard mode. |
| Detached launch with the full bypass chain, no terminal to keep open. Use this for apps with root detection. |
| Same chain but an interactive Frida REPL, for when you want to poke at the runtime live. |
| Installs any PEM/DER CA into the system trust store. |
| Pulls Burp's CA from a running Burp and installs it system-wide. |
| Pulls base + split APKs into |
| apktool + jadx + apkleaks; reports manifest flags, exported components, network-security-config, pinning call sites, native libs. |
| MobSF at http://127.0.0.1:8010 (creds |
| Installs every host dependency: full on macOS, tooling-only on Linux, refuses on native Windows. Safe to re-run. |
|
|
Interception: three layers
The layers are cumulative. Each catches what the previous one misses.
1. System CA (installed). mitmproxy's CA is in /system/etc/security/cacerts,
so apps trust it even when they ignore user-installed certs, which every app
targeting API 24+ does by default. Verified working against Google's own
play-fe.googleapis.com and every target app tested so far: traffic decrypts
cleanly without any app-side pinning changes.
1b. Detection bypasses. Loaded with every launch:
android-disable-root-detection.js (su binaries, root manager packages,
Runtime.exec("su")) and android-anti-detection.js, which covers emulator/VM
tells (15 android.os.Build fields spoofed to a Samsung SM-S928B, getprop
filtered for qemu/goldfish/nox/mumu strings, emulator device nodes hidden from
File.exists), debugger and tracer checks (Debug.isDebuggerConnected,
TracerPid masked in /proc reads), Frida artefacts (agent lines dropped from
/proc/self/maps reads, frida* paths hidden), plus spoofed telephony
identifiers so an emulator's blank IMEI/operator does not give it away.
Each module reports [anti-detect] ok: <name>. If a defensive app dies on
launch, bisect by setting ANTI_DETECT_ONLY or ANTI_DETECT_SKIP in
frida-scripts/config.js:
var ANTI_DETECT_ONLY = ['Build','SystemProperties']; // enable only these
var ANTI_DETECT_SKIP = ['BufferedReader']; // or drop just oneHonest limit: these defeat client-side checks only. Play Integrity or SafetyNet verified by a backend cannot be bypassed here. The verdict is signed by Google and checked off-device.
2. Frida unpinning (launch.sh / hook.sh). Defeats apps that pin. The chain hooks
HttpsURLConnection, SSLContext.init, Conscrypt CertPinManager,
NetworkSecurityConfig, OkHttp CertificatePinner + OkHostnameVerifier,
TrustKit, and Cordova/Worklight/Netty/appmattus CT, plus a fallback auto-patcher
that catches custom TrustManagers at runtime, a proxy override for apps that
ignore system proxy settings, and root-detection bypass.
3. WireGuard mode (capture.sh wg). The fallback for apps that bypass the
HTTP proxy entirely (raw sockets, custom DNS, gRPC). mitmproxy prints a
WireGuard config; import it into a WireGuard client on the device. Turn the
global proxy off first. Don't stack layers 1 and 3.
Using Burp instead of mitmproxy
In Burp: Proxy → Proxy settings → Proxy listeners → Edit → Binding → All interfaces. Burp binds to
127.0.0.1by default and the emulator cannot reach that.Stop mitmproxy so port 8080 is free (or run Burp on another port and set
MP_PROXY_PORTto match)../scripts/install-burp-ca.sh 8080Re-run the
config.jscert step if you want Frida's runtime cert injection to use Burp's CA too. ReplaceCERT_PEMinfrida-scripts/config.js.
Feeding traffic to Claude Code
Raw flows are far too large and noisy to hand to an LLM: a single session is
megabytes of images, bundles and protobuf. scripts/mpfeed.py is a mitmproxy
addon that rides along with every capture and distils the traffic into something
readable. It runs automatically; you don't invoke it directly.
Every capture writes loot/session-<timestamp>/:
File | What it's for |
| One line per request. Skim this first. |
| Endpoints collapsed into templates ( |
| Auto-detected JWTs (with claims decoded), API keys, credentials, PII, card numbers. |
| Full detail, one JSON object per flow. |
| Raw flows, replay with |
Then just point me at it:
"Analyse
~/mobile-pentest/loot/session-20260805-164934/"
Scope it to your target so out-of-scope apps aren't captured or analysed:
MP_SCOPE=api.target.com,cdn.target.com ./scripts/capture.sh dumpAlready have a flow file? Convert it offline:
./scripts/analyze.sh loot/session-*/flows.mitm api.target.comWhat it does for you automatically:
Drops images, fonts, CSS/JS, media and binary blobs, so only meaningful traffic survives.
Truncates bodies to 4 KB (
MP_MAX_BODYto change).Decodes JWT claims inline, so
role,subandexpare visible without you pasting tokens into a decoder.Collapses
/users/4821and/users/9930into one/users/{id}endpoint, and lists the concrete IDs as IDOR candidates.Flags every endpoint that returned 2xx with no
AuthorizationorCookieheader. That's the fastest route to a broken-access-control finding.
Detector accuracy was tuned against real traffic: millisecond epoch timestamps are no longer mistaken for Luhn-valid card numbers, version strings are no longer mistaken for emails, and noisy detectors (UUID, ObjectId, email, phone) only fire when the match is properly delimited rather than sliced out of a base64 blob. Each detector is capped at 3 hits per flow so one chatty request can't flood the file.
Where the requests are
mp log shows Frida hook diagnostics (=> com.android.okhttp.Address $init …).
That's the unpinning chain reporting each hooked call, not your traffic.
The HTTP requests live in the capture session:
mp req # last 25 requests, readable
mp req -f # follow live as you use the app
mp req 100 # last 100
mp web # open the flows in the mitmweb UI (browse / edit / replay)mp req asks the running proxy where it is actually writing, rather than
assuming the newest directory. Those differ whenever a capture was reused.
Full detail per request is in loot/session-*/flows.jsonl (one JSON object per
flow: headers, bodies, IDOR candidates, flags). index.md is the skim view,
surface.md the endpoint rollup.
Web pentest with Burp
For Repeater / Intruder / Scanner, hand the traffic to Burp:
Start Burp. Proxy → Proxy settings → Proxy listeners → Edit → Binding → All interfaces. Burp binds to
127.0.0.1by default and the emulator cannot reach that.mp burp: stops mitmproxy, pulls Burp's CA through the running listener, installs it into the system store, updatesfrida-scripts/config.jsso the runtime injection uses Burp's CA too, and repoints the device.mp launch: relaunch the app so the new CA is injected.
mp burp --mitm switches back.
Re-signing custom request-integrity fields
Some APIs attach a custom integrity/MAC field to every request body (hashed
and signed with a key extracted from the app), so editing a body in Repeater
invalidates it. The pattern is generally: canonicalise the fields the app
signs, hash them, then encrypt/sign with the key pulled from the decompiled
APK or its native libs. This lab doesn't ship a target-specific signer, since
the exact fields and algorithm differ per app. Write a small
scripts/<target>-resign.py following that shape and both brief.py and the
dashboard's Repeater will pick it up automatically if you wire it in next to
zc-macin.py's loading pattern.
On one real target this turned out to be trivially bypassable: the MAC was RSA-encrypted with a public key embedded in the app itself, so anyone could mint a valid one. Worth checking for on any app that does this.
Worth trying first, before writing a signer at all: send a modified body without touching the integrity field. Some apps compute the MAC over a freshly generated request ID or timestamp that differs from what they transmit, and the server still accepts the request anyway. That's strong evidence the field isn't verified strictly, or at all, and which answer you get is itself the finding.
Inspecting one app's traffic
Every capture records which app was hooked when each flow was seen, so the UI can be filtered down to a single app instead of the whole device.
mp seen # which apps produced traffic in this session
mp web # picker over those apps -> opens filtered mitmweb
mp web tv.app1001.android # skip the picker
mp web '*' # everything, unfilteredmp web derives the app's hosts from the session and builds a mitmweb view
filter (~d api\-gateway\.1001\.tv). Note that mitmproxy treats (, ) and
| as filter-language operators, not regex, so ~d (a|b) is a syntax
error. The filter is emitted as ~d a | ~d b with dots escaped instead.
How attribution works, and its limit. The emulator NATs every app behind one
address, so per-connection attribution is not available. Instead launch.sh
records the hooked package in .active-app and the capture feed tags each flow
with it. The honest unit is "captured while app X was hooked", not "sent by
app X". Traffic from background OS services during that window is tagged the
same way. Combine with mp scope <host> when you need certainty about which
traffic is really the target's.
Claude Code engagements (mp engage)
Beyond one-shot analysis, mp engage starts a full Claude Code session for a
target: its own folder, live traffic wired in via MCP, and the pentest
skills/plugins active exactly as in a hand-started session.
mp engage # interactive session in a new Terminal, in the target folder
mp engage <pkg> --auto # headless agent: reviews the live capture, writes findings
mp engage --stop # stop running agentsEach run creates engagements/<pkg>_<timestamp>/ seeded with:
CLAUDE.md: target, scope, authorisation, and how to reach the live traffic toolstraffic/: symlink to the live capture session, plus aBRIEF.mdsnapshotfindings/: where the agent writes one report per issue
The dashboard's Claude Analysis tab tracks BOTH modes live, including an
interactive session you're driving by hand in Terminal. It polls
engagements/<latest>/findings/ regardless of who's writing to it, sorts newest
first (not alphabetically, so a fresh Critical isn't buried under older Lows),
and shows a Latest finding card at the top with severity and age. A small
dot appears on the Claude Scan nav tab whenever a new finding lands while
you're looking elsewhere, and opening the tab jumps straight to it.
Interactive mode opens a real claude session in Terminal.app in that
folder, with full skills, plugins, permissions and MCP. --auto mode runs a
headless agent (analysis-only allowlist: the mp-traffic MCP tools plus
Read/Write/Grep/Skill, no Bash, no request sending) that streams events to
scan.log and writes findings as it goes.
The Claude Analysis tab in the dashboard drives both: Start analysis, Open interactive, Stop, a live findings list with a reader, and the agent's current activity. The Traffic tab's Scan with Claude button starts an auto-scan on the current session.
In a real engagement the auto agent produced 8 findings unattended: BOLA/IDOR on ObjectId-addressed endpoints, an HS256 JWT carrying PII, mass assignment, wildcard CORS, and an unauthenticated CodePush OTA channel, each with evidence flow numbers and a read-only confirmation test.
Note: the agent's first message can trip the broad model safeguard on cyber-security phrasing; it auto-falls-back to another model and continues. The prompt is framed as a security review of already-captured data to minimise this.
Sessions & resuming
Every mp scan/mp switch starts a fresh capture session, but never
deletes the old one. loot/session-<timestamp>/ directories accumulate
forever (clean them up by hand if disk space matters); nothing in this lab
ever removes a past session automatically.
Dashboard. The Sessions tab lists every capture session (target app, flow count, age, with the live one marked) and every Claude Code engagement (mode, finding count, running or not, a resume link when it has one). Click a session to browse its requests read-only, the same detail view as Traffic minus the live-only actions. There's no "Analyse with Claude", since that targets the live session, but "Repeat this request" still works, fetching from the archived one.
CLI. mp sessions lists the 15 most recent capture sessions with their
tagged target, flow count, and disk size, plus a count of Claude Code
engagements.
Resuming an engagement. Every mp engage run (interactive or --auto)
now pins a claude --session-id and saves it to .session_id in the
engagement folder. Pick it back up any time:
mp engage --resume <engagement-dir-name> # e.g. com.example.app_20260101-120000opens a new Terminal window running claude --resume <id> in that folder,
with the full prior context restored. The dashboard's Sessions tab does the
same via its resume link. Engagements from before this feature existed
have no saved session id and show "not resumable".
Every tab resets cleanly on a target change. Switching target, whether via
mp scan/mp switch, the dashboard's New Scan, or the MCP target_scan/
target_switch tools, all go through the same config file the dashboard
polls. That clears the Traffic tab's tree filter and selected request, reloads
the Login tab for the new package, and refreshes the findings view.
Previously a stale tree filter or an already-open Login tab could keep
showing the old target's view even though capture had correctly moved to
the new one, which looked exactly like "still stuck on the old app". It's the
same class of bug as the automatic-scope-clearing gotcha (see
Gotchas), just on the dashboard's client side instead of the
capture backend. The Repeater's loaded request/history is deliberately
not reset, since it's for replaying any captured request regardless of
what's currently live.
Logging in
MuMu Player Pro is already a normal window on your Mac. Click into it and type, same as any app. That's "manual login" and it already works with no extra tooling. What's actually useful to add is a place to keep track of what to type between sessions, and a way to push a stored value into whatever field is focused without risking a typo on a long password or token.
mp login [pkg] bring MuMu to the front, show stored creds (masked)
mp login [pkg] --reveal same, unmasked
mp creds set <pkg> <label> <value> [--type text|password|totp]
mp creds rm <pkg> <label>
mp creds type <pkg> <label> tap the field on-screen FIRST, then this sends
that value (or, for a totp field, the live
code) to itThe dashboard's Login tab does the same thing visually: per-package
fields (label / value / type), a Send button per field that types it
into whatever's currently focused on the device, and for totp-type
fields a live 6-digit code with a 30-second countdown bar instead of a raw
value. Paste the base32 secret once and never manually compute a code again.
Storage: ~/mobile-pentest/creds/<pkg>.json, file mode 600 (dir 700).
Not encrypted. This is a local single-user lab tool, not a vault. Use
test/throwaway accounts only. CLI and dashboard read and write the exact
same file, so setting a field one way shows up immediately in the other.
Typing is done via adb shell input text, wrapped in real POSIX
single-quotes for the device-side shell. Verified against a password
containing ', ", &, $, and | all at once, which lands
character-for-character correct. ASCII only; for non-Latin scripts, type
directly into the MuMu window instead.
Site map & Repeater
Two additions to the Traffic tab aimed at manual work, not just analysis.
Site map tree
The Traffic tab has a left-hand tree, host → path, with numeric and hex
IDs folded to {id} (so /user/4821 and /user/9930 collapse onto one
/user/{id} branch, same folding mp brief already uses). Each node shows
its hit count. Click any node to filter the request table to just that
subtree. The active filter shows as a chip next to the search box, with an
✕ to clear it. Clicking also expands/collapses; hosts start expanded one
level, everything deeper starts collapsed. This is the same shape as Burp's
Target → Site map, scoped to what got captured in the current session.
Repeater
Every request's detail pane has a "Repeat this request" button next to "Analyse with Claude". It opens the Repeater tab pre-filled with that request's method, URL, headers and body, all editable, plus a Send button that transmits it for real and shows the raw response (status, timing, headers, body).
Repeater tab
METHOD + URL bar, re-sign checkbox, Send
┌─────────────────────┬──────────────────────┐
│ headers (editable) │ response headers │
│ body (editable) │ response body │
│ history (this tab) │ │
└─────────────────────┴──────────────────────┘Requests are sent directly from the Mac, not through the emulator or its
proxy. The captured APIs are ordinary internet endpoints, so no device hop
is needed. Re-sign macIn auto-detects when the body is JSON containing a
macIn-style integrity field and recomputes it via a target-specific signer
(see Re-signing custom request-integrity fields)
before sending, so an edited body stays validly signed. Same mechanism as
replay.py, now interactive. History keeps the last 25 sends from this
browser session (not persisted across dashboard restarts); click any entry to
reload both the request and its response. New clears the editor to build
a request from scratch, useful for probing an endpoint by hand, like IDOR
testing with a guessed ID the capture never actually saw.
Nothing here is a dry run. Send transmits immediately. Treat it with the
same care as replay.py --send: know what you're sending before you click it.
Wiring traffic into Claude Code (lightweight)
Three routes, increasing directness:
1. MCP server, full control plane. bin/mp-mcp is a single MCP server
(36 tools) covering both traffic analysis and everything the CLI/dashboard can
do. Register it once and drive the whole lab from chat:
claude mcp add --scope user mobix ~/mobile-pentest/bin/mp-mcp(Already registered it under the old name? mp-traffic still works, same
binary and same tools, just a different local alias. No need to re-add.)
group | tools | what it does |
traffic (read-only) |
| query the live capture: session state, list/filter/search requests, one request in full with payloads decoded, the ranked brief |
lab lifecycle |
| device/environment state, installed apps, bring the environment up/down without necessarily touching target/capture |
targeting |
| set/switch/scan the target (same flow as |
bypass |
| read/toggle the Frida scripts and anti-detection modules |
Burp / MobSF |
| hand traffic to Burp and back, start MobSF |
engagements |
| start/stop a Claude Code engagement, read back its findings, resume a past one by session id |
sessions |
| every past capture session (target, flow count, live/archived) and engagement (mode, findings, resumable session id); nothing here is ever deleted by a new scan |
credentials |
| manage stored test creds and push a value into a focused device field. Never returns a stored plaintext password/TOTP secret, see below |
Just ask: "use target_scan to start a fresh scan on com.example.app" or "use traffic_flows to show unauthenticated requests".
Deliberately not exposed, even under "full control plane": raw mp shell
(arbitrary command execution), objection/logcat (interactive, no clean
request/response shape), and any tool that would return a plaintext stored
credential. Every mutating tool shells out to the exact same whitelisted mp /
helper-script commands the CLI and dashboard already use. Package names and
hosts are regex-validated before they ever reach a subprocess argv, never a
shell string. creds_list mirrors creds.py show (passwords masked, TOTP
shown as a live rotating code, never the raw secret); creds_send types a
stored value into the on-device field currently focused without ever echoing
it back.
The headless --auto engagement agent (below) keeps its original safety
property regardless of this expansion: its --allowedTools allowlist names
the 6 traffic_* tools explicitly, not a wildcard, so it still can't reach
any of the new mutating tools: no Bash, no request sending, no scan/switch/
credential actions.
Re-register after moving the directory:
claude mcp add --scope user mobix ~/mobile-pentest/bin/mp-mcp
2. Dashboard button. Analyse with Claude in the Traffic toolbar runs the
session brief through claude -p headlessly and renders the analysis in the
detail pane. Each request also gets its own Analyse this request with
Claude.
3. Files. mp brief still writes BRIEF.md + replay.py if you'd rather
hand over a path.
mp brief # -> BRIEF.md + replay.py in the session dirThen just say: "read ~/mobile-pentest/loot/session-<ts>/BRIEF.md".
BRIEF.md is one file built to be acted on rather than browsed. It ranks every
endpoint by risk with the reasoning attached, and splits concrete test
candidates into broken-access-control (2xx with no credentials), IDOR/BOLA
(object references seen in paths and query strings), and mass-assignment /
injection (endpoints with fat request bodies). It ends with a coverage note
when everything captured was unauthenticated, so the gap is never mistaken for
a clean result.
mp analyze runs the full feed and the brief. mp brief alone is faster when
you only want the actionable view.
The replay harness
brief.py also writes a self-contained replay.py into the session:
cd ~/mobile-pentest/loot/session-<ts>
./replay.py --list # captured requests
./replay.py 3 # DRY RUN - prints, sends nothing
./replay.py 3 --send # actually transmit
./replay.py 3 --set '{"MTI":"4009"}' # patch the body, re-sign, dry run
./replay.py 3 --set-body-json '{"a":1}' # set requestBody (base64) + re-signDry run is the default. --send is always required to transmit. That
matters when the target is a live payment system.
If the body carries a macIn field the harness re-signs it automatically via
scripts/zc-macin.py, so patched requests stay valid. For targets without a
signature the signer is simply absent and the body passes through unchanged.
Static analysis
./scripts/pull-apk.sh com.target.app
./scripts/static-scan.sh ~/mobile-pentest/apks/com.target.app/base.apk
./scripts/mobsf.sh start # then upload the APK at http://127.0.0.1:8010static-scan.sh writes to loot/static-<name>/: jadx/sources/ for reading
code, apkleaks.txt for hardcoded secrets and endpoints, pinning-hits.txt for
where to aim Frida, exported-components.txt for IPC attack surface.
Runtime exploration
objection -g com.target.app explore
# then, inside objection:
# android hooking list activities
# android hooking watch class_method <method> --dump-args --dump-return
# android keystore list
# android sslpinning disable
# env # app data pathsUseful direct pulls for insecure-storage findings:
adb shell "run-as com.target.app ls -la /data/data/com.target.app/shared_prefs"
adb shell "find /data/data/com.target.app -name '*.db' -o -name '*.xml'"Installing an app
Onboard a new target from a file or a link, then analyse it:
mp install app.apk # local APK
mp install app.xapk # split bundle (.xapk/.apkm/.apks/dir also work)
mp install https://host/app.apk # direct APK URL, downloads then installs
mp install 'https://play.google.com/store/apps/details?id=com.x' # opens Play Store on device
mp install <src> --passive # static analysis only, no hookingThe pipeline: install → detect the new package → set it as target →
passive analysis (pull APK, apktool + jadx + apkleaks, and a MobSF static
scan at http://127.0.0.1:8010) → active analysis (hook it and start a live
capture) → ready for mp brief / mp engage.
Notes:
Play Store links open the store page on the device; complete the install there (Google sign-in and payment can't be automated), then it continues.
Re-installing an app already present is fine. The package is read from the APK manifest via apktool when the install adds nothing new.
In the dashboard, the Install tab does the same: upload an APK, or paste a link/path, with a live install log.
Switching apps
Use mp switch <pkg> (or the dashboard Apps picker) to change target, not
mp target, which only edits config. Switching does the whole cycle: force-stops
the outgoing app, stops the old capture, rolls a fresh session, and launches
the new app hooked.
Why it matters: three things used to cause "I changed the app but the traffic didn't change":
No live capture was running, only a
mitmweb -rreplay of an old session, which is a frozen file and never updates. The dashboard Traffic tab is the live view;mp webopens a snapshot.mp targetalone doesn't restart anything. The old app stayed hooked and captured.mp switchis the real change;mp targetnow warns if a capture is still live.The old app kept running in the background, and because capture is device-wide, its traffic polluted the new session and got mis-tagged as the new app.
mp switchforce-stops the outgoing app first.
For a clean per-app capture, also set mp scope <host> so only the target's
hosts are recorded.
Apps that refuse to run on a rooted device
Some apps check for root and hard-stop. One target app in testing showed "Rooted device detected. Please unroot your device to continue." and never left the splash screen. That's a fairly typical response.
Start them through launch.sh. The bypass chain includes root detection, so
the checks are neutralised before the app looks:
./scripts/launch.sh <pkg>Tapping the launcher icon starts the app unhooked and the check fires again.
Every session has to begin with launch.sh. Confirm it worked by the
root checks blocked: N line it prints (commonly 20-30 for apps with a real
root-detection library), or watch live with ./scripts/launch.sh --log.
A typical root-check chain probes, from the hook log: su in eight paths
(/system/bin, /system/xbin, /sbin, /data/local/*, /su/bin, …),
/system/app/Superuser.apk, Runtime.exec("su"), and the SuperSU / Magisk /
noshufou package names.
launch.sh drives the frida CLI, not the Python bindings, deliberately.
Since Frida 17 the Java global comes from frida-java-bridge, which the CLI
injects and a raw create_script() does not. Bindings fail with
ReferenceError: 'Java' is not defined and every Java hook silently no-ops.
Piping from tail -f /dev/null gives the CLI a stdin that never reaches EOF, so
the session survives without a TTY.
Gotchas
After an emulator reboot, re-run
start-env.sh. The CA survives (it's on/system, which persists), but frida-server and the proxy setting do not.Frida version lock: host CLI and device server must match exactly, both are 17.9.10 here. Frida is not a brew formula; the CLI is pip-installed straight into brew's
python@3.13site-packages (python3.13 -m pip install --break-system-packages frida==17.9.10 frida-tools objection, which is whatscripts/install.shruns). To move to a newer version, bump the pin in bothscripts/install.shandscripts/selftest.sh, re-run the pip install with the new version, download the matchingfrida-serverarm64 binary intobin/, and re-push it. A stray pipxfrida-toolsvenv can carry a different version, so don't let it shadow the real CLI onPATH.mp doctorchecks for this on every run.Google endpoints will still fail TLS (
accounts.google.com,clients4.google.com). Google Play Services pins independently of your target app. That noise is expected. Ignore it; it doesn't mean interception is broken.timeoutdoesn't exist on macOS. Usegtimeout(coreutils, installed).Spawning under the full hook chain occasionally loses a race and the app dies with
Process crashed: Bad access due to invalid addressbefore the scripts finish installing. It is not deterministic, roughly one launch in four or five, and the same command succeeds on retry.launch.shretries once automatically and printsspawn lost a race … retrying once. If a specific app fails every time, that is a real incompatibility. Bisect it from the Bypass tab rather than assuming the race.The MuMu adb serial is not stable. After a restart the instance registers either as
emulator-5554or as127.0.0.1:<adb_port>, and that port increments between runs (26624, 26625, …). Nothing should hardcode a serial:scripts/env.shresolves it at source time viamp_resolve_device, and the boot wait re-resolves on every pass because the serial can change while you're waiting. Pin one withMP_DEVICE_FORCEif you ever need to.MobSF's container reports
unhealthyfor the first minute or so while it builds its DB. It still serves.Switching target auto-clears a leftover scope. A scope set for the previous app's hosts silently filters the new target's traffic out of the analysis feed entirely. Flows still land in
flows.mitm, butflows.jsonl/the dashboard/traffic_flowsshow nothing, which looks exactly like capture is stuck on the old app.mp scan/mp switchdetect a target change and clearSCOPEautomatically (with a warning); re-set it withmp scope <host>once you know the new target's hosts. See Sessions & resuming for the matching dashboard-side fix (stale tree filter / Login tab showing the old target).
Scope discipline
Whatever real apps happen to be installed on your device, only test
applications covered by a signed authorization / rules of engagement. Traffic
capture is broad by default: capture.sh records everything the device sends,
including from apps that are not your target. Prefer hook.sh on the specific
target package, and clear loot/ between engagements so evidence from one
client never mixes with another's.
This server cannot be installed
Maintenance
Related MCP Servers
- FlicenseAqualityDmaintenanceAn AI-driven Android dynamic analysis tool that enables AI models to perform automated reverse engineering by controlling Frida. It provides capabilities to inject scripts, manage application processes, and analyze real-time execution data through the Model Context Protocol.1063
- Alicense-qualityCmaintenanceEnables authorized Android security testing with static and dynamic analysis, Frida instrumentation, storage inspection, and traffic interception via MCP tools.MIT
- AlicenseAqualityBmaintenanceA minimal, secure MCP server for AI-assisted mobile development, enabling build, install, interact, and inspect Android/iOS apps.33227Apache 2.0
- AlicenseAqualityCmaintenanceMCP server wrapping Drozer for LLM-driven Android IPC security testing, enabling autonomous enumeration and exploitation of app components via natural language.191Apache 2.0
Related MCP Connectors
Remote MCP for Android CLI agent build gate, structured receipts, audit logs, and reviewer-ready evi
Offline methodology engine for authorized penetration testing, CTF, and security research.
Zero-config MCP security scanner for AI-generated apps. 25K+ vulnerability patterns.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/blackfoxxx/Mobix'
If you have feedback or need assistance with the MCP directory API, please join our Discord server