SSL Pinning Bypass

SSL Pinning Bypass

Techniques for getting your proxy in the middle of pinned HTTPS traffic on Android and iOS, from Objection one-liners through custom Frida hooks for the cases that fight back.

May 20, 2026
Updated Apr 8, 2026
2 min read

Why Pinning Exists

SSL pinning is the developer's attempt to prevent exactly what you are trying to do. The app ships with a copy of the expected certificate or public key, and refuses to talk to a server that does not match. A user-installed CA in the system store is irrelevant if the app does its own pinning check.

There are several pinning libraries and a dozen ways to roll your own. Most apps use one of the common ones. Some apps roll their own and obfuscate it. Both are bypassable, the first takes thirty seconds, the second takes an afternoon.

The First Thing I Try

objection -g com.target.app explore
> android sslpinning disable

Or on iOS:

> ios sslpinning disable

This handles, in my experience, about 70 percent of real-world apps. It hooks the common implementations:

Android:

  • OkHttpClient CertificatePinner
  • TrustManagerImpl.verifyChain
  • X509TrustManager.checkServerTrusted
  • WebViewClient.onReceivedSslError
  • Conscrypt's ConscryptEngine

iOS:

  • SSLSetSessionOption callback
  • NSURLSession didReceiveChallenge delegate
  • AFNetworking AFSecurityPolicy
  • TrustKit

If Objection works, move on. If it does not, dig in.

Identifying the Pinning Method

When Objection fails, the next step is figuring out what the app actually uses. A few hints:

Android

  • Search the decompiled code in jadx for CertificatePinner, X509TrustManager, checkServerTrusted, OkHttpClient.Builder
  • Look at network_security_config.xml (referenced from the manifest)
  • Check for com.datatheorem.android.trustkit (TrustKit Android)
  • Search for native libraries, libssl, libcrypto, custom pinning in .so files

iOS

  • Run class-dump and search for class names containing Pin, Trust, Security
  • Look for URLSession:didReceiveChallenge:completionHandler: implementations
  • Check for TrustKit initialization in the binary strings
  • Look for SSLSetSessionOption calls in disassembly

Frida Hooks for Stubborn Pinning

When the canned bypass does not work, write the hook by hand. A few patterns I keep around.

Android, OkHttp custom CertificatePinner

Java.perform(function () {
  var CertificatePinner = Java.use('okhttp3.CertificatePinner');
  CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (hostname, peerCertificates) {
    console.log('[+] OkHttp CertificatePinner.check bypassed for ' + hostname);
    return;
  };
});

Android, custom X509TrustManager

Java.perform(function () {
  var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
  TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
    console.log('[+] verifyChain bypassed for ' + host);
    return untrustedChain;
  };
});

Android, hook every TrustManager

Java.perform(function () {
  var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
  var SSLContext = Java.use('javax.net.ssl.SSLContext');

  // Build a trust manager that trusts everything
  var TrustManager = Java.registerClass({
    name: 'com.example.TrustEverything',
    implements: [X509TrustManager],
    methods: {
      checkClientTrusted: function (chain, authType) {},
      checkServerTrusted: function (chain, authType) {},
      getAcceptedIssuers: function () { return []; }
    }
  });

  var TrustManagers = [TrustManager.$new()];
  var SSLContextInit = SSLContext.init.overload(
    '[Ljavax.net.ssl.KeyManager;',
    '[Ljavax.net.ssl.TrustManager;',
    'java.security.SecureRandom'
  );
  SSLContextInit.implementation = function (km, tm, sr) {
    console.log('[+] SSLContext.init replacing trust managers');
    SSLContextInit.call(this, km, TrustManagers, sr);
  };
});

iOS, AFNetworking AFSecurityPolicy

if (ObjC.available) {
  var AFSecurityPolicy = ObjC.classes.AFSecurityPolicy;
  Interceptor.attach(AFSecurityPolicy['- evaluateServerTrust:forDomain:'].implementation, {
    onLeave: function (retval) {
      console.log('[+] AFSecurityPolicy bypass');
      retval.replace(0x1);
    }
  });
}

iOS, NSURLSession delegate

if (ObjC.available) {
  var resolver = new ApiResolver('objc');
  var matches = resolver.enumerateMatches('-[* URLSession:didReceiveChallenge:completionHandler:]');

  matches.forEach(function (m) {
    Interceptor.attach(m.address, {
      onEnter: function (args) {
        console.log('[+] Intercepting ' + m.name);
        var completionHandler = new ObjC.Block(args[4]);
        var originalImpl = completionHandler.implementation;
        completionHandler.implementation = function (disposition, credential) {
          // 0 = NSURLSessionAuthChallengeUseCredential
          // Build a credential that trusts everything
          var challenge = new ObjC.Object(args[3]);
          var trust = challenge.protectionSpace().serverTrust();
          var newCred = ObjC.classes.NSURLCredential.credentialForTrust_(trust);
          originalImpl(0, newCred);
        };
      }
    });
  });
}

When Pinning Lives in Native Code

A few apps, mostly banking and high-value targets, push pinning into a native library. Common signs:

  • libssl.so is a custom build
  • The app loads libcustom_crypto.so or similar
  • Strings in the .so reference your domain or BoringSSL

Approaches:

  1. Find the native function with frida-trace -i "ssl_*" -i "*verify*" com.target.app and pick out what looks relevant
  2. Hook SSL_get_verify_result or X509_verify_cert from libssl
  3. Patch the binary itself if hooking is too noisy
// Hook BoringSSL's verify function
Interceptor.attach(Module.getExportByName('libssl.so', 'SSL_get_verify_result'), {
  onLeave: function (retval) {
    console.log('[+] SSL_get_verify_result returned ' + retval + ', forcing 0');
    retval.replace(0);
  }
});

Network Security Config (Android Only)

Modern Android apps can declare pinning in res/xml/network_security_config.xml:

<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">api.target.com</domain>
    <pin-set>
      <pin digest="SHA-256">base64hash==</pin>
    </pin-set>
  </domain-config>
</network-security-config>

This is enforced by the platform, not by application code. To bypass it, either:

  1. Hook the platform's NetworkSecurityConfig class with Frida
  2. Patch the XML and rebuild the APK with apktool
  3. Use the system trust store bypass that Objection ships, which handles this case

Verifying the Bypass

After running the hook, watch for traffic in Burp:

# Start the app fresh through Frida
frida -U -f com.target.app -l bypass.js --no-pause

If you see traffic in Burp, you are done. If the app silently fails or crashes, the bypass missed something. Check logcat or Console.app for SSL errors and adjust.

Last updated on