subtitle

Blog

subtitle

From WebView
to Remote Code Injection

How a Misconfigured JSInterface led to an RCE

Summary

Hi Everyone! My name is Lyes, and i’ve been a full-time bug bounty hunter for almost a year now, my main interest is Android apps, and i wanted to share with you an interesting bug i found a while ago that demonstrates how a chain fo small mistakes can lead to a very big impact.

 

I encountered an app that had an internal WebView and seemed to allow a variety of hosts. Upon further investigation, I found that it exposes a JSInterface with a variety of functionalities, one of which was a file sharing feature that happened to allow a path traversal. This, combined with Over-The-Air updates for its React Native bundles, led to an overwrite of update files and a full RCE, all from one URL click.

TL;DR

To summarize, the chain of attack is as follows:
 
1. Victim clicks a malicious link that triggers the app to open via deeplink (intent://browser/https://sites.google.com/…#Intent;scheme=targetapp;…) and load an attacker-controlled webpage in the app’s WebView (leveraging trusted subdomain bypass: Google Sites with embedded iframe pointing to attacker’s server).
 
2. Once loaded, the malicious webpage calls the exposed JavaScript interface (window.NativeMessageHandler) to perform a handshake sequence, registering itself as a trusted Native sender and gaining access to privileged native functions.
 
3. As an authenticated Native actor, exploit the shareContent function’s path traversal vulnerability (fileName not validated) to write to /data/data/com.target.app/shared_prefs/OTAPrefs.xml using fileName: “../../shared_prefs/OTAPrefs.xml”, overwriting the React Native OTA configuration and setting up a “pending update” pointing to /data/data/com.target.app/index.android.bundle.
 
4. Use the same path traversal vulnerability to write the malicious React Native (Hermes) bundle to the specified path: fileName: “../../index.android.bundle” with base64-encoded malicious bundle as data.
 
5. When the victim relaunches the app, it reads the modified OTA config, detects the pending update from the shared prefs entry, and loads the malicious bundle from the attacker-specified path, granting full JavaScript execution with native module access.
 
6. The malicious React Native code imports and leverages native modules (specifically RNKeychainManager or similar) to decrypt and exfiltrate authentication tokens (including refresh tokens), session cookies from /data/data/com.target.app/ directories, and other sensitive internal files.
 
7. Attacker achieves complete account takeover with persistent access (via stolen refresh tokens) and gains control over all linked platform sessions stored in the app’s WebView cookies and local storage.
 

Initial Analysis

Through the first analysis of the app, and by doing some static analysis, I found a couple of interesting hints that suggested a potential exploitation path:
 
1. The app has an exported activity called MainActivity that registers a variety of deeplinks.
 
2. The MainActivity has a Browsable intent-filter with a Custom Scheme, meaning that potentially any exploit that I find can be triggered via a browser. The AndroidManifest entry looked like this:
 
3. The app has an internal browser, based on a WebView, that doesn’t seem to have any host restrictions as any website can be opened in it.
 
4. The WebView registers a JSInterface NativeMessageHandler that uses postMessage and is accessible for any host/domain.
 
We have all the necessary elements for a good exploit here: there’s the entrance, the entrance is remote, possibly no validation, and there’s an exposed JSInterface also with no apparent validation. If we find a bug in that interface, then we’ll surely be able to exploit it.

The JSInterface

The NativeMessageHandler JSInterface only has a single exported method, which is postMessage. This is both a blessing and a curse. The curse being that we can’t really get all the exported methods/actions by just looking at that class, and the blessing is that this can be accessed from an iframe (which will be much needed later on) and we can control the data to be sent much more.
 
After some time reversing the logic of this handler, I understood the following:
 
1. The handler registers a bunch of actions.
 
2. There are two types of actions: Standard and Native.
 
3. Without thinking, I understood that I need to get to the Native actions.
 
4. To get to those Native actions, I need to register my page as a Native sender.
 
5. There are two standard  actions to do this:
 
1. Initiate the registration process by using the following call:
 
2. Finalize it by using the following call:
6. Once that’s done, we get access to the juicy Native actions!
 
7. There was a variety of actions, each action had a dedicated handler that:

     * Checks whether the message is indeed handled by this handler (checks the action attribute).

     * If yes, it handles it accordingly and gets the args from the params attribute of the message.

8. One of the actions was shareContent, and upon investigation, it took a fileName and a dataString args.

At this point, I was pretty sure that there’s something fishy about this interface. Sometimes the stars are aligning too well for it to be nothing or not exploitable. Even if there’s nothing, I gotta make it work somehow, it’s too good to let it slide.

Content Sharing

This action, as I previously mentioned, took a fileName and a dataString, and it used them as follows:
This is oversimplified, and there was a lot of uninteresting code, but these are the most important bits, and clearly there is a Path Traversal and Arbitrary Write, which gives us Arbitrary File Write!
 
Awesome, now we can write anywhere, but the question is: where can we write for this to be really exploitable?

React Native and Updates

React Native Over-The-Air (OTA) updates allow mobile apps to update their JavaScript bundles without going through the app store review process. In a typical React Native app, the native container (Java/Kotlin for Android, Objective-C/Swift for iOS) remains static, but the JavaScript bundle that contains the app’s business logic can be updated dynamically. When the app launches, it checks a remote server for new bundle versions, downloads them if available, and loads the updated code on the next restart. This mechanism typically stores configuration in shared preferences (Android) or UserDefaults (iOS), including metadata like the bundle version, download URL, and local file path. Popular implementations include Microsoft’s CodePush and custom solutions. While OTA updates provide faster iteration and bug fixes, they also create a security-critical attack surface: if an attacker can manipulate the OTA configuration or bundle storage location, they can inject malicious JavaScript code that executes with full access to the app’s native modules, effectively achieving remote code execution within the app’s sandbox.
 
 
Upon investigating the app’s internal storage (/data/data/com.target.app/) looking for any potentially interesting write targets, I found a weird directory called app_ota that contained a version-like directory, which contained an index.android.bundle file. At this point, I knew that the stars weren’t aligning for nothing, and that I potentially found my first RCE.
 
I started looking for how this update mechanism worked, and by checking the code and relevant classes, I understood the following:
 
1. Bundle Storage: The app stores React Native bundles in version-specific directories at /data/data/com.target.app/app_ota/{version}/index.android.bundle.
 
2. Configuration File: The OTA system uses a shared preferences XML file at /data/data/com.target.app/shared_prefs/OTAPrefs.xml to track update states and bundle locations.
 
3. Update States: The configuration file maintains multiple states:
 
Current bundle (alive): The currently running bundle version tracked in @react-native-ota/last-alive-bundle-version.
 
Pending update (candidate): A new bundle waiting to be loaded, tracked in @react-native-ota/pending-update.
 
4. Pending Update Structure: The pending update entry contains critical metadata in JSON format:
5. Loading Mechanism: On app launch, the update system:
 
– Reads the OTAPrefs.xml configuration.
 
– Checks for a pending update in @react-native-ota/pending-update .
 
– If found, loads the bundle from the path specified in the chunks.index field.
 
– Executes the new bundle code with full native module access.
 
 
The beauty of this mechanism for exploitation is that it’s version-agnostic: by controlling both the configuration file and the bundle path, an attacker can bypass version-specific directories entirely and point the loader to any arbitrary location in the app’s internal storage.
 
Now the path is clear:
 
1. Load a webpage in the WebView.
 
2. Register a Native receiver.
 
3. Call the shareContent function.
 
4. Overwrite the updates config file to always point to an update, no matter the version.
 
5. Create a new update file (Hermes JS bundle).
 
6. Crash the app.
 
After this, when the user restarts the app, it should load the malicious bundle.

Minor Problem Before The Finish Line

When I tried to run the full chain of exploitation, I noticed that in fact the deeplink doesn’t open any given URL—there was a server-side check that sends the host to an API endpoint and verifies whether it’s TRUSTED or not.
 
I started looking at all the trusted domains looking for an open redirection, as once the URL is loaded into the WebView there’s no validation. I tried for a while but couldn’t find anything, then I found an interesting thing:
 
1. The app trusts google.com and its subdomains.
 
2. A redirection through Google would be an extra user click, since there’s always a consent page (at least that’s what I thought, but after the submission I realized that there was a way to do this without having to go through the consent page).
 
3. Google has a subdomain called sites.google.com.
 
4. This subdomain allows hosting of arbitrary webpages through an iframe.
 
5. Once the webpage is in the iframe, it simply uses postMessage to deliver the JS calls, and it’s done!
 
This was an interesting case that showcases that over-trusting hosts is never a good idea, and that there’s always a way to get some code running in your WebView if you do so.

Final Exploit

With all the pieces in place, here’s how the complete attack chain works from a single malicious URL click to full account takeover:

Step 1: Entry Point - Deeplink to Trusted Subdomain

The attack begins with a specially crafted deeplink that leverages the app’s trust in certain domains. While the app restricts which websites can be loaded in its WebView, it trusts all subdomains of google.com – including sites.google.com, a service that allows anyone to create websites with embedded iframes.
 
The attacker creates a Google Site that embeds an iframe pointing to their malicious server. When the victim clicks the link, the app opens the Google Site, which loads the attacker’s iframe – bypassing all domain restrictions.

Step 2: Native Registration Handshake

Once the malicious page loads in the WebView, it needs to register as a trusted “Native” sender to access privileged functions. This involves a two-step handshake:
This bypasses the checks in the message queue controller and grants access to native actions.

Step 3: Version Detection (Cross-Version Compatibility)

To make the exploit work across all app versions, we first need to detect the current app version. We can do this by making the app send an HTTP request to our server – the app includes its version in the User-Agent and some custom headers.

Step 4: Overwrite OTA Configuration

Now comes the critical part – overwriting the OTA configuration file to create a fake “pending update” that points to our malicious bundle:
This configuration tells the app that there’s a pending update at /data/user/0/com.target.app/index.android.bundle – a static path that works regardless of version numbers.

Step 5: Deploy Malicious React Native Bundle

Next, we write the malicious Hermes-compiled React Native bundle to the path specified in our fake configuration:

Step 6: Trigger App Restart

The final step is to force the app to restart so it loads our malicious bundle. This can be done by:
 
– Crashing the app intentionally (corrupt a file, loop-load URLs, etc.)
 
– Waiting for the user to naturally restart the app

Step 7: Post-Exploitation - Malicious Bundle Execution

When the app restarts, it reads the OTA configuration, finds the pending update, and loads our malicious bundle. The malicious React Native code now has full access to all native modules:
All of this can be summarized in the following Diagram:

Impact

This exploit chain resulted in complete compromise of the victim’s account through:

 

1. Direct Access:
 
Persistent Account Takeover: Stolen refresh tokens from Android Keychain (RNKeychainManager) provide long-term access without credentials.
 
Sensitive Data: Complete user profile, preferences, history, and any stored personal information.

 

2. Multi-Platform Compromise:
 
– Cross-Platform Sessions: All authentication cookies for websites visited through the app’s WebView, stored in:
 
data/data/com.target.app/app_webview/Cookies
 
/data/data/com.target.app/app_webview/Local Storage/
 
/data/data/com.target.app/databases/*
 
– This cascading impact means one compromised app = multiple compromised third-party platform accounts.

 

3. Persistent Control:
 
Code Execution: Malicious bundle remains active across app restarts until app reinstallation or data wipe.
 
Silent Monitoring: Can intercept all API calls, log user actions, and exfiltrate data continuously.
 
UI Manipulation: Ability to display fake screens for credential phishing or to trick users into sensitive actions.
 
Remote Updates: Can dynamically load additional malicious code from attacker’s server using native modules.
 
4. Attack Characteristics:
 
One-Click: Entire attack chain triggered from a single malicious URL (deliverable via SMS, email, social media, messaging apps).
 
Silent Execution: No visible warnings, permission prompts, or suspicious behavior detectable by victim.
 
Mass Scalable: One exploit URL can compromise unlimited victims simultaneously.
 
No Special Access: Operates within app’s existing sandbox, requires no root, ADB, or additional permissions.

This bug was triaged as a Critical and was remediated quickly.

How Djini could've saved me some sleepless nights

I tried to replicate this same finding with Djini.AI -the Mobile-Oriented LLM Agent from MobileHackingLab – and see how it could’ve helped me maybe find this sooner, or spare me sometime debugging and reversing the code.

 

I always believed that these types of agents would really improve the reverse-engineering process because of their ability to understand obfuscate code, to follow call flows more rapidly and more efficiently, and, if trained on a Offensive-approach, to spot misconfigurations and potential bugs in way less time then us.

 

This summarizes my experince with Djini on this app.

Spotting the JSInterface

To use djini, you simply provide it with an APK, and you let the magic happen:

 

1. It analyzes the given APK with some statical analysis tools.

 

2. It creates a overall image about what potentially could be wrong or what is a potential attack vector.

 

3. It then starts dynamical and intelligent analysis on these findings.

 

4. In the end, you’re presented with a bunch of findings that server as potential valid bugs, or potential entry points.

 

I saw that it detected the usage of JSInterfaces in one the findings, and i wanted to dig more, so i asked it to identify the webviews in the app, and find out how they’re initialized and what sort of JSBridges do they implement, the result was this:

 

 

So i can already see which webview is used, what JSInterface is exported and what methods does it exposes. This is already great, but arguably a researcher can find it in a reasonable amount of time, so i tried to push it further.

Finding the vulnerable action

After finding the JSInterface, i wanted to see whether it’ll be able to understand the functioning of this JSInterface and figure out what actions can be used. After two prompts of asking it to understand the logic behind this interface, how does it process data, and what is the relevant impact of it, it returned this:

Among these actions was the vulnerable shareContent action that led to the file write. This is amazing, because what took me hours of reverse engineering and dynamic analysis, this LLM found it in minutes, and it can also do dynamic analysis with Frida so it can confirm this behavior. At this point i confirmed that the usage of LLMs in this field would really improve my workflow, if i can spare myself the reverse engineering time, and just focus on chaining bugs and finding meaningful impacts, then i’m in.

Confirming the file write

To end this, i just wanted to see whether he would be able to find the file write or not, so i asked it about that action to summarize it’s behavior and to see whether there’s something wrong with it, and to my surprise, i got a valuable answer:

 

 

It understood the usage of this action, it went through all the processing logic that contained a long call flow, and it understood, when asked, that there is indeed a file write and that no sanitation is in the fileName.

 

This goes to show the reality of reverse engineering, most of the time it’s just going through code and trying to figure what is it doing, and how can you use it to make an impact. In theory, anyone can do this if they understand the coding language and have the right tools(like JaDX) to read that code. If these are the requirement, then with the way LLMs are evolving, they will become another tool to read, understand and get a grasp of the code you’re trying to reverse, more quickly and more efficiently then before.

Conclusion

This case shows that the real challenge in modern mobile security isn’t finding individual bugs, but connecting them into high-impact exploit chains fast enough. That work is deep, manual, and difficult to scale — especially for enterprise teams securing many mobile applications in parallel.

 

Djini.ai accelerates this process by removing reverse-engineering overhead and surfacing dangerous primitives early, so security teams, pentesters, and researchers can focus on impact instead of boilerplate analysis.

 

With iOS jailbroken and Android rooted devices, plus bundled premium courses and certifications from Mobile Hacking Lab, it provides a complete end-to-end mobile security workflow.

 

If your team wants to move faster and go deeper, schedule a demo and see it in action.

 

Book a meeting: https://calendly.com/d/cryq-nqv-8gf/book-a-demo

Check out the subscription: https://djini.ai/pricing/

 
2 Comments
  • Raouf

    January 22, 2026

    Well done Lyes, i liked the last graph it sums it up pretty well. You demonstrated how crucial it is to not rush to reporting after finding a bug, and take as much time as needed to chain bugs and escalate it to a Critical bug which is rare in Mobile bug bounty

    REPLY
  • Tony Shavez

    January 22, 2026

    Great write‑up, Qt — this was a really satisfying read. The way you chained the JSInterface abuse, OTA mechanics, and trusted-domain bypass into a clean, end‑to‑end exploit shows serious depth and patience. I especially liked how you didn’t just stop at “arbitrary file write” but pushed until you found a meaningful, persistent RCE path — that’s real researcher mindset.

    If I had one suggestion to make it even stronger: you could slightly tighten the early sections by adding a small “threat model / why this matters” paragraph right after introducing the Native actions. A quick framing of why this interface is dangerous in a WebView context would help less experienced readers immediately grasp the stakes before diving into the details.

    Overall though, this is solid, impactful work — technical, well‑chained, and very practical. Definitely the kind of case study people will reference 👏

    REPLY

Comments are closed.