August 24, 2026
I WATCHED AN APP LIE TO ME — AND FOUND THE BUG NO SCANNER WOULD EVER SEE
It was 2 a.m. and the payment request was right there on my screen, captured byte for byte:

By CypherNova1337
7 min read
I WATCHED AN APP LIE TO ME — AND FOUND THE BUG NO SCANNER WOULD EVER SEE
{"asset":"USD","amount":"10","method":"VendorPay (Deposits)", ...}
The documentation said the method enum was "card". The app's own UI implied "card". Every automated scanner on the market would have fuzzed "card" until the heat death of the universe and learned nothing. The real identifier — the one the server actually accepted — was VendorPay (Deposits).
I found it in one evening with a tool that every security professional has heard of and almost nobody uses properly: MITMPROXY.
This is a story about that evening, and about everything the tool taught me on the way. It's also an argument — because after watching this play out on real engagements again and again, I'm convinced interception is the most overlooked skill in security.
PART 1 — THE HOOK: ONE REQUEST THE DOCS NEVER MENTIONED
The target was a major exchange's mobile app. The goal was mapping a deposit flow — the classic what does the client really send problem. The documentation covered the public API. The app, of course, didn't use the public API.
You cannot ask a server which undocumented endpoint handles card deposits? and expect an answer. You cannot grep documentation that doesn't exist. But you can watch the app work. So I needed the app's traffic in front of me.
That's where the first wall appeared, and it's the wall that stops most people permanently: the app simply WOULD NOT TALK TO MY PROXY.
PART 2 — BASICS UNDER FIRE: THE CA, THE PHONE, AND THE TYPO
Let me compress what you already know: install mitmproxy, set your device's proxy to your machine's IP on port 8080, visit mitm.it, install the CA certificate. Browsers trust it. Done.
Unless, of course:
- You typo the port in the phone's Wi-Fi proxy settings (880 instead of 8080), and spend forty minutes debugging a proxy that is actually listening on the right port while the phone points at a dead one.
- You fix the port but Android's network stack keeps using the STALE cached value until the Wi-Fi reconnects.
- Your host firewall silently drops inbound TCP while ICMP happily flows, so every ping test lies to you about connectivity.
Every one of those happened, in order, on the same night. The lesson isn't I'm an idiot (though the typo was on me). The lesson is that THE SETUP IS WHERE THE TOOL LOSES 90% OF ITS WOULD-BE USERS. The debugging loop is: is the proxy listening? Is the device pointing at it? Does traffic arrive? Does the client trust the CA? Four questions, each with five failure modes per platform.
Here's the trick that finally made the phone deterministic: skip Wi-Fi entirely. adb reverse tcp:8080 tcp:8080 The phone's localhost:8080 now tunnels over the USB cable straight into my proxy. No Wi-Fi flakiness. No firewall. No stale proxy cache. Set the phone's proxy to 127.0.0.1:8080, and the channel is immune to everything except unplugging the cable. I've never gone back.
PART 3 — THE MOBILE WALL, CLIMBED THE HARD WAY
With the channel open, the app's traffic arrived — and then the app refused my certificate. Modern Android apps don't trust user-installed CAs by default. This is where most mobile testing efforts die: the app just doesn't work behind a proxy.
The fix is the app's network_security_config.xml. The app shipped one with certificate transparency enforcement enabled. I patched it to trust user CAs, rebuilt the APK, re-signed it with my own key, installed it — and the app crashed on the sign-in screen. This is the part nobody writes about.
The crash was Resources$NotFoundException on the login screen's text field. A full APK rebuild had RENUMBERED THE RESOURCE IDS, and the app's compiled code references resources by ID, not name. The fix that actually worked was surgical: compile ONLY the patched XML file, zip-swap it into a copy of the ORIGINAL APK, and leave the original resource table untouched. Same patch, zero renumbering, app runs.
But the handshakes still failed. My CA was trusted, the config was patched, and the app still rejected the connection. The culprit: CERTIFICATE TRANSPARENCY ENFORCEMENT, which some app configurations apply against proxy chains even when the CA itself is trusted. Remove the CT element, rebuild, retry — and suddenly the app's startup requests flowed: markets, sessions, the lot.
Then the sign-in stalled. Google Play Services was routing through the proxy and refusing my certificate on its own infrastructure calls — a component that cannot be made to trust you. The answer is pass-through: --ignore-hosts for those hosts so they're tunneled untouched, while everything else still gets decrypted. (And verify the option works in YOUR mitmproxy version — proxy options drift between releases, and a silently ignored flag cost me an hour of false conclusions.)
Finally, the last wall: the login demanded a PROOF-OF-WORK HEADER — a challenge2 with difficulty and randomness parameters, and a required x-pow solution. The algorithm wasn't documented anywhere. So I read the app's own bytecode and reconstructed it: take the challenge seed, append random bytes, double-SHA256, check the top bits are zero, submit the solution as hex. Twenty minutes of disassembly instead of an evening of guessing. (Even then, the server escalated after repeated bad proofs — dynamic challenges are a real anti-automation layer, and the endpoint got stricter with every failed attempt. Respect that. It's working as designed.)
By 3 a.m. the app was running through my proxy, logged in, and HONEST. Every request it made was on my screen — and that's when the real work began.
PART 4 — READING THE WIRE LIKE AN ORACLE
Interception isn't the deliverable. It's the LENS. Here's what the lens showed once the channel was open.
THE ERROR ORACLE. Modern APIs are polite. Send a malformed request and the response explains exactly what it wanted:
{"field":"payload","error":"This value should not be blank."}
Feed it the next guess, read the next error, iterate. In a dozen requests I reconstructed the deposit request schema — field names, types, required order, even the validation SEQUENCE: shape first, then funding-method existence, then amount limits, then fraud signals, then charge. The order matters more than the schema. It tells you which manipulations the server rejects before they can cause harm, and which ones need a live transaction to test. You can map an entire money-movement flow without risking a single cent.
THE ENUMERATION DIFFERENTIAL. The forgot-password form wanted an email. I submitted a fake address, then a real one. Identical responses — byte for byte, same status, same body. No enumeration channel. The ABSENCE of a difference is a finding too: it means the team did this part right, and you can stop hunting there and move on. Two minutes, both answers in hand. A scanner can't ask this question because a scanner doesn't know what a should-be-different response looks like.
THE FIELDS NOBODY VALIDATED. The client sent a return URL for the 3DS challenge. The client sent a device-fingerprinting correlation ID. The client sent its own amount string. Each of those is a candidate trust boundary. Watching the real request tells you the field exists. Sending a tampered variant tells you whether anyone checks. Most of these tests are free and change nothing — and occasionally one of them sails through, which is the exact moment the engagement gets interesting.
THE UNDOCUMENTED TRUTH. The real method enum was "VendorPay (Deposits)". The app's real origin header — the gate every client sends and no doc mentions — was 20g2, not the values any public page used. The login envelope carried a device name and a credential type object that no API reference describes. All of it was in the traffic, in plain sight, the entire time.
None of this came from a scanner. None of it was in the documentation. It was all in the wire, waiting for someone to watch.
PART 5 — WHY IT STAYS OVERLOOKED
If the tool is this good, why does everyone skip it?
"HTTPS IS ENCRYPTED" BECAME A MENTAL STOP SIGN.
People learned that encryption protects data in transit and concluded it therefore cannot be inspected. It can — by anyone the endpoints trust. When you control the device and the CA, you are one of the endpoints' trusted parties. The encryption is intact; you've simply placed an observer you control inside the trust boundary, the way a debugger sits inside a process.
THE FRICTION IS REAL, AND IT COMPOUNDS.
CA stores differ per OS. Network security configs behave differently across API levels. Proxy options drift between tool releases. Rebuilding an app without breaking its resource table is arcane knowledge that lives in forum threads, not manuals. Every practitioner has an evening lost to a certificate that SHOULD have worked. Friction selects against adoption, and the mobile wall is the highest friction in the business.
AUTOMATED TOOLING PROMISES THE SAME RESULT WITH LESS EFFORT.
SAST, DAST, API scanners — they produce findings without anyone touching a proxy. But a scanner can only flag what its rules encode. It cannot notice that two emails get identical responses when the design should differ. It cannot learn that the app sends a field nobody documents. It cannot map the order of validation gates. The tools are complements, and the industry quietly treats them as substitutes.
TUTORIALS STOP AT THE SHALLOW END. Install the CA, view a request is the standard article, and that's where the literature ends.
The advanced material — the resource-ID pitfall, the CT quirk, the adb tunnel, pass-through host lists, reconstructing proof-of-work from bytecode — lives only in scattered notes and hard-won evenings. The people who climb the wall keep the map to themselves.
The practitioners who push through develop a specific habit of mind: TRUST THE WIRE, NOT THE STORY. The spec says one thing. The client sends another. The server enforces a third. Interception is how you read all three at once — and it's the cheapest, fastest way to find the seams between them.
THE MAP, FOR THE NEXT PERSON
- Run mitmdump with a logging addon during EVERY manual session. Archive the output. You're building primary evidence, not notes.
- On mobile, the walls are known and climbable: user-CA trust via network security config, surgical single-file APK patches (never full rebuilds), adb reverse for the channel, pass-through for vendor hosts.
- Use the error oracle deliberately. Validation errors are the API explaining itself.
- Use differentials deliberately. Two submissions, one difference — that's a finding, and its absence is also information.
- Map validation layers before touching anything with real-world effect. Know the gates, in their order, before you test the one you care about.
- And stay on the right side of the line: your devices, your accounts, authorized programs, disclosed scope. The tool's power is proportional to the trust placed in the people who run it.
mitmproxy isn't a beginner tool you graduate past. It's the ground-truth layer — the thing you run when you need to know what ACTUALLY happens.
The app I watched that night was well-defended. The seams I found were honest design, not vulnerabilities. But I found them — all of them — in one session with a free tool and a USB cable. The scanner would still be fuzzing "card".