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:

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>
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">
...
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>
...
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
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();
}
}
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();
Looking at the url we added to our intent, we can see that
http://level13.hacker101.comis 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));
After that, it combines everything into a URL and loads it into the WebView element:
String finalUrl = baseUrl + "&hash=" + hexHash;
webView.loadUrl(finalUrl);
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>
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:
-
Run
adb devicesto list the device that we're connected to, this should show the emulated device.
$ adb devices List of devices attached emulator-5554 device Run
adb shellto get a shell to the device we're connected to.-
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":

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

Top comments (0)