What Is Mobile App Penetration Testing? (iOS and Android)
- ✓Mobile app penetration testing attacks the client binary, on-device storage, and the backend APIs an iOS or Android app talks to, graded against the OWASP MASVS standard and reproduced with the MASTG test cases.
- ✓The attacker controls the device: they can root or jailbreak it, decompile the app with jadx or Hopper, dump shared_prefs with adb, and hook functions at runtime with Frida. Your binary is an untrusted, fully visible artifact.
- ✓Static analysis (apktool, jadx, MobSF) finds the leads; dynamic analysis on a rooted or jailbroken device (Frida, objection, Burp) confirms which leads are real and exploitable.
- ✓The findings that recur are cleartext tokens in shared_prefs (MASVS-STORAGE), hardcoded keys in the binary (MASVS-CRYPTO), and missing or one-script pinning (MASVS-NETWORK), not exotic zero-days.
- ✓A single-platform engagement runs about 5 to 10 days and produces CVSS-scored findings, each tied to a MASVS control and a MASTG test ID.
Run MobSF over almost any production Android build and the score comes back in the 30s out of 100, with a wall of red rows before you have even opened a device. That gap is the point: a web pentest of your backend never touches the cleartext token in shared_prefs, the AES key compiled into a constants file, or the certificate pin a stock Frida script peels off in ten seconds. Those live on the client, and the client belongs to the attacker.
Mobile application penetration testing is the practice of attacking that client the way a real adversary does. This post walks the engagement end to end: where you start, the actual tool output you read along the way, how iOS and Android diverge, a sample findings table from a report, and the configuration changes that actually close the gaps.
What is mobile app penetration testing?
Mobile app penetration testing is an authorized security assessment of a mobile application's client code, its on-device data, and the backend APIs it calls, with the goal of finding and exploiting vulnerabilities before an attacker does. You report each one with reproduction steps, a CVSS score, and the OWASP MASVS control it violates.
It is fundamentally different from a server pentest because the threat model assumes the attacker owns the runtime. They root or jailbreak the device, decompile the app, read its private storage, hook its functions in memory, and tamper with its traffic. So the assessment treats the binary as a fully visible, fully controllable artifact, not a trusted black box. Risk categories come from the OWASP Mobile Top 10; the verifiable controls and test procedures come from MASVS and the MASTG.
Where do you start when you unpack the binary?
You start static, because it is free, fast, and tells you where to point the device phase. On Android the first move is to decode the package and look at what apktool reconstructs:
$ apktool d banking.apk -o src/
I: Using Apktool 2.9.3 on banking.apk
I: Baksmaling classes.dex...
I: Decoding AndroidManifest.xml with resources...
$ tree -L 2 src/
src/
├─ AndroidManifest.xml
├─ apktool.yml
├─ res/
├─ assets/
│ └─ config.properties <- bundled config, always read this
└─ smali/That config.properties in assets/ is the kind of file that ships secrets. The manifest tells you the attack surface (every exported="true" component is reachable). Then decompile to readable Java with jadx and grep the tree for the obvious mistakes:
$ jadx -d out/ banking.apk && grep -rniE 'secret|api[_-]?key|AES' out/sources/
out/sources/com/bank/crypto/KeyVault.java:14:
private static final String AES_KEY = "j8Hk2Lp9QwRt5VxZ"; <- hardcoded 128-bit key
out/sources/com/bank/net/ApiClient.java:31:
String API_SECRET = "sk_live_4eC39HqLyjWDarjt";A symmetric key in KeyVault.java means every install ships the same key, so anything it encrypted on disk is decryptable by anyone with the APK. On iOS the equivalent entry is a decrypted IPA loaded into Hopper or Ghidra, with otool -L for linked libraries and class-dump for the Objective-C class layout.
Static analysis only tells you what is possible
Static findings are leads, not confirmed vulnerabilities. MobSF gives you a fast scored triage that ranks where to dig, but its score is a starting point, not a verdict:
$ docker run -p 8000:8000 opensecurity/mobile-security-framework-mobsf
# upload banking.apk, then read the summary:
Security Score : 38/100
Grade : C
[HIGH] App can be installed on a vulnerable Android version (minSdk 21)
[HIGH] Debug certificate / android:debuggable could be true
[WARN] App uses SharedPreferences in MODE_WORLD_READABLE pattern
[HIGH] Hardcoded secrets found (3) - see 'Secrets' tabMobSF flags that a string looks like a key and that a component is exported. What it cannot tell you is whether that token is live, whether the pin falls to a stock script, or whether the biometric gate protects a real authorization check or a cosmetic screen. Only the running app answers those questions, which is why the device phase exists.
How does dynamic analysis confirm a finding?
Dynamic analysis runs the app on a rooted or jailbroken device and proves which static leads are exploitable. The single most common confirmation is pulling the app's private storage after you have logged in and clicked around:
$ adb shell run-as com.bank.app cat shared_prefs/session.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="auth_token">eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjQ0MTd9...</string>
<string name="refresh">rt_9f2a7c1b04e3</string>
<int name="pin" value="4417" />
</map>That is a live JWT, a refresh token, and a user PIN in plaintext, readable on any rooted device. The next confirmation is the network layer. Proxy the app through Burp, and if it pins, attach Frida with objection's pinning bypass and watch it land:
$ objection -g com.bank.app explore
com.bank.app on (Pixel: 13) [usb] # android sslpinning disable
(agent) Custom TrustManager check bypassed
(agent) OkHttp 3.x CertificatePinner.check() bypassed
(agent) SSLContext.init() custom TrustManager injected
[+] Pinning bypassed. Traffic now visible in Burp.Three lines of console output and the pin is gone. When the app refuses the proxy or uses raw sockets, you escalate to forced redirection, covered in intercepting proxy-unaware app traffic. This static-then-dynamic loop is exactly what agentic pentesting automates across many apps and methods at once.
How is iOS pentesting different from Android?
The methodology and the MASVS controls are shared, but the platform internals diverge in ways that change your toolchain on day one. Android ships an APK you unpack with apktool and decompile with jadx; iOS ships an IPA with a Mach-O you must first decrypt (FairPlay), then disassemble in Hopper or Ghidra.
- Secure storage: Android uses the Keystore and SharedPreferences; iOS uses the Keychain and plists/NSUserDefaults. Misuse on either side leaks tokens to a rooted or jailbroken attacker.
- Privilege: Android needs root (Magisk) to run frida-server and read
/data/data; iOS needs a jailbreak or a re-signed debug build. - IPC: Android exposes exported activities, services, and content providers (probe with Drozer); iOS uses URL schemes, universal links, and XPC.
- Getting the binary: iOS code signing and FairPlay encryption mean you usually work from a decrypted IPA pulled off-device with frida-ios-dump.
The full per-control test list for both platforms lives in our mobile app penetration testing checklist.
A WebView that no one pinned
On a recent assessment of a retail banking app, the login and money-movement flows pinned their certificate flawlessly, and the team was rightly proud of it. The rewards section, though, loaded in a WebView with setJavaScriptEnabled(true) and an addJavascriptInterface bridge exposing a native object, with no pinning on that traffic at all. We MITM'd the rewards endpoint, injected JavaScript that called the exposed bridge, and reached native app context, including the same session token from the storage finding above. The happy path was bulletproof; the side door was wide open.
That is the recurring lesson. The exploitable findings are rarely zero-days in the binary. They are the storage the team forgot to encrypt, the SDK that pins separately from the host, and the one screen that skipped the control everything else enforced. Below is what that report excerpt looks like, scored and mapped to controls.
What do the fixes actually look like?
Durable remediation is configuration, not advice to "use strong passwords." Each finding above has a concrete fix:
- Cleartext token in shared_prefs: store it with
EncryptedSharedPreferencesbacked by a Keystore master key (MasterKey.Builder(context).setKeyScheme(AES256_GCM).build()) on Android, or the Keychain withkSecAttrAccessibleWhenUnlockedThisDeviceOnlyon iOS. Never store the PIN at all. - Hardcoded AES key: generate a per-install key in the Android Keystore (
KeyGenParameterSpecwithsetUserAuthenticationRequiredwhere appropriate) so the key never exists in the binary or leaves secure hardware. - Bypassable pin: pinning alone never stops a rooted attacker, but a Network Security Config (
<pin-set>innetwork_security_config.xml) plus a real attestation check raises the cost; the durable control is server-side, with short-lived, scoped tokens that are useless once exfiltrated. - WebView bridge: remove
addJavascriptInterfacefor untrusted content, setsetJavaScriptEnabled(false)where possible, and pin the WebView's traffic too.
Treat client-side root and jailbreak detection as friction, not a boundary, because any client check can eventually be hooked. We map these controls to regulated data in mobile pentesting for data protection compliance.
