webrtcH4cKS: ~ Dear NY Times, if you’re going to hack people, at least do it cleanly!

So the New York times uses WebRTC to gather your local ip addresses… Tsahi describes the non-technical parts of the issue in his blog. Let’s look at the technical details… it turns out that the Javascript code used is very clunky and inefficient.

First thing to do is to check chrome://webrtc-internals (my favorite tool since the hangouts analysis). And indeed, nytimes.com is using the RTCPeerConnection API. We can see a peerconnection created with the RtpDataChannels argument set to true and using stun:ph.tagsrvcs.com as a STUN server.

Also, we see that a data channel is created, followed by calls to createOffer and setLocalDescription. That pattern is pretty common to gather IP addresses.

Using Chrome’s devtools search feature it is straightforward to find out that the RTCPeerConnection is created in the following Javascript file:

http://s.tagsrvcs.com/2/4.10.0/loaded.js

Since it’s minified here is the de-minified snippet that is gathering the IPs:

Mt = function() { function e() { this.addrsFound = { "0.0.0.0": 1 } } return e.prototype.grepSDP = function(e, t) { var n = this; if (e) { var o = []; e.split("\r

").forEach(function(e) { if (0 == e.indexOf("a=candidate") || 0 == e.indexOf("candidate:")) { var t = e.split(" "), i = t[4], r = t[7]; ("host" === r || "srflx" === r) && (n.addrsFound[i] || (o.push(i), n.addrsFound[i] = 1)) } else if (0 == e.indexOf("c=")) { var t = e.split(" "), i = t[2]; n.addrsFound[i] || (o.push(i), n.addrsFound[i] = 1) } }), o.length > 0 && t.queue(new y("webRTC", o)) } }, e.prototype.run = function(e) { var t = this; if (c.wrip) { var n = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection; if (n) { var o = { optional: [{ RtpDataChannels: !0 }] }, i = []; - 1 == w.baseDomain.indexOf("update.") && i.push({ url: "stun:ph." + w.baseDomain }); var r = new n({ iceServers: i }, o); r.onicecandidate = function(n) { n.candidate && t.grepSDP(n.candidate.candidate, e) }, r.createDataChannel(""), r.createOffer(function(e) { r.setLocalDescription(e, function() {}, function() {}) }, function() {}); var a = 0, s = setInterval(function() { null != r.localDescription && t.grepSDP(r.localDescription.sdp, e), ++a > 15 && (clearInterval(s), r.close()) }, 200) } } }, e }(), 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 43 44 45 46 47 48 49 50 51 52 Mt = function ( ) { function e ( ) { this . addrsFound = { "0.0.0.0" : 1 } } return e . prototype . grepSDP = function ( e , t ) { var n = this ; if ( e ) { var o = [ ] ; e . split ( "\r

" ) . forEach ( function ( e ) { if ( 0 == e . indexOf ( "a=candidate" ) | | 0 == e . indexOf ( "candidate:" ) ) { var t = e . split ( " " ) , i = t [ 4 ] , r = t [ 7 ] ; ( "host" === r | | "srflx" === r ) & & ( n . addrsFound [ i ] | | ( o . push ( i ) , n . addrsFound [ i ] = 1 ) ) } else if ( 0 == e . indexOf ( "c=" ) ) { var t = e . split ( " " ) , i = t [ 2 ] ; n . addrsFound [ i ] | | ( o . push ( i ) , n . addrsFound [ i ] = 1 ) } } ) , o . length > 0 & & t . queue ( new y ( "webRTC" , o ) ) } } , e . prototype . run = function ( e ) { var t = this ; if ( c . wrip ) { var n = window . RTCPeerConnection | | window . webkitRTCPeerConnection | | window . mozRTCPeerConnection ; if ( n ) { var o = { optional : [ { RtpDataChannels : ! 0 } ] } , i = [ ] ; - 1 == w . baseDomain . indexOf ( "update." ) & & i . push ( { url : "stun:ph." + w . baseDomain } ) ; var r = new n ( { iceServers : i } , o ) ; r . onicecandidate = function ( n ) { n . candidate & & t . grepSDP ( n . candidate . candidate , e ) } , r . createDataChannel ( "" ) , r . createOffer ( function ( e ) { r . setLocalDescription ( e , function ( ) { } , function ( ) { } ) } , function ( ) { } ) ; var a = 0 , s = setInterval ( function ( ) { null ! = r . localDescription & & t . grepSDP ( r . localDescription . sdp , e ) , ++ a > 15 & & ( clearInterval ( s ) , r . close ( ) ) } , 200 ) } } } , e } ( ) ,

Let’s look at the run function first. It is creating a peerconnection with the optional RtpDataChannels constraint set to true. No reason for that, it will just unnecessarily create candidates with an RTCP component in Chrome and is ignored in Firefox.

As mentioned earlier,

stun:ph.tagsrvcs.com

is used as STUN server. From Wireshark dumps it’s pretty easy to figure out that this is running the [coturn stun/turn server](https://code.google.com/p/coturn/); the SOFTWARE field in the binding response is set to

Coturn-4.4.2.k3 ‘Ardee West’.

The code hooks up the onicecandidate callback and inspects every candidate it gets. Then, a data channel is created and createOffer and setLocalDescription are called to start the candidate gathering process.

Additionally, in the following snippet

var a = 0, s = setInterval(function() { null != r.localDescription && t.grepSDP(r.localDescription.sdp, e), ++a > 15 && (clearInterval(s), r.close()) }, 200) 1 2 3 4 var a = 0 , s = setInterval ( function ( ) { null ! = r . localDescription & & t . grepSDP ( r . localDescription . sdp , e ) , ++ a > 15 & & ( clearInterval ( s ) , r . close ( ) ) } , 200 )

the localDescription is searched for candidates every 200ms for three seconds. That polling is pretty unnecessary. Once candidate gathering is done, onicecandidate would have been called with the null candidate so polling is not required.

Lets look at the grepSDP function. It is called in two contexts, once in the onicecandidate callback with a single candidate, the other time with the complete SDP.

It splits the SDP or candidate into individual lines and then parses that line, extracting the candidate type at index 7 and the IP address at index 4.

Since without a relay server one will never get anything but host or srflx candidates, the check in following line is unnecessary. The rest of this line does eliminate duplicates however.

Oddly, the code also looks for an IP in the c= line which is completely unnecessary as this line will not contain new information. Also, looking for the candidate lines in the localDescription.sdp will not yield any new information as any candidate found in there will also be signalled in the onicecandidate callback (unless someone is using a 12+ months old version of Firefox).

Since the JS is minified it is rather hard to trace what actually happens with those IPs.

If you’re going to hack people, at least do it cleanly!

{“author”: “Philipp Hancke“}

Want to keep up on our latest posts? Please click here to subscribe to our mailing list if you have not already. We only email post updates. You can also follow us on twitter at @webrtcHacks for blog updates.