
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.
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 disableOr on iOS:
> ios sslpinning disableThis handles, in my experience, about 70 percent of real-world apps. It hooks the common implementations:
Android:
OkHttpClientCertificatePinnerTrustManagerImpl.verifyChainX509TrustManager.checkServerTrustedWebViewClient.onReceivedSslErrorConscrypt'sConscryptEngine
iOS:
SSLSetSessionOptioncallbackNSURLSessiondidReceiveChallengedelegateAFNetworkingAFSecurityPolicyTrustKit
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.sofiles
iOS
- Run
class-dumpand search for class names containingPin,Trust,Security - Look for
URLSession:didReceiveChallenge:completionHandler:implementations - Check for
TrustKitinitialization in the binary strings - Look for
SSLSetSessionOptioncalls 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.sois a custom build- The app loads
libcustom_crypto.soor similar - Strings in the
.soreference your domain orBoringSSL
Approaches:
- Find the native function with
frida-trace -i "ssl_*" -i "*verify*" com.target.appand pick out what looks relevant - Hook
SSL_get_verify_resultorX509_verify_certfrom libssl - 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:
- Hook the platform's
NetworkSecurityConfigclass with Frida - Patch the XML and rebuild the APK with apktool
- 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-pauseIf 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.
Related Notes
Last updated on
Objection
Objection is a runtime mobile exploration toolkit built on Frida. It packages most of the boring stuff into a REPL, SSL pinning bypass, root detection bypass, IPC enumeration, file dumping, all without writing a hook.
Network Protocol Attacks
Comprehensive guides to exploiting network protocols including SMB, FTP, SSH, RDP, DNS, SMTP, and WebDAV for penetration testing and security assessments.