DEV Community

Chigozie Oduah
Chigozie Oduah

Posted on • Originally published at Medium

Intentional Exercise Hacker 101 CTF writeup

A moderate-difficulty android-based hacker 101 CTF exercise. The goal of this exercise is to get the app to display the flag.

Getting Started

After starting the challenge, we're given an Android APK to work with, and we'll start by opening this file in Android Studio. To do that, we'll click on the three dots menu on the top right and then Profile or Debug APK, or More Actions > Profile or Debug APK in the welcome menu, and select the level13.apk file we're provided with.

Next, we'll start up a virtual device, and drag the APK to the device to install the app. And now we can start our analysis.

App analysis

We'll start by exploring the app to get a surface level understanding of how it works. Immediately after opening the app, we're greeted with this page:
Homepage

When we click Flag, we get an "invalid request" page message. Since our goal is to access this page, let's look into how the app's logic will allow us to do that. To do this, we'll need to get more technical.

Technical analysis

For this analysis there're three parts of the application we'll be going into:

  • The manifest file (AndroidManifest.xml)
  • The app's entrypoint (MainActivity.smali)
  • The app's server

AndroidManifest.xml

We'll start by looking at the AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="28"
    android:compileSdkVersionCodename="9"
    package="com.hacker101.level13"
    platformBuildVersionCode="28"
    platformBuildVersionName="9">

    <uses-sdk
        android:minSdkVersion="23"
        android:targetSdkVersion="28" />

    <uses-permission
        android:name="android.permission.INTERNET" />

    <application
        android:theme="@ref/0x7f0c0005"
        android:label="@ref/0x7f0b0027"
        android:icon="@ref/0x7f0a0000"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:roundIcon="@ref/0x7f0a0001"
        android:appComponentFactory="android.support.v4.app.CoreComponentFactory">

        <activity
            android:name="com.hacker101.level13.MainActivity">

            <intent-filter>

                <action
                    android:name="android.intent.action.MAIN" />

                <category
                    android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>

                <action
                    android:name="android.intent.action.VIEW" />

                <category
                    android:name="android.intent.category.DEFAULT" />

                <category
                    android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="http"
                    android:host="level13.hacker101.com" />
            </intent-filter>

            <intent-filter>

                <action
                    android:name="android.intent.action.VIEW" />

                <category
                    android:name="android.intent.category.DEFAULT" />

                <category
                    android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="https"
                    android:host="level13.hacker101.com" />
            </intent-filter>
        </activity>
    </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

In this file, we can see two valuable pieces of information, the program's entrypoint com.hacker101.level13.MainActivity:

...
<activity
            android:name="com.hacker101.level13.MainActivity">
...
Enter fullscreen mode Exit fullscreen mode

And the intent filters:

...
            <intent-filter>

                <action
                    android:name="android.intent.action.VIEW" />

                <category
                    android:name="android.intent.category.DEFAULT" />

                <category
                    android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="http"
                    android:host="level13.hacker101.com" />
            </intent-filter>

            <intent-filter>

                <action
                    android:name="android.intent.action.VIEW" />

                <category
                    android:name="android.intent.category.DEFAULT" />

                <category
                    android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="https"
                    android:host="level13.hacker101.com" />
            </intent-filter>
...
Enter fullscreen mode Exit fullscreen mode

A simplified explanation of what intent filters are, are they're essentially a description of the signals (intents) that applications listen for. The android manifest file tells the operating system that the application wants to listen for intents on URIs that point to http://level13.hacker101.com and https://level13.hacker101.com.

This is going to be useful information for understanding the application.

MainActivity.smali

This is where the application's logic resides. We'll start by opening up MainActivity.smali and navigate to the onCreate method definition This is the part of the code that runs when the app is opened:

