Skip to content

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 interfaces

    You can bind to just your LAN IP (192.168.x.x) if you prefer, but binding to All 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:

# Linux / macOS
ip addr show | grep "inet 192"

# Windows
ipconfig | findstr "192"

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:

Settings → Wi-Fi → Long-press your network → Modify → Advanced → Manual proxy
Host: 192.168.x.x   ← your LAN IP from Step 2
Port: 8080

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.

adb push cacert.der /sdcard/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.

adb push cacert.der /sdcard/cacert.der

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:

  1. Open a browser on the device/emulator and visit any HTTP site (e.g. http://example.com)
  2. In Burp, go to Proxy → HTTP history — you should see the request appear
  3. 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
adb push redirect.sh /data/local/tmp/redirect.sh
adb shell chmod +x /data/local/tmp/redirect.sh

3. Apply the Rules as Root

adb shell
su
sh /data/local/tmp/redirect.sh

4. Verify

Apps that previously showed no traffic in Burp should now appear in Proxy → HTTP history.

5. Clean Up When Done

iptables -t nat -F OUTPUT

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, iptables rules set in a root shell may only apply to the default network namespace — use ip netns exec if 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:

  1. Server presents a certificate
  2. Android checks: is this cert signed by a trusted CA? → if yes, allow

With pinning, an extra step is added by the app:

  1. Server presents a certificate
  2. Android: is this cert signed by a trusted CA? → yes
  3. 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:

SHA256 Fingerprint=2B:59:2A:F5:AB:CD:...

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:

adb shell
su

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:

adb shell
su

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:

am force-stop com.android.settings

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.

  1. Install the Burp CA as a user cert:

    adb push cacert.der /sdcard/cacert.der
    

    On device: Settings → Security → Install from storage → CA Certificate → select cacert.der.

  2. Download the module ZIP from https://github.com/NVISOsecurity/MagiskTrustUserCerts/releases

  3. In the Magisk app: Modules → Install from storage → select the ZIP → flash it.

  4. Reboot the device.

  5. 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, custom TrustManager, 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.

apktool d app.apk -o decoded/

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:

<application android:networkSecurityConfig="@xml/network_security_config" ...>

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/ or res/raw/ loaded with CertificateFactory and fed into a custom TrustManager or SSLContext
  • Bundled keystore — a .bks (BouncyCastle) or .p12/.pfx file in assets/ or res/raw/ loaded with KeyStore.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:

<!-- decoded/res/xml/network_security_config.xml -->
<base-config cleartextTrafficPermitted="true">
    <trust-anchors>
        <certificates src="system" />
        <certificates src="user" />
    </trust-anchors>
</base-config>

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 formatcacert.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

apktool d app.apk -o decoded/

Find the .add() call in smali (search for the sha256/ string in decoded/):

grep -r "sha256/" decoded/

Open the matching smali file. The pin string will appear as a const-string before the .add() call, e.g.:

const-string v1, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

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:

const-string v1, "sha256/<your-burp-ca-hash-here>"

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):

<pin-set>
    <pin digest="SHA-256"><your-burp-ca-hash-here></pin>
</pin-set>

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.

<domain-config>
    <domain includeSubdomains="true">api.example.com</domain>
    <!-- pin-set removed -->
</domain-config>

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):

openssl x509 -inform DER -in cacert.der -noout -fingerprint -sha256

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 Authorization header
  • 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