Man in the Middle⚓
Intercepting Android application traffic using a proxy.
Setup: Burp Suite + Android⚓
1. Configure Burp Proxy Listener⚓
In Burp: Proxy → Proxy Settings → Add a listener with these settings:
- Binding tab → Bind to port:
8080, Bind to address:All interfacesYou can bind to just your LAN IP (
192.168.x.x) if you prefer, but binding toAll interfaces(0.0.0.0) is easier — Burp will keep working even if your IP changes (DHCP renew, switching networks) and it covers all adapters including the emulator's virtual one. - Request handling tab → check "Support invisible proxying"
Invisible proxying lets Burp handle traffic from apps that don't honour the system proxy or use their own HTTP client. Without it, those requests are silently dropped.
2. Find Your Machine's LAN IP⚓
Both the device and emulator need your machine's LAN IP to reach Burp:
Note the 192.168.x.x address — you'll use it in the next step.
3. Set the Proxy on the Device⚓
Device and machine must be on the same Wi-Fi network.
On the device:
Set via ADB — no need to touch the emulator UI:
# Set proxy
adb shell settings put global http_proxy 192.168.x.x:8080
# Verify
adb shell settings get global http_proxy
# Remove when done
adb shell settings put global http_proxy :0
Tip
10.0.2.2 is a special emulator alias that always points to your host machine's localhost — also works if you prefer not to look up your LAN IP.
4. Install Burp CA Certificate⚓
Export the Burp CA in DER format: Burp → Proxy → Proxy Settings → Import/Export CA Certificate → Export Certificate in DER format → save as cacert.der.
On device: Settings → Security → Install from storage (or search "Install certificate") → select cacert.der.
The cert is installed as a User certificate. This is enough for apps targeting API ≤ 23 (Android 6 and below). For API 24+ apps, see the section below.
On emulator: Settings → Security → Install from storage → select cacert.der.
Same limitation applies — user certs are ignored by API 24+ apps. Since some emulators support adb root, you can push directly to the system store instead (see the System Store Push bypass below) — refer to the Setup section if adb root doesn't work on your emulator build.
5. Verify the Setup⚓
Before testing the target app, confirm traffic is flowing through Burp:
- Open a browser on the device/emulator and visit any HTTP site (e.g.
http://example.com) - In Burp, go to Proxy → HTTP history — you should see the request appear
- If the browser works but the target app shows no traffic, the app either ignores the system proxy (see iptables Transparent Proxy below) or pins its certificate (see Certificate Pinning)
iptables Transparent Proxy⚓
The system proxy approach above only works if the app respects the Android http_proxy setting. Many apps don't — they use their own HTTP client and connect directly. iptables lets you intercept this traffic at the kernel level, redirecting all outbound TCP on port 80/443 to Burp before it ever leaves the device.
How It Differs From the System Proxy⚓
| System Proxy | iptables | |
|---|---|---|
| How traffic is redirected | App must honour global http_proxy |
Kernel NAT rule — bypasses app-level config |
| Root required | No | Yes |
| App awareness | App explicitly connects to Burp host:port | App targets its real server — kernel silently redirects |
| Scope | Only proxy-aware HTTP clients | All TCP on the targeted ports |
| Burp config | Standard listener | Invisible proxying must be enabled |
Setup⚓
Requires a rooted device (Magisk) or an emulator with adb root.
1. Enable Invisible Proxying in Burp⚓
Burp → Proxy → Proxy Settings → your listener → Request handling → check "Support invisible proxying".
This is required because redirected requests arrive without a proxy CONNECT header. Burp reads the original destination from the Host header instead.
2. Create the Redirect Script⚓
Create redirect.sh locally, then push it to the device:
#!/bin/sh
# Replace with your machine's LAN IP and Burp port/ IF PREROUTING doesn't work, try OUTPUT
BURP_IP=192.168.x.x
BURP_PORT=8080
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination $BURP_IP:$BURP_PORT
iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination $BURP_IP:$BURP_PORT
3. Apply the Rules as Root⚓
4. Verify⚓
Apps that previously showed no traffic in Burp should now appear in Proxy → HTTP history.
5. Clean Up When Done⚓
Limitations⚓
- Root is required — no workaround for unrooted physical devices
- TLS is still TLS: CA trust and certificate pinning bypass steps still apply exactly as above
- Only TCP is affected; UDP traffic (QUIC, DNS-over-UDP) is not redirected
- On some Android 10+ builds,
iptablesrules set in a root shell may only apply to the default network namespace — useip netns execif rules appear to have no effect
Android 7+ / API 24+: Making Apps Trust Your CA⚓
Apps targeting API 24+ ignore user-installed CAs by design. All bypass methods are in the Bypasses section below.
Certificate Pinning⚓
A separate problem that sits on top of CA trust. Even if your CA is fully trusted by the OS, the app can still reject the connection — because certificate pinning makes the app verify not just a valid cert, but the specific cert (or public key) it was shipped expecting.
How It Works⚓
Normal TLS validation:
- Server presents a certificate
- Android checks: is this cert signed by a trusted CA? → if yes, allow
With pinning, an extra step is added by the app:
- Server presents a certificate
- Android: is this cert signed by a trusted CA? → yes
- App: does this cert's public key hash match my hardcoded pin? → if no, throw an exception and abort
When you proxy through Burp, the cert served is Burp's CA-signed cert for the domain — not the real server's cert. Burp passes step 2 (because you installed its CA), but fails step 3. The app sees a mismatch between the public key it expected and the one Burp presented, and kills the connection.
Pins are typically a SHA-256 hash of the certificate's public key (SubjectPublicKeyInfo), not the full certificate — this lets the server rotate its cert without changing the pin, as long as the key pair stays the same.
Detection⚓
Decompile with jadx and look for these patterns:
OkHttp CertificatePinner⚓
The most common pinning mechanism in Android apps. OkHttp has built-in pinning support.
CertificatePinner pinner = new CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
The string sha256/... is the Base64-encoded SHA-256 hash of the server's public key. When OkHttp receives a response, it hashes the cert's public key and compares it to this value. If it doesn't match, the request fails with a CertificatePinningException before any data is returned to the app.
Search jadx for: CertificatePinner, sha256/
Custom TrustManager⚓
A lower-level approach. The app implements X509TrustManager and overrides checkServerTrusted() to add its own logic.
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// hardcoded check — any non-empty body here is suspicious
for (X509Certificate cert : chain) {
if (!cert.getSubjectDN().toString().contains("example.com")) {
throw new CertificateException("Pinning failed");
}
}
}
checkServerTrusted is called by the TLS stack after the standard CA validation. The chain array is the full certificate chain the server presented, with chain[0] being the leaf (server) cert. If the method throws a CertificateException, the connection is aborted.
A legitimate default TrustManager would either call super.checkServerTrusted() or be empty — any custom logic checking cert values is pinning. Red flags: comparing issuer/subject strings, comparing public key bytes, computing hashes of certs in the chain.
Search jadx for: TrustManager, checkServerTrusted, X509TrustManager
Network Security Config (NSC)⚓
A declarative XML-based approach — no code required. Configured in res/xml/network_security_config.xml and referenced from AndroidManifest.xml.
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2026-01-01">
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- backup pin, required by Android if expiration is set -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
Android enforces the pin natively — no library or custom code is needed. The <domain-config> block applies to matching hostnames; includeSubdomains="true" covers all subdomains. The expiration attribute is optional but good practice (forces devs to rotate pins).
This is the easiest pinning type to detect (it's plaintext XML) and the easiest to bypass (just remove the <pin-set> block and repack the APK).
Search jadx for: pin-set, or look at res/xml/network_security_config.xml directly.
Certificate Fingerprint⚓
The fingerprint is a SHA-256 hash of the entire DER-encoded certificate. This is what you see in a browser's cert viewer under "SHA-256 Fingerprint", and what tools like keytool or certificate managers display for identification purposes.
# From a PEM file
openssl x509 -in cert.pem -noout -fingerprint -sha256
# From a DER file
openssl x509 -inform DER -in cert.der -noout -fingerprint -sha256
Output looks like:
When is this used for pinning? Rarely. Some custom TrustManager implementations compare the full certificate fingerprint rather than just the public key. If the hardcoded bytes in the app match the length and format of a full cert hash (32 bytes / 64 hex chars), it's likely pinning the whole cert rather than the key.
The downside of pinning the full cert: every time the server renews its certificate (even with the same key pair), the pin breaks. This is why most modern apps pin the public key instead.
Public Key Hash (Pin Value)⚓
The pin hash is a SHA-256 hash of the certificate's public key only — specifically the DER-encoded SubjectPublicKeyInfo (SPKI) structure, Base64-encoded. This is what sha256/... in OkHttp CertificatePinner and <pin digest="SHA-256"> in NSC represent.
Pinning the public key (rather than the whole cert) lets the server renew/rotate its certificate without the app breaking, as long as the underlying key pair doesn't change.
From a saved certificate file⚓
# From a PEM certificate
openssl x509 -in cert.pem -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64
# From a DER certificate
openssl x509 -inform DER -in cert.der -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64
The output is a Base64 string. Prefix it with sha256/ and it's a valid OkHttp CertificatePinner pin value, and also matches what you'd put in an NSC <pin> element.
From a live server (no file needed)⚓
# Pull the leaf cert and compute its public key hash in one pipeline
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64
-servername sends the SNI header — required for multi-domain servers. Without it you may get the wrong cert.
To see the full chain (useful when the app pins an intermediate CA rather than the leaf):
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null
Each -----BEGIN CERTIFICATE----- block is one cert — leaf first, root last. Save each block to a .pem file and run the hash pipeline on each to find which one matches the pin in the app.
Comparing a found pin to a cert⚓
If you extracted a sha256/AbCdEf...= string from the APK and want to confirm what it pins to:
# Run this on each cert in the chain
openssl x509 -in cert.pem -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64
# Match the output against the pin string found in the app
Bypasses⚓
Why User Certs Are Ignored by Default⚓
Android's Network Security Config (NSC) defines which certificate sources an app trusts. From API 24+, the default trust configuration changed:
| API level | Default <base-config> trust anchors |
|---|---|
| API ≤ 23 (Android ≤ 6) | system + user |
| API 24+ (Android 7+) | system only |
This means: even if you install Burp's CA as a user certificate via Settings → Security → Install certificate, apps targeting API 24+ won't trust it — the OS accepts it, but apps reject it at the TLS layer before returning any data.
A system certificate lives in /system/etc/security/cacerts/ and is trusted by all apps unconditionally. A user certificate is installed into a per-user store (/data/misc/user/0/cacerts-added/) and is only trusted if the app explicitly allows it in its NSC.
The NSC is either declared explicitly in res/xml/network_security_config.xml (referenced via android:networkSecurityConfig in the manifest) or falls back to the platform default. If no NSC file exists, Android applies the default trust policy for the app's targetSdkVersion.
| Method | Solves | Root needed? | APK repack? |
|---|---|---|---|
| System store push | CA trust (API 24+) | Yes | No |
| Magisk module | CA trust (API 24+) | Yes (Magisk) | No |
Frida ssl-kill-switch2 |
CA trust (API 24+) + certificate pinning | No (with frida-gadget) | No |
objection |
CA trust (API 24+) + certificate pinning | No (with frida-gadget) | No |
| Patch NSC — trust anchors | CA trust (API 24+) only | No | Yes |
| Patch NSC — pin-set | Certificate pinning (NSC-based) only | No | Yes |
| apktool smali patch | Certificate pinning (OkHttp / TrustManager) | No | Yes |
| Bundled cert / keystore swap | App bypasses system TLS stack entirely | No | Yes |
System Store Push (root required)⚓
Promotes Burp's CA into the OS system certificate store so all apps trust it unconditionally — no APK changes needed.
Use the root method from the Setup section to gain elevated access, then:
# Compute hash and convert DER → PEM (system store requires PEM format)
HASH=$(openssl x509 -inform DER -subject_hash_old -in cacert.der | head -1)
openssl x509 -inform DER -in cacert.der -out ${HASH}.0
adb root
adb remount
adb push ${HASH}.0 /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/${HASH}.0
adb reboot
After reboot the cert appears under System certificates and all apps trust it regardless of API level.
On Android 9 and below the system partition can be remounted read-write directly.
# 1. Compute the hash filename Android expects
HASH=$(openssl x509 -inform DER -subject_hash_old -in cacert.der | head -1)
# 2. Convert DER → PEM
openssl x509 -inform DER -in cacert.der -out ${HASH}.0
# 3. Push to SD card
adb push ${HASH}.0 /sdcard/
# 4. Root shell and copy into system store
adb shell
su
mount -o rw,remount /system
cp /sdcard/${HASH}.0 /system/etc/security/cacerts/
chmod 644 /system/etc/security/cacerts/${HASH}.0
exit
exit
# 5. Reboot
adb reboot
mount -o rw,remount /system fails on Android 10+ due to dm-verity. Instead, mount a tmpfs (in-memory writable filesystem) on top of the cacerts directory — the underlying /system partition stays untouched.
The cert lasts until the next reboot. Don't reboot or it's gone.
# On your host — prepare the cert file
HASH=$(openssl x509 -inform DER -subject_hash_old -in cacert.der | head -1)
openssl x509 -inform DER -in cacert.der -out ${HASH}.0
adb push ${HASH}.0 /sdcard/${HASH}.0
Then enter a root shell on the device:
Run each line one at a time:
cp -r /system/etc/security/cacerts /data/local/tmp/cacerts_bak
mount -t tmpfs tmpfs /system/etc/security/cacerts
cp /data/local/tmp/cacerts_bak/* /system/etc/security/cacerts/
cp /sdcard/<HASH>.0 /system/etc/security/cacerts/<HASH>.0
chmod 644 /system/etc/security/cacerts/<HASH>.0
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/<HASH>.0
killall zygote zygote64
Replace <HASH> with the value from the HASH= command above. killall restarts the Android framework so it re-reads the cert store — no reboot needed.
On Android 14+, the cert store moved into the Conscrypt APEX module at /apex/com.android.conscrypt/cacerts/. Mounting over /system/etc/security/cacerts/ has no effect — the APEX path takes precedence.
The APEX path only exists inside specific Linux mount namespaces (zygote and system_server). You need to mount the tmpfs inside both of those namespaces. The cert lasts until the next reboot.
Root namespace system_server namespace zygote namespace
/apex/com.android.conscrypt/ /apex/com.android.conscrypt/ /apex/com.android.conscrypt/
cacerts/ ← original cacerts/ ← mount tmpfs here cacerts/ ← mount tmpfs here
(Settings reads from here) (apps inherit from here)
# On your host — prepare the cert file
HASH=$(openssl x509 -inform DER -subject_hash_old -in cacert.der | head -1)
openssl x509 -inform DER -in cacert.der -out ${HASH}.0
adb push ${HASH}.0 /sdcard/${HASH}.0
Then enter a root shell:
Step 1 — Mount in zygote's namespace (new apps forked from zygote will trust the cert):
nsenter --mount=/proc/$(pidof zygote64)/ns/mnt sh
mount -t tmpfs tmpfs /apex/com.android.conscrypt/cacerts
cp /system/etc/security/cacerts/* /apex/com.android.conscrypt/cacerts/
cp /sdcard/<HASH>.0 /apex/com.android.conscrypt/cacerts/<HASH>.0
chmod 644 /apex/com.android.conscrypt/cacerts/<HASH>.0
chcon u:object_r:system_file:s0 /apex/com.android.conscrypt/cacerts/<HASH>.0
exit
Step 2 — Mount in system_server's namespace (Settings reads from here):
nsenter --mount=/proc/$(pidof system_server)/ns/mnt sh
mount -t tmpfs tmpfs /apex/com.android.conscrypt/cacerts
cp /system/etc/security/cacerts/* /apex/com.android.conscrypt/cacerts/
cp /sdcard/<HASH>.0 /apex/com.android.conscrypt/cacerts/<HASH>.0
chmod 644 /apex/com.android.conscrypt/cacerts/<HASH>.0
chcon u:object_r:system_file:s0 /apex/com.android.conscrypt/cacerts/<HASH>.0
exit
Step 3 — Force Settings to reload:
Reopen Settings → Security → Trusted credentials → System — your cert should appear.
Warning
Do not run killall zygote zygote64 after setting this up. Killing zygote respawns it with a fresh namespace, wiping the tmpfs mount you just set up. If you accidentally kill zygote, you need to redo Steps 1 and 2 with the new zygote PID.
Magisk Module — Always Trust User Certs⚓
The "Always Trust User Certs" Magisk module copies every user-installed certificate into the system store at boot — without touching the system partition directly. This is the preferred method for physical devices on Android 10+ where remounting /system fails.
Can it be used on an emulator? Technically yes if Magisk is installed on the AVD, but for emulators use the root method from the Setup section and the System Store Push above instead — it's simpler.
-
Install the Burp CA as a user cert:
On device: Settings → Security → Install from storage → CA Certificate → select
cacert.der. -
Download the module ZIP from
https://github.com/NVISOsecurity/MagiskTrustUserCerts/releases -
In the Magisk app: Modules → Install from storage → select the ZIP → flash it.
-
Reboot the device.
-
Verify: Settings → Security → Trusted credentials → System — your Burp CA should appear there.
Tip
This method survives app updates and reinstalls — unlike patching the NSC, you don't need to re-patch when the app updates.
Frida / objection (runtime hook — preferred)⚓
Patches the running process in memory. Nothing is written to disk permanently. Covers two distinct problems:
- API 24+ CA trust — hooks the TLS stack so the app accepts any CA, including Burp's user-installed one
- Certificate pinning — hooks OkHttp
CertificatePinner, customTrustManager, and some native SSL pinning
In most cases you only need one tool for both.
# objection: attach to a running app and disable pinning (also bypasses CA trust checks)
objection -g com.example.app explore
android sslpinning disable
objection loads Frida, injects into the process, and hooks all known pinning and CA validation sinks. After running this command, trigger the action in the app that makes the network request — traffic should now appear in Burp.
Tip
If the app launches and pins before you can attach, use the --startup-command flag or frida-gadget embedded in the APK to hook before the first network call.
Patch NSC — trust anchors (API 24+ CA trust, no root)⚓
The app targets API 24+ and ignores user-installed CAs by default. Patching the NSC <trust-anchors> tells the app to also accept user certs — no root needed, but requires an APK repack.
Edit (or create) decoded/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
Ensure AndroidManifest.xml references it:
Rebuild, sign, and reinstall — see Smali Patching.
Note
This only fixes the CA trust problem. If the app also does certificate pinning, you still need to address the <pin-set> and/or code-level pinning separately.
Bundled Certificate or Keystore Replacement (apktool)⚓
Some apps don't pin via code or NSC at all — they ship their own trust store inside the APK and load it programmatically, bypassing Android's standard TLS stack entirely. The two common forms:
- Bundled DER/PEM certificate — a raw cert file in
assets/orres/raw/loaded withCertificateFactoryand fed into a customTrustManagerorSSLContext - Bundled keystore — a
.bks(BouncyCastle) or.p12/.pfxfile inassets/orres/raw/loaded withKeyStore.getInstance("BKS"), containing one or more trusted CA certs
When the app builds an SSLContext from its own keystore, it doesn't use Android's system or user trust stores at all — so installing the Burp CA system-wide has no effect.
Identify the pattern in jadx:
// Bundled cert — look for CertificateFactory + InputStream from assets/raw
InputStream is = context.getAssets().open("server.der");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate ca = cf.generateCertificate(is);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("ca", ca);
// Bundled keystore — look for KeyStore.load() with an InputStream from assets/raw
KeyStore ks = KeyStore.getInstance("BKS");
InputStream is = context.getResources().openRawResource(R.raw.my_keystore);
ks.load(is, "keystorepassword".toCharArray());
Search jadx for: getAssets().open, openRawResource, KeyStore.getInstance("BKS"), CertificateFactory
Step 1: Decompile and locate the bundled file
apktool d app.apk -o decoded/
# Look for cert/keystore files in assets and res/raw
find decoded/assets decoded/res/raw -type f | grep -E "\.(der|pem|crt|bks|p12|pfx|keystore)"
Step 2: Replace the bundled file
Convert Burp's CA to the same format the app uses and drop it in place:
# Replace with DER (most common for bundled certs)
cp cacert.der decoded/assets/server.der
# If the app loads PEM, convert first
openssl x509 -inform DER -in cacert.der -out decoded/assets/server.pem
Then also patch the NSC to trust user certs, so if any other code path runs standard TLS, Burp's CA is trusted there too:
A BKS keystore is a BouncyCastle format keystore. You need to replace its contents with Burp's CA, keeping the same filename and password (the password is usually a hardcoded string — find it in the smali near the ks.load() call).
Install BouncyCastle if you don't have it:
# Download bcprov jar (needed for keytool to handle BKS format)
# https://www.bouncycastle.org/download/bouncy-castle-java/
Convert Burp's CA DER to PEM, then import into a new BKS keystore with the same password:
# Convert Burp CA to PEM
openssl x509 -inform DER -in cacert.der -out burp_ca.pem
# Create a new BKS keystore containing only Burp's CA
# Replace 'keystorepassword' with the password found in smali
keytool -importcert \
-alias burpca \
-file burp_ca.pem \
-keystore decoded/res/raw/my_keystore.bks \
-storetype BKS \
-providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
-providerpath /path/to/bcprov-jdk18on-xxx.jar \
-storepass keystorepassword \
-noprompt
Note
If the keystore already exists, keytool -importcert will add your cert to it. To start fresh (remove the original server cert), delete the file first and let keytool create a new one, or use keytool -delete to remove the existing alias before importing.
Then patch the NSC as above so standard TLS paths also trust Burp's CA.
Step 3: Rebuild, sign, and reinstall
See Smali Patching for the full rebuild and signing steps.
Manual patch (apktool) — hardcoded pin values⚓
Use when objection can't hook the pinning (e.g. native library, custom obfuscation). Rather than deleting pinning logic outright — which can trigger tamper detection — replace the hardcoded pin values with a hash computed from your Burp CA. The pinning code still runs, it just accepts Burp's cert instead of the real server's.
Step 1: Compute your Burp CA's public key hash
Export Burp's CA in DER format (Burp → Proxy → Proxy Settings → Import/Export CA Certificate → Export Certificate in DER format → cacert.der), then:
openssl x509 -inform DER -in cacert.der -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64
Copy the output — this is your replacement pin.
Step 2: Decompile and replace the pin
Find the .add() call in smali (search for the sha256/ string in decoded/):
Open the matching smali file. The pin string will appear as a const-string before the .add() call, e.g.:
Option A — Replace with your Burp CA hash (preferred):
Keep the sha256/ prefix and swap the Base64 value. The pinning check still runs normally, it just accepts Burp's cert:
Option B — Remove the .add() call entirely:
In smali, find the invoke-virtual that corresponds to .add("host", "sha256/...") and delete it along with its associated const-string lines. Without any pins registered, CertificatePinner accepts all certs. Only use this if the app doesn't verify the builder state — some apps check at startup that a pinner is configured and crash if it's empty.
Open decoded/res/xml/network_security_config.xml.
Option A — Replace the pin value with your Burp CA hash (preferred):
The hash here is raw Base64 — no sha256/ prefix.
Option B — Delete the <pin-set> block entirely:
Remove the <pin-set>...</pin-set> element from the <domain-config>. Without it, Android falls back to standard CA validation for that domain — no pinning is enforced. This is safe to do for NSC since Android won't crash if the block is absent.
If checkServerTrusted compares raw bytes or a fingerprint string, find the hardcoded value in smali.
Option A — Replace with your Burp CA hash (preferred):
Compute the matching value from your Burp CA and substitute the const-string in smali:
For a full-cert fingerprint (SHA-256 of the whole cert):
For a public key hash (SubjectPublicKeyInfo):
openssl x509 -inform DER -in cacert.der -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| openssl base64
Replace the matching const-string in smali with the computed value.
Option B — Remove the method body:
Delete the contents of checkServerTrusted in smali, leaving only a return-void. Android's TLS stack won't throw and will accept any cert chain. More detectable than replacing the value since the method now does nothing, but simpler if the comparison logic is complex or obfuscated.
Step 3: Rebuild, sign, and reinstall
See Smali Patching for the full rebuild and signing steps.
Using Burp to Test⚓
Once traffic is flowing into Burp, this is how you actually use it to find and exploit issues.
Intercept Requests⚓
In Burp: Proxy → Intercept → Intercept is on
Every request from the device pauses here before being forwarded. You can:
- Read the full request — headers, body, cookies, tokens
- Modify any value — change a user ID, price, role, status flag
- Drop the request entirely
- Click Forward to send it on
Tip
Turn intercept off most of the time and use HTTP history instead — intercepting every request is noisy. Only turn it on when you want to catch and modify a specific action.
HTTP History⚓
Proxy → HTTP history — a log of every request/response that went through Burp.
- Right-click any request → Send to Repeater to replay and modify it
- Right-click → Send to Intruder for fuzzing
- Filter by host to focus on the target app's API
Repeater — Replay and Modify Requests⚓
Repeater lets you tweak a captured request and resend it as many times as you want without triggering the action in the app again.
Common things to test in Repeater:
- Change a numeric ID in the URL:
/api/user/1337→/api/user/1(IDOR) - Remove or tamper with an
Authorizationheader - Change a request body field:
"role":"user"→"role":"admin" - Change
"price":99.99→"price":0.01 - Remove a parameter entirely to see how the server handles it
- Add unexpected fields to a JSON body
What to Look For⚓
| Finding | What to check in Burp |
|---|---|
| IDOR | Is a user/resource ID in the URL or body? Try another user's ID |
| Auth bypass | Remove the Authorization / session cookie — does the server still respond? |
| Broken access control | Change a role/privilege field in the request body |
| Sensitive data exposure | Read the responses — tokens, keys, PII in plaintext? |
| Parameter tampering | Modify prices, quantities, status flags |
| JWT issues | Decode the token in Proxy → HTTP history, send to Decoder or jwt.io |
| Mass assignment | Add extra fields to POST/PATCH body the server shouldn't accept |
Non-HTTP Traffic⚓
For traffic that doesn't go through the HTTP proxy (e.g. raw TCP, UDP):
# Capture on device
adb shell tcpdump -i any -w /sdcard/capture.pcap
# Pull and open in Wireshark
adb pull /sdcard/capture.pcap
Troubleshooting⚓
| Symptom | Likely cause | Fix |
|---|---|---|
| No traffic in Burp at all | Proxy not set on device, or wrong IP/port | Verify Step 2-3; check adb shell settings get global http_proxy |
| Request goes through but Burp shows nothing / connection resets | App is using HTTP/2 and Burp isn't downgrading it | In Burp: Proxy → Proxy Settings → your listener → HTTP/2 → uncheck "Allow HTTP/2 ALPN". Forces Burp to negotiate HTTP/1.1, which it handles fully |
| Browser works, app doesn't | App ignores system proxy | Use iptables transparent proxy (requires root), or Drony / ProxyDroid |
| SSL error in app | Burp CA not installed / not trusted by API 24+ app | Install cert + apply one of the API 24+ options above |
adb root fails on emulator |
Using a Google Play AVD | Switch to a Google APIs AVD |
mount -o rw,remount /system fails |
Android 10+ read-only overlay | Use tmpfs overlay method for Android 10–13, or nsenter + APEX method for Android 14+ |
| App crashes after patching NSC | Manifest not referencing the XML, or XML syntax error | Check AndroidManifest.xml has android:networkSecurityConfig attribute |