Press enter or click to view image in full size
Three days. That’s how long it took me to get Burp Suite seeing traffic from a Flutter app during a security assessment.
I tried everything I knew. Objection. ReFlutter, which actually patches the Flutter binary itself. Custom CA installation. VPN-based interception. Standard Frida SSL bypass scripts from GitHub. Each one either failed silently or gave me the exact same result: app opens, appears to load, shows “no internet.” Not an SSL error. Not a certificate warning. Just “no internet,” like the proxy wasn’t even there.
At some point I stopped trying individual tools and started asking a different question: what is actually happening at each layer, and why is patching one thing not enough? That’s when things started making sense.
What eventually worked wasn’t a new tool or a clever trick. It was running all the right hooks at the same time, covering every SSL validation path the app could be using. I put those together into two scripts. That’s what this post is about.
Why Flutter Makes This Harder Than It Should Be
Most Android SSL bypass guides assume Java or Kotlin. Flutter is built differently.
Flutter ships its own TLS implementation, BoringSSL, compiled directly into libflutter.so. It has nothing to do with Android's certificate trust chain. Installing Burp's CA through Settings, the first thing every guide tells you to do, has zero effect on Flutter's networking. The app just doesn't use that trust store.
That’s the first problem. The second one is subtler. Even if you patch Flutter’s TLS correctly, some apps run a connectivity check through a Java or WebView layer before Flutter even initializes. That check goes through Android’s certificate chain, which on API 24 and above won’t trust user-installed CAs. So the Flutter bypass works, the Flutter layer is satisfied, and the app still shows “no internet” because the Java layer already rejected the connection a few milliseconds earlier.
This is exactly why ReFlutter wasn’t enough. It patches the Flutter binary, full stop. If anything is happening outside Flutter, in Java or WebView, ReFlutter never touches it.
Covering one layer doesn’t work. You have to cover all of them.
The Scripts
Script 1: disable-flutter-tls-v1.js
This one handles the Flutter TLS layer.
Flutter’s BoringSSL has a function called ssl_verify_peer_cert in handshake.cc that does the actual peer certificate verification. The script finds this function in memory using byte pattern matching. It has patterns for arm64, arm, x64, and x86 across both Android and iOS. Once it finds the function, it replaces the implementation with one that always returns 0, meaning every certificate passes without any check.
function hook_ssl_verify_peer_cert(address) {
Interceptor.replace(address, new NativeCallback((pathPtr, flags) => {
return 0;
}, 'int', ['pointer', 'int']));
}There’s a timing problem that causes silent failures on a lot of devices. Frida attaches to the process before libflutter.so finishes loading. Pattern matching runs, finds nothing, exits cleanly, and you see no error, but the bypass never actually happened. The script handles this by retrying up to five times with a one-second delay between attempts. Once the library is found, the retry counter resets so the pattern search also gets its full number of attempts.
Script 2: universal_bypass.js
This one covers everything in the Java layer, outside Flutter’s Dart runtime.
X509TrustManager is Android’s standard interface for certificate validation. The script registers a custom implementation where checkClientTrusted, checkServerTrusted, and getAcceptedIssuers are all empty. No certificate chain ever gets checked.
var TrustManager = Java.registerClass({
name: 'com.burp.bypass.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() { return []; }
}
});SSLContext.init() gets hooked so that every SSL context created anywhere in the app, including inside third-party libraries, gets the bypass trust manager injected into it at initialization time.
HostnameVerifier is hooked to return true for every hostname. Some apps validate the server hostname as a completely separate step from certificate validation. Without this, you can pass the certificate check and still get blocked.
WebViewClient.onReceivedSslError calls handler.proceed() instead of showing an error page. Without this, any WebView inside the app will just stop loading when Burp intercepts the connection.
InAppWebViewClient is the hook that was missing from every existing script I found. Apps using the flutter_inappwebview plugin register their own WebView client subclass at com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewClient. Hooking the parent WebViewClient class does nothing for this subclass. You have to hook it by its full name specifically.
try {
var InAppWebViewClient = Java.use('com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewClient');
InAppWebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
handler.proceed();
};
} catch(e) {
console.log("[-] InAppWebView not present: " + e);
}The try/catch exists because if the app doesn’t use flutter_inappwebview, that class simply doesn’t exist. A bare Java.use() call on a missing class crashes the entire script before any other hook runs. The catch keeps everything else alive.
Running both scripts
frida -U -f com.your.app.package -l ./disable-flutter-tls-v1.js -l ./universal_bypass.jsIf the app freezes after launch, type %resume in the Frida REPL to unpause it. If you attached to an already running process, skip this — there's nothing to resume.
Watch the output. The Flutter script will log which byte pattern matched and at what address. The Java script logs each hook as it fires. Traffic should start showing up in Burp within a few seconds of the app making its first network request.
On most Flutter apps I’ve tested, this is enough. Try it before going any further.
When the Scripts Are Not Enough
A small number of apps ignore system routing or have extra checks at the network level. For those, you need the traffic path set up correctly underneath the scripts.
Burp Invisible Proxy
When iptables redirects traffic to Burp, the app sends raw TCP directly, no CONNECT handshake. Without invisible proxy mode, Burp doesn’t know how to handle this and logs “Client request violates HTTP protocol” while showing nothing in Intercept.
Get Iamarbaz’s stories in your inbox
Join Medium for free to get updates from this writer.
Go to Proxy, then Proxy Listeners, select your listener, click Edit, go to Request handling, and enable Support invisible proxying. Leave Redirect to host empty.
Also worth checking: make sure Burp is actually binding to all interfaces.
netstat -an | grep 8080
# 0.0.0.0:8080 is correct. 127.0.0.1:8080 means the emulator can't reach it.iptables Traffic Redirection
Flutter apps make direct TCP connections and ignore Android’s proxy settings entirely. iptables handles the redirect at the kernel level, rewriting the destination address before the packet leaves the device.
adb reverse --remove-all
adb shell su -c 'iptables -t nat -F'adb shell su -c 'iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination 10.0.2.2:8080'
adb shell su -c 'iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination 10.0.2.2:8080'adb shell su -c 'iptables -t nat -L -n -v'
Always flush before adding rules. Duplicate DNAT entries stack silently and the routing behavior they produce is not obvious.
Burp’s CA as a System Certificate
If there’s Java-level certificate validation that Frida doesn’t catch in time, Burp’s CA needs to be in the system store. User-installed certificates are ignored on API 24 and above. The bind mount approach gets around the read-only partition:
openssl x509 -inform DER -in cacert.der -out cacert.pem
openssl x509 -inform PEM -subject_hash_old -in cacert.pem | head -1
# e.g. 9a5ba575, so the file must be named 9a5ba575.0adb shell su -c 'cp -a /system/etc/security/cacerts /data/local/tmp/system_cacerts'
adb push cacert.pem /data/local/tmp/system_cacerts/
adb shell su -c 'mv /data/local/tmp/system_cacerts/cacert.pem /data/local/tmp/system_cacerts/9a5ba575.0'
adb shell su -c 'chmod 644 /data/local/tmp/system_cacerts/9a5ba575.0'
adb shell su -c 'mount -o bind /data/local/tmp/system_cacerts /system/etc/security/cacerts'DNSChef: When iptables Still Isn’t Enough
On a handful of apps, even the full iptables setup wasn’t getting traffic into Burp. The tell was tcpdump: packets were leaving the device on port 443 going somewhere other than Burp. The app was doing something at the network level that DNAT wasn’t catching.
DNSChef takes a completely different approach. Instead of redirecting traffic after DNS resolution happens, you intercept the DNS resolution itself. Point the device’s DNS server at your machine, run DNSChef to answer every query with your IP, and the app’s traffic arrives at Burp before iptables even has to act.
adb shell settings put global dns1 <your-machine-ip>
adb shell settings put global dns2 <your-machine-ip>sudo python dnschef.py --fakeip <your-machine-ip> --interface <your-machine-ip>Then run the Frida scripts on top:
frida -U -f com.your.app.package -l ./disable-flutter-tls-v1.js -l ./universal_bypass.jsIf the app freezes, type %resume in the REPL. Skip it if you attached to a running process.
With everything running, the app resolves its backend domain and gets your machine’s IP back. It sends HTTPS there. Burp picks it up in invisible proxy mode. Frida has already patched TLS validation so the app accepts Burp’s certificate. The app has no visibility into any of it.
I’ve needed this combination maybe twice. Both times tcpdump was what showed me why iptables alone wasn’t doing it.
If Things Still Aren’t Working
Check the Burp Event Log before assuming the scripts failed. “Failed to negotiate TLS connection” means the CA isn’t trusted, so run the scripts or use the bind mount. “Client request violates HTTP protocol” means invisible proxy isn’t on. If the Event Log shows nothing at all, traffic isn’t reaching Burp, and tcpdump will tell you where it’s actually going.
adb shell su -c 'tcpdump -i any -n port 443'To Wrap Up
The two scripts cover the vast majority of Flutter apps on their own. The InAppWebViewClient hook is the part that was missing from everything else I found. If you’ve tried other bypass scripts and WebView traffic is still getting blocked, that subclass hook is probably why they didn’t work.
The rest of this post, iptables, bind mount, DNSChef, exists for edge cases. I needed those setups occasionally. You might not need them at all.
I spent three days on this. Hopefully you won’t have to.
Credits and Prior Work
These scripts are a compilation. The Flutter TLS patch comes from NVISOsecurity’s disable-flutter-tls-verification project. The Java-layer hooks — X509TrustManager, SSLContext, HostnameVerifier, WebViewClient — are standard Frida patterns that have been around for years and exist in various forms across dozens of public scripts. The InAppWebViewClient hook is the one addition I put together after hitting that specific problem myself and not finding it handled anywhere else.
I collected what was scattered, tested what actually worked, and put it in two files. That’s it.