Level 7B - Signull (mobile / web)

Upon seeing the apk, I immediately threw it into jadx-gui to decompile the java code and understand what the program was doing.
We can zoom into the actual program's source code by going to Source code > com > ropllc.signull

Let's look at the important parts of signullAPI: (fqdn is https://chals.tisc25.ctf.sg:21742)
checkRooted() checks if the device is rooted by checking build tags, superuser paths, root management apps and su binaries, and checkDebugger() checks if a particular file exists. (Fun fact: checkDebugger() checks if /data/local/tmp/frida-server is present)
Both these checks can be bypassed by simply patching the smali code of the application. Since apkProtection() wraps checkRooted() and checkDebugger(), I simply patched it to always return false. You can perform patching easily by using the tool apktool which is integrated with visual studio code (link here).
Now let's look at Stage1Activity:
The apk's main activity is set as Stage1Activity, but upon inspecting the code we notice that it's simply inspecting the integrity of the application and it doesn't even load Stage2Activity if the checks pass. As such, we can also patch the application by changing the MainActivity to Stage2Activity instead. Here's part of my patched AndroidManifest.xml:
Now let's look at Stage2Activity:
The important parts are:
SSL Pinning. This is when an application stores the cert/public key of the server, which makes MITM (Man in the Middle) attacks harder
apk protection checks using
apkProtection(): We patched this to always return false alreadyRetrieving
flagfrom thehelpendpoint (which as we previously saw insignullAPIshould be https://chals.tisc25.ctf.sg:21742/help.php)Retrieving
access_tokenandcsrfTokenfrom theindexendpoint (https://chals.tisc25.ctf.sg:21742/index.php)When the login button is clicked, we send a JSON request to https://chals.tisc25.ctf.sg:21742/login.php containing the entered username and password, and set our
access_tokenandcsrfToken.If the login is successful we go to Stage3Activity, but if it's unsuccessful we get an error message.
I decided to do a MITM approach to intercept the requests and responses between my android device and the server. I set up an emulated Android device using Android Studio, and installed Magisk on it. Installing Magisk allows us to add system certificates belonging to burp suite, allowing us to MITM using a burp suite proxy. More on setting this up here.
I also needed to bypass the SSL Pinning, and for that I used this frida script taken from here. To set up frida you need to install frida-server in the device (usually to /data/local/tmp/frida-server), then use adb to get a root shell into the device and run the frida server in the background using /data/local/tmp/frida-server & . You can then install frida on your host, then run a command like frida -U -f com.ropllc.signull -l .\frida-multiple-unpinning.js to launch the application with the SSL pinning bypass script. If it works correctly you should see:
By simply launching the app into Stage2Activity with the frida script you should be able to bypass SSL pinning and see the first half of the flag:

This is also the flag obtained from /help.php.
Now, how do we get the next part of the flag? After analysing the code for Stage3Activity, I realised that we need to successfully login in stage 2 to see the message. In other words, bypassing Stage2Activity won't get us the next flag part.
Small note: To login, you need to send a json payload that is encrypted using the RSA public key of the server. You'll see the implementation of this later.
Instead of using the Android UI interface to enter credentials, I used ChatGPT with a jadx-gui mcp to write a script to send requests to the login (and other) endpoint(s).
I spent several days trying to brute-force the credentials to the service, but this never worked.
If you used some invalid credentials the server would respond with a JSON object like:
Which tells us that we should target the admin account.
Around the midway mark of the CTF, the challenge author announced that the login page "was now working correctly". I eventually realised that the login page was vulnerable to an SQL Injection.
If your username was something like admin' OR '1'='1' # you would get this response:
Which means we successfully logged in!
Now, we unfortunately realise that we need to find the third part of the flag...
For this, we need to look into Stage3Activity:
Stage3Activity is basically using the verified access_token to access /message.php. It uses the message ID specified in an input field for the request. It then displays the obtained message if there is any.
Similarly I used Python to script the requests and responses to this endpoint. The request body would look something like
After trying to bruteforce the different messageId values I found that the messageId field was also vulnerable to an SQL injection.
Since we already know that the # character comments out the rest of the SQL query, we can do something like
to obtain all table names from the SQL database.
Most importantly, when our access_token is 1i28nc4vhi5bcc8p44afsra3g2, we see the databases messages_1i28nc4vhi5bcc8p44afsra3g2_389150 and secret_1i28nc4vhi5bcc8p44afsra3g2_389150.
This means that the table names where messages are stored depend on the access token we are using. As such, we should use our existing admin access token to read the secret value from the secret table. I modified the script to first get the table name corresponding to the existing session, then use the SQL injection vulnerability to read the entries in the secret table. Below is the solve script:
So the final flag is TISC{Th3_53cr3t_15_r0p11c_15_th3_B1GG35T_5c4m}
Last updated