Wie is de Mol?
Wie is de Mol? (‘Who is the Mole?’) is a popular Dutch television game show, currently airing its 21st season. Contestants compete in challenges to win money that goes into a shared prize pool. One of the contestants acts as a mole, attempting to sabotage the other contestants in their efforts to win challenges. The identity of the mole is unknown and it is up to the contestants to determine who it might be. Every episode a contestant leaves the show, until the final contestant leaves with the money collected during the season.
For the last couple of years, my colleagues have been competing in a group in the Wie is de Mol? app, available for iOS and Android. The app allows users to place a bet (using free digital tokens, there are no payments involved as far as I know) on the persons who they deem most likely to be the mole.
When I received an invitation from a colleague for this year’s group, I couldn’t resist to take a look inside the Android app and its accompying API to see whether I could dig up any interesting vulnerabilities. I should note that I first made sure that the broadcasting network, AVRO, has a vulnerability disclosure policy in place that allows such research. They appear to welcome security research, so off we go!
Preface
My goal for this post is to share my process in tackling challenges that may occur while reverse engineering an Android application in a black box setting, therefore I won’t delve into too much detail on the usage of any of the tools listed below. Having a basic understanding of Android and web applications is recommended.
Tools
Application | Usage |
Apktool | Used to disassemble the Android application and to inject Frida Gadget as a native library |
Frida | Used for dynamic instrumentation at runtime |
Burp Suite | Used to intercept traffic between the Android app and its backend |
CyberChef | Used for quick encoding/decoding/hashing/etc. |
For information on how to inject Frida Gadget into the apk, please take a look at one of the many tutorials out there, such as this one or this one.
Intercepting HTTP Traffic
Frida enables us to override common certificate pinning implementations, so we can intercept traffic using a proxy of our choosing (in this case, Burp Suite). After setting up the environment, launch the Android application and interact with it to generate some traffic. Let’s start out by pressing the ‘I forgot my password’ button, resulting in the following request and response:
Request
1POST /profile/forgot_password/ HTTP/1.12Content-Type: application/json; charset=UTF-83Content-Length: 264Host: api.wieisdemol.nl5Connection: close6Accept-Encoding: gzip, deflate7User-Agent: okhttp/4.7.28X-Signature: HMAC widm-api:UaXaUB6YCKY2AeeuFk00K/Cj5O5cB77NivxtWFT03iU=9Date: 2021-01-01T23:38:50.5291011{"email":"***REDACTED***"}
Response
1HTTP/1.1 400 Bad Request2Server: nginx/1.19.53Date: Fri, 01 Jan 2021 22:38:50 GMT4Content-Type: application/json5Content-Length: 1316x-config-version: 97Via: 1.1 google8Alt-Svc: clear9Connection: close1011{"detail":{"email":["Geen e-mailaccount gevonden.\nMisschien was je ingelogd met een social account (bijv. Facebook of Google)?"]}}
Notice the X-Signature
header in the request: this header provides a HMAC signature which is verified by the backend, so as to ensure the integrity of the message. Sending a request with an invalid signature, or with no signature at all, results in a 403 response code:
This makes modifying requests a bit harder, because we will need to resign the message before sending it to the API. Fortunately, we have the application’s decompiled bytecode at hand to take a look at what’s going on.
Inside the HMAC
In the decompiled bytecode (a.k.a. smali), we can simply search for the string HMAC
and see what pops up. Results turn out to be limited, and we can determine that the signature is composed using the SHA256 hashing function in the following method:
1.method public static final calculateHmac(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B23.locals 945const-string v0, "secret"67invoke-static {p0, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V89const-string v0, "method"1011invoke-static {p1, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V1213const-string v0, "body"1415invoke-static {p2, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V1617const-string v0, "contentType"1819invoke-static {p3, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V2021const-string v0, "date"2223invoke-static {p4, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V2425const-string v0, "path"2627invoke-static {p5, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V2829[... snip ...]
Using Frida, we can override the calculateHmac
method to log its arguments before executing, resulting in the following data:
1{2 "0": "█████",3 "1": "POST",4 "2": "{\"email\":\"***REDACTED***\"}",5 "3": "application/json; charset=UTF-8",6 "4": "2021-01-01T22:13:30.166",7 "5": "/profile/forgot_password/"8}
Notice how these arguments line up perfectly with the method signature as seen in the decompiled bytecode above. As an added bonus, we now have the UTF-8 encoded ‘secret’ key used to sign messages: █████
.
- Update — January 5, 2021
Upon request from the broadcasting network, AVRO, I have removed all references to the secret signing key from this blog post (as well as from the repository linked down below).
Remember that a HMAC function takes two arguments: a key and a message. Therefore, the aforementioned arguments need to be transformed into a message (i.e. a byte sequence) before being passed onto the hashing function. Usually developers simply compose the message by concatenating the strings. However, on occasion, a custom implementation is used. To be on the safe side, as well as to avoid making false assumptions, let’s hook the call to the class method that makes the actual call into the crypto library. This method is aptly named hmac
and, as expected, accepts two strings: the message to be signed and a key. Logging provides the following arguments:
1{2 "0": "POST\n078bdf5f7296200ae9a89901305dec37852dbf84e21496550ad2c477149aa6ac\napplication/json; charset=UTF-8\n2021-01-01T22:13:30.166\n/profile/forgot_password/",3 "1": "█████"4}
It appears that the strings, as seen in the first argument, are separated by newline characters. Somewhat surprisingly, a sequence that we haven’t seen before surfaces: 078bdf5f7296200ae9a89901305dec37852dbf84e21496550ad2c477149aa6ac
. Looking at our findings so far, notice that this sequence is right where the body of the request was placed before. Perhaps this represents a hash of the body? Since I’m not sure what this seemingly random sequence denotes, we’ll return to the decompiled methods to take a closer look.
Following some of the cross-references, it doesn’t take long to find a call to another class implementing hashing, this time using a SHA-256 message digest (i.e. good-old hashing without any secrets). Internally, a call is made into Java’s built-in security library:
1invoke-static {p1}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;23move-result-object p145invoke-virtual {p1, p0}, Ljava/security/MessageDigest;->digest([B)[B
Let’s verify whether we can compute the exact same hash for the request shown above, by applying the SHA-256 hashing algorithm to the request body. Start out by hashing the body of the request using CyberChef, like so:
Great, that’s the exact same hash as seen in the arguments above. Notice that I excluded the backslashes in the JSON, as these were added by JSON.stringify
in our Frida script (due to escaping) and are therefore not part of the original body (as confirmed by the original request at the top of this post).
Now that we have all the arguments we need, all that’s left is to compute the final HMAC used to verify the integrity of the HTTP request. Again, we could use CyberChef to do the heavy lifting for us, but for some reason I did not manage to get CyberChef to produce the same results as the built-in Java library, most likely due to encoding differences. Therefore, I wrote a small Python script based on a ready-to-use snippet I found online. Computing the signature using this script indeed results in the expected base64 encoded string as present in the original request:
Automated Request Signing
In order to effectively interact with the API, some automation might come in useful. Specifically, we want to be able to compose arbitrary requests, which we can then copy-paste into a tool to generate a valid HMAC signature, requiring as little manual labor as needed. Since we’ve already done most of the work, creating a Python script is not that much effort. You can find the resulting script in this repository.
What’s Next?
Now that we have successfully reverse engineered the signing algorithm, we can continue using the app as one normally would, while intercepting requests in the background. We can analyze these requests for interesting behavior, modifying and signing them as we please, thanks to our automated request signer. At the moment, I haven’t found any vulnerabilities yet, but if I find anything interesting, I’ll be sure to update this post once the issue has been resolved by the broadcasting network. If you find a vulnerability using any of the methods described here, please let me know! You can contact me on Twitter or via any of the links down in the footer of the page. Thanks for stopping by! :)