Forging API Signatures to Find 15 IDORs in an E-Wallet App
Richard · April 30, 2020
A lot of apps assume that signing a request is enough to stop parameter tampering — if the client computes a signature over the payload and the server checks it, changing a parameter should invalidate the signature. That assumption held right up until the signing function itself turned out to be reachable.
The setup
This particular e-wallet app signed sensitive requests with an MD5-based
signature computed client-side over the request parameters. Endpoints like
deleteBankAccount took a target ID plus a signature, and the server
trusted any request where the signature matched. Change the ID without
recomputing the signature, and the request gets rejected outright.
So the actual target wasn’t the endpoints — it was the function computing the signature.
A stock deleteBankAccount request — change id without recomputing signature and the server rejects it outright.
Cracking the signing function
Decompiling the APK with apktool and reading through it in jadx-gui
surfaced the method responsible: a straightforward MD5Encode function
that concatenated a handful of request parameters and hashed them. Nothing
obfuscated, nothing dynamically loaded — just a plain static method.
The whole signing routine, sitting in plain, unobfuscated Java.
That meant the signature could be recomputed offline: extract the
relevant Java, compile it locally, and calculate valid signatures for
arbitrary parameter values without touching the app at all. For
deleteBankAccount, that was enough on its own — swap in another user’s
bank account ID, compute a matching signature locally, and the server
accepted it as a legitimate request from that account’s owner.
The extracted signing method recompiled locally, generating valid signatures for a whole range of IDs at once — no device, no Frida, no app required.
A direct copy of the app’s own signing logic — since nothing server-side ever validated it, reimplementing it offline was enough.
Scaling it up with Frida
Some endpoints combined more parameters, or used values that were awkward to obtain outside a live session (session tokens, rotating IDs). Rather than reimplementing the signing logic offline for every case, hooking the method at runtime made this trivial:
Java.perform(() => {
const Signer = Java.use('com.razer.pay.security.Signer');
Signer.MD5Encode.implementation = function (input) {
const sig = this.MD5Encode(input);
console.log(`[+] input=${input} signature=${sig}`);
return sig;
};
});
With the hook in place on a rooted test device, any payload — legitimate or tampered — could be run through the app’s own signing function and get back a valid signature for it. From there, forging requests for other users’ data was just a matter of building the payload and letting the app sign it.
Every call to MD5Encode on the live device, hooked and echoed back with its signature — legitimate traffic or forged, the app signs whatever it’s handed.
What that unlocked
Chained across the affected endpoints, this pattern led to roughly 15 separate IDORs: deleting other users’ linked bank accounts, joining chat groups without invitation, reading private transaction history, and viewing (and in some flows, participating in) other users’ fund transfers.
One of the chained IDORs: forging a signature for addGroupUser was enough to join a chat group with zero invitation.
The lesson
Signing a request is only as strong as how hard the signing function is to reach. If the signature is computed entirely client-side with no server secret involved, it isn’t really authentication — it’s just an extra step an attacker has to automate. The interesting work here wasn’t finding the IDOR endpoints; it was recognizing that the thing meant to stop tampering was itself sitting right there in the APK, waiting to be called.