.method protected onCreate(Landroid/os/Bundle;)V
    .registers 9

    .line 19
    invoke-super {p0, p1}, Landroid/support/v7/app/AppCompatActivity;->onCreate(Landroid/os/Bundle;)V

    const p1, 0x7f09001c

    .line 20
    invoke-virtual {p0, p1}, Lcom/hacker101/level13/MainActivity;->setContentView(I)V

    const p1, 0x7f070090

    .line 21
    invoke-virtual {p0, p1}, Lcom/hacker101/level13/MainActivity;->findViewById(I)Landroid/view/View;

    move-result-object p1

    check-cast p1, Landroid/webkit/WebView;

    .line 22
    new-instance v0, Landroid/webkit/WebViewClient;

    invoke-direct {v0}, Landroid/webkit/WebViewClient;-><init>()V

    invoke-virtual {p1, v0}, Landroid/webkit/WebView;->setWebViewClient(Landroid/webkit/WebViewClient;)V

    .line 23
    invoke-virtual {p0}, Lcom/hacker101/level13/MainActivity;->getIntent()Landroid/content/Intent;

    move-result-object v0

    .line 24
    invoke-virtual {v0}, Landroid/content/Intent;->getData()Landroid/net/Uri;

    move-result-object v0

    const-string v1, "https://ctf-id.ctf.hacker101.com/appRoot"

    const-string v2, ""

    if-eqz v0, :cond_41

    .line 28
    invoke-virtual {v0}, Landroid/net/Uri;->toString()Ljava/lang/String;

    move-result-object v0

    const/16 v2, 0x1c

    invoke-virtual {v0, v2}, Ljava/lang/String;->substring(I)Ljava/lang/String;

    move-result-object v2

    .line 29
    new-instance v0, Ljava/lang/StringBuilder;

    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v1

    :cond_41
    const-string v0, "?"

    .line 31
    invoke-virtual {v1, v0}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z

    move-result v0

    if-nez v0, :cond_5a

    .line 32
    new-instance v0, Ljava/lang/StringBuilder;

    invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    const-string v1, "?"

    invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v1

    :cond_5a
    :try_start_5a
    const-string v0, "SHA-256"

    .line 34
    invoke-static {v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;

    move-result-object v0

    const-string v3, "s00p3rs3cr3tk3y"

    .line 35
    sget-object v4, Ljava/nio/charset/StandardCharsets;->UTF_8:Ljava/nio/charset/Charset;

    invoke-virtual {v3, v4}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B

    move-result-object v3

    invoke-virtual {v0, v3}, Ljava/security/MessageDigest;->update([B)V

    .line 36
    sget-object v3, Ljava/nio/charset/StandardCharsets;->UTF_8:Ljava/nio/charset/Charset;

    invoke-virtual {v2, v3}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B

    move-result-object v2

    invoke-virtual {v0, v2}, Ljava/security/MessageDigest;->update([B)V

    .line 37
    invoke-virtual {v0}, Ljava/security/MessageDigest;->digest()[B

    move-result-object v0

    const-string v2, "%064x"

    const/4 v3, 0x1

    .line 38
    new-array v4, v3, [Ljava/lang/Object;

    const/4 v5, 0x0

    new-instance v6, Ljava/math/BigInteger;

    invoke-direct {v6, v3, v0}, Ljava/math/BigInteger;-><init>(I[B)V

    aput-object v6, v4, v5

    invoke-static {v2, v4}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

    move-result-object v0

    .line 39
    new-instance v2, Ljava/lang/StringBuilder;

    invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V

    invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    const-string v1, "&hash="

    invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    move-result-object v0

    .line 40
    invoke-virtual {p1, v0}, Landroid/webkit/WebView;->loadUrl(Ljava/lang/String;)V
    :try_end_a0
    .catch Ljava/security/NoSuchAlgorithmException; {:try_start_5a .. :try_end_a0} :catch_a1

    goto :goto_a5

    :catch_a1
    move-exception p1

    .line 42
    invoke-virtual {p1}, Ljava/security/NoSuchAlgorithmException;->printStackTrace()V

    :goto_a5
    return-void
.end method
Enter fullscreen mode Exit fullscreen mode

Smali code is just a higher-level representation of the dex byte code. It is relatively straight forward to convert Smali to Java code, but it takes some getting used to. But for ease of understanding this writeup, I'll also the Java translation. However, if you want to learn more about Smali code, this cheatsheet and this article are very useful resources.

Here's its equivalent Java translation:

@Override
protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(R.layout.activity_main);

    WebView webView = (WebView) findViewById(R.id.webview);
    webView.setWebViewClient(new WebViewClient());

    Intent intent = getIntent();
    Uri data = intent.getData();

    String baseUrl = "https://ctf-id.ctf.hacker101.com/appRoot";
    String extraPath = "";

    if (data != null) {
        String dataString = data.toString();
        extraPath = dataString.substring(28);
        baseUrl = baseUrl + extraPath;
    }

    if (!baseUrl.contains("?")) {
        baseUrl = baseUrl + "?";
    }

    try {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        String secretKey = "s00p3rs3cr3tk3y";

        // Hash the secret key first
        digest.update(secretKey.getBytes(StandardCharsets.UTF_8));
        // Then hash the extra path provided in the intent
        digest.update(extraPath.getBytes(StandardCharsets.UTF_8));

        byte[] hashBytes = digest.digest();

        // Convert hash to a 64-character hex string
        String hexHash = String.format("%064x", new BigInteger(1, hashBytes));

        // Append the hash as a query parameter and load
        String finalUrl = baseUrl + "&hash=" + hexHash;
        webView.loadUrl(finalUrl);

    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
}
Enter fullscreen mode Exit fullscreen mode

From this code sample, we can see that the application listens for if an intent was used to run the app and gets its URI data:

    Intent intent = getIntent();
    Uri data = intent.getData();
Enter fullscreen mode Exit fullscreen mode

Looking at the url we added to our intent, we can see that http://level13.hacker101.com is exactly 28 characters.

Then it takes a slice of the string 28th character to the end of the string and hashes it with the secret key:

        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        String secretKey = "s00p3rs3cr3tk3y";

        // Hash the secret key first
        digest.update(secretKey.getBytes(StandardCharsets.UTF_8));
        // Then hash the extra path provided in the intent
        digest.update(extraPath.getBytes(StandardCharsets.UTF_8));

        byte[] hashBytes = digest.digest();

        // Convert hash to a 64-character hex string
        String hexHash = String.format("%064x", new BigInteger(1, hashBytes));
Enter fullscreen mode Exit fullscreen mode

After that, it combines everything into a URL and loads it into the WebView element:

        String finalUrl = baseUrl + "&hash=" + hexHash;
        webView.loadUrl(finalUrl);
Enter fullscreen mode Exit fullscreen mode

Without any intent to get the data from, the url it generates is https://ctf-id.ctf.hacker101.com/appRoot?&hash=61f4518d844a9bd27bb971e55a23cd6cf3a9f5ef7f46285461cf6cf135918a1a (You can see the page by disabling wifi and cellular in the android virtual device and opening the app) which it loads as the first page.

Web server

In this section, we'll be taking a look at the server https://492b0cfd403532a8fa21fd4c01adcbb7.ctf.hacker101.com/appRoot. We'll start by opening the url in a browser and take a look at the dev tools panel to see the page's HTML code:

<h1>Welcome to Level13</h1>
<a href="appRoot/flagBearer">Flag</a>
Enter fullscreen mode Exit fullscreen mode

From that, we can see that clicking the Flag link takes us to the appRoot/flagBearer page, which gives us the "Invalid request" message.

And now we have all the information we need to begin exploiting the application to open the page.

Exploitation

The idea I have for how we can access the flag page is: we craft an intent with http://level13.hacker101.com/flagBearer. Because the application takes a substring from the data string, which becomes "/flagBearer". Following the application logic with the this intent, it would hash the substring with the secret key, and combine the appRoot url, the substring from the intent and its hash, and load it into the WebView.

We can craft and trigger intents using the adb command by following these steps:

  1. Run adb devices to list the device that we're connected to, this should show the emulated device.

    $ adb devices
    List of devices attached
    emulator-5554   device
    
  2. Run adb shell to get a shell to the device we're connected to.

  3. Craft and trigger our intent in the shell:

    $ am start -d "http://level13.hacker101.com/flagBearer"
    

After triggering this intent, we will see a dialog in the virtual device prompting you for what app you want to open it in. Select the level13 app and any of "just once" or "Always":
intent prompt

This will take you to the flagBearer page in the app, where the flag will appear:
flag

Top comments (0)