Storage⚓
Where Android apps store data, how to extract it, and what tends to become a real finding during a pentest.
Why Storage Matters⚓
A lot of mobile findings are not about transport security or fancy runtime hooks. They are about data at rest:
- Session tokens left in SharedPreferences
- Refresh tokens or API keys stored in plaintext files
- SQLite databases holding PII or payment data
- WebView cookies and local storage surviving logout
- Sensitive exports cached to shared storage where any app can read them
Storage review answers a simple question: if I get filesystem access, partial app access, backup access, or another app on the same device, what secrets fall out immediately?
Storage Map⚓
Most app data lives under the app sandbox:
Typical locations:
| Location | What it usually holds | What to look for |
|---|---|---|
shared_prefs/ |
XML key-value pairs | auth tokens, feature flags, email, device IDs |
databases/ |
SQLite / Room DBs | PII, messages, offline records, cached API data |
files/ |
Arbitrary app files | exported PDFs, JSON blobs, certs, serialized objects |
cache/ |
Temporary files | API responses, images, report exports, unencrypted temp data |
app_webview/ |
Chromium WebView state | cookies, local storage, IndexedDB, autofill artifacts |
no_backup/ |
Files excluded from Android backup | long-lived keys, install IDs, migration markers |
| External storage | Files outside the sandbox | world-readable exports, logs, backups |
Getting Access to App Data⚓
Choose the least intrusive method that works.
1. run-as on a Debuggable App⚓
If the manifest has android:debuggable="true", you can enter the sandbox without root:
This is the cleanest path on test builds.
2. Rooted Device or Rooted Emulator⚓
On a rooted device or a Google APIs emulator with adb root:
Or copy the whole sandbox out:
3. Backup-Based Extraction⚓
On older apps and devices, backup paths may still work if backup is allowed. This is less reliable on modern Android, but it is still worth checking when allowBackup is enabled or the app targets older APIs.
See the backup discussion under App Structure for the exact workflow.
4. Runtime Access with Frida⚓
If direct filesystem access is limited, read data through the app's own APIs:
- SharedPreferences via
getSharedPreferences() - SQLite wrappers or DAOs via hooked methods
- WebView cookies via
CookieManager - Keystore usage via crypto helper hooks
Use this when you have code execution in-process but not root.
SharedPreferences⚓
SharedPreferences are simple XML files stored in:
Example:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="access_token">eyJhbGciOi...</string>
<string name="refresh_token">def50200...</string>
<string name="user_email">victim@example.com</string>
<boolean name="is_logged_in" value="true" />
</map>
What to Look For⚓
- Access tokens, refresh tokens, JWTs
- API keys and environment URLs
- Role or feature flags like
isAdmin,isPremium,debug_enabled - Cached credentials or PINs
- Encryption keys stored next to encrypted data
Recon⚓
adb shell run-as com.example.app ls shared_prefs
adb shell run-as com.example.app cat shared_prefs/auth.xml
# Rooted path
adb shell cat /data/data/com.example.app/shared_prefs/auth.xml
If the app uses EncryptedSharedPreferences, note that this is better than plaintext but still not magic. The real question becomes: where is the master key derived from, and can the app itself decrypt values for you at runtime?
SQLite Databases⚓
Android apps commonly use SQLite directly or through Room. Database files live in:
Typical contents:
- User profiles and account metadata
- Chat messages and notifications
- Offline sync queues
- Cached API responses
- Payment or order records
- Device binding and session state
Basic Workflow⚓
# List databases
adb shell run-as com.example.app ls databases
# Copy one out
adb shell run-as com.example.app cp databases/app.db /sdcard/app.db
adb pull /sdcard/app.db
# Inspect locally
sqlite3 app.db ".tables"
sqlite3 app.db ".schema"
sqlite3 app.db "select * from users limit 10;"
High-Value Tables⚓
Look for table names containing:
user,account,profilesession,token,authmessage,chat,notificationpayment,card,order,invoiceconfig,feature_flag,remote_config
Common Findings⚓
- Sensitive records stored unencrypted
- SQL injection in app-managed providers or raw query builders
- Deleted data still recoverable from cache tables or WAL files
- Privileged flags editable client-side and trusted by the UI
Also pull the SQLite sidecar files if they exist:
The WAL often contains recently written rows not yet merged into the main DB.
Internal Files and Cache⚓
Not everything is in SharedPreferences or SQLite. Apps often dump raw blobs into files/ or cache/.
Common examples:
- Exported PDFs and CSVs
- Downloaded API responses
- Serialized Java objects
- Temporary image uploads
- Crash artifacts with stack traces or request bodies
- Cached GraphQL or REST responses
Recon⚓
adb shell run-as com.example.app find files -maxdepth 3 -type f
adb shell run-as com.example.app find cache -maxdepth 3 -type f
# Rooted alternative
adb shell find /data/data/com.example.app/files -maxdepth 3 -type f
adb shell find /data/data/com.example.app/cache -maxdepth 3 -type f
Pull anything that looks interesting:
adb shell run-as com.example.app cp files/session.json /sdcard/session.json
adb pull /sdcard/session.json
What to Look For⚓
- Plaintext JSON containing tokens or user records
- Reports exported to cache before share intents
- Files with misleading extensions that are really ZIPs, SQLite DBs, or protobuf blobs
- Temp files created before encryption or before upload
External and Shared Storage⚓
Anything written outside the app sandbox is immediately more exposed. Historically this meant SD card paths; on modern Android it includes app-specific external dirs plus media collections.
Examples:
Why It Matters⚓
- Other apps may be able to read it, depending on Android version and granted permissions
- Users often back it up or sync it without realising it contains secrets
- Pentesters can often pull it without root
Recon⚓
adb shell ls /sdcard/
adb shell find /sdcard/Android/data/com.example.app -maxdepth 4 -type f
adb shell find /sdcard/Download -maxdepth 2 -type f | grep -i "example\|report\|export"
Common Findings⚓
- Auth tokens in exported config files
- Logs written to world-readable paths
- Unencrypted backups or offline cache files
- Temporary camera or document uploads left behind after completion
On Android 10+ scoped storage limits broad file access, but apps can still leak their own sensitive data by writing it to shared locations.
WebView Storage⚓
If the app uses WebView, Chromium leaves its own storage under app_webview/.
Interesting artifacts include:
- Cookies
- Local Storage
- IndexedDB
- Autofill state
Typical paths:
/data/data/com.example.app/app_webview/
/data/data/com.example.app/app_webview/Cookies
/data/data/com.example.app/app_webview/Default/Local Storage/
/data/data/com.example.app/app_webview/Default/IndexedDB/
What to Look For⚓
- Session cookies surviving logout
- OAuth or SSO tokens stored in browser state
- Sensitive profile data cached in local storage
- Internal admin endpoints visible in cached web content
Runtime Access Example⚓
Java.perform(function () {
var CookieManager = Java.use("android.webkit.CookieManager");
var cm = CookieManager.getInstance();
console.log(cm.getCookie("https://app.example.com"));
});
That is often faster than pulling and parsing the SQLite cookie DB by hand.
Android Keystore⚓
The Android Keystore protects key material better than raw files, but it does not automatically make the overall design secure.
Questions to ask:
- Is the app encrypting sensitive data, or just a few fields?
- Where does the ciphertext live?
- Can the app decrypt it for any logged-in user session without extra checks?
- Is the same keystore key reused across users or environments?
What Usually Goes Wrong⚓
- The app stores ciphertext securely but leaves the decrypted value in cache or logs
- The app stores the keystore alias and encrypted blob together, then exposes a method that decrypts on demand
- A rooted or instrumented attacker can call the app's decrypt method directly
So when you see Keystore usage in jadx, do not stop at "encrypted". Trace the full encrypt → store → load → decrypt flow.
Logs and Crash Artifacts⚓
Storage review should include anything the app writes for debugging or support.
Look for:
- Local log files in
files/,cache/, or external storage - Stack traces including tokens, request bodies, or SQL queries
- Crash reporters caching unsent reports
- Screenshot or screen-recording features that dump sensitive content
Basic recon:
adb shell run-as com.example.app find . -type f | grep -Ei "log|crash|report|trace|dump"
adb logcat | grep -i "token\|auth\|password\|exception"
If the app integrates a crash SDK, search for its cache path in jadx and inspect what it stores locally before upload.
Quick Storage Checklist⚓
[ ] Check shared_prefs/ for tokens, flags, PII, URLs, keys
[ ] Check databases/ plus -wal / -shm files
[ ] Enumerate files/ and cache/ for JSON, exports, temp blobs
[ ] Check external storage paths for app-owned files
[ ] If WebView is used, inspect app_webview/ state
[ ] Trace any encryption scheme end-to-end; do not stop at "uses Keystore"
[ ] Search for logs, crash files, screenshots, and support bundles
[ ] If direct filesystem access is blocked, read data through Frida hooks
Recommended Review Order⚓
For a new target:
1. Check manifest flags: debuggable, allowBackup, exported providers
2. Get filesystem access: run-as, root, or backup path
3. Read SharedPreferences first
4. Dump and inspect SQLite databases and WAL files
5. Enumerate files/, cache/, no_backup/
6. Check external storage and app_webview/
7. Trace any encryption or Keystore use in jadx / Frida
8. Validate whether stored values can be replayed or tampered with