2018년, Brannon Dorsey라는 연구자가 자기 집 네트워크를 향해 웹 페이지 하나를 겨눴더니 자신의 Google Home, Chromecast, Roku, Sonos 스피커와 대화할 수 있었다. 같은 Wi-Fi에 붙은 해커의 노트북에서가 아니라, 공용 인터넷의 도메인이 띄운 브라우저 탭에서. Google Home은 8008 포트의 문서화되지 않은 REST API로 응답했고, Roku는 8060 포트의 External Control Protocol로 응답했다(이건 CVE까지 받았다: CVE-2018-11314). Google, Roku, Sonos 모두 패치를 냈다.
그가 쓴 기법은 새롭지 않았다. 투표권이 생기고도 남을 만큼 오래된 것이었다.
동일 출처 정책은 장소가 아니라 이름을 믿는다
브라우저가 사이트들 사이에서 하는 모든 보안 판단은 단 하나의 규칙 위에 서 있다. 동일 출처 정책(same-origin policy)이다. https://evil.example에서 로드된 스크립트는 https://evil.example하고만 대화할 수 있고 그 외엔 안 된다. “출처(origin)“는 스킴·호스트·포트의 세 쌍이다. 세 개가 모두 일치해야만 두 페이지가 같은 출처다.
가운데 항목을 자세히 보자. 출처의 기준은 호스트 — 이름이다. 그 이름이 해석되는 IP 주소가 아니다. 브라우저는 두 요청이 한 묶음인지를 evil.example 같은 문자열을 비교해 정하지, 그 문자열이 가리키는 기계를 비교하지 않는다.
평소엔 이름이 안정적인 주소에 매핑되니 괜찮아 보인다. 하지만 그 매핑은 브라우저가 소유하지 않는다. DNS가 소유한다. 그리고 DNS는 도메인을 운영하는 자가 통제한다 — 당신이 공격자의 사이트를 방문하고 있다면, 그건 곧 공격자다.
이게 버그의 전부다. 브라우저는 이름으로 정의된 경계를 집행하는데, 그 이름이 무엇을 뜻하는지는 공격자가 통제한다.
리바인딩은 이렇게 작동한다
당신이 evil.example을 방문한다. 그쪽 네임서버는 공격자의 진짜 공용 IP와 함께 일부러 아주 작은 TTL — 몇 초 — 을 돌려준다. 브라우저가 페이지를 로드하고, 자바스크립트가 돌기 시작하고, 출처는 evil.example, 전부 정상이다.
그다음 스크립트는 TTL이 지나길 기다렸다가 같은 출처로 또 한 번 요청한다. 캐시된 레코드가 만료된 걸 본 브라우저는 DNS에 다시 묻는다. 이번에 공격자의 네임서버는 192.168.1.1을 돌려준다. 아니면 127.0.0.1. 아니면 당신 방화벽 뒤에 있는 아무 주소나.
브라우저 입장에선 바뀐 게 없다. 여전히 evil.example, 여전히 같은 출처, 동일 출처 정책 통과. 네트워크 입장에선 모든 게 바뀌었다. 요청은 이제 당신 공유기의 관리자 페이지로, 또는 부엌에 있는 스마트 스피커의 인증 없는 API로 간다. 스크립트가 당신의 브라우저를 프록시 삼아 내부 네트워크와 대화하고 있고, 동일 출처 정책은 이름이 한 번도 움직이지 않았다는 이유로 이를 통과시켜 준다.
이 공격을 명명하고 해부한 2007년 스탠퍼드 논문 — Collin Jackson, Adam Barth, Andrew Bortz, Weidong Shao, Dan Boneh의 “Protecting Browsers from DNS Rebinding Attacks” — 은 여기에 가격표를 붙였다. 공격자가 광고비 100달러 미만으로 약 10만 개의 브라우저를 일시적으로 탈취해, 각각을 자기 네트워크로 향하는 열린 프록시로 바꿀 수 있다고 추산했다. 브라우저가 발판이고, 당신은 눈알들을 임대하는 셈이다.
뻔한 방어로는 부족한 이유
뻔한 방어는 리바인딩을 무시하는 것이다. 첫 응답을 캐시하고 짧은 TTL을 못 본 척한다. 브라우저는 실제로 이렇게 한다. DNS 피닝(pinning)이라 부른다 — 호스트명이 한 번 IP로 해석되면, 브라우저는 TTL이 뭐라 하든 한동안 그 IP에 고정한다.
피닝은 도움이 되지만 문제를 해결하진 못한다. 공격자는 그냥 핀이 풀릴 때까지 기다린다 — 페이지는 가만히 떠 있다가 다시 시도하면 된다. 또는 A 레코드를 한 번에 두 개 주고, 그중 하나가 가리키는 호스트를 내려 버려서 세션 중간에 브라우저가 두 번째(내부) 주소로 페일오버하게 만든다. Intruder의 연구진은 바로 이 다중 응답 동작을 악용해, 몇 분짜리 창을 1초 미만으로 줄이는 Chrome·Safari 대상 순간 리바인딩을 시연했다. 공격자의 타이밍을 추측해서 리바인딩을 막으려는 모든 계층은, 타이밍을 직접 고르는 공격자에게 진다.
다른 계열의 방어는 브라우저가 아니라 리졸버에서 작동한다. 안쪽을 가리키는 응답을 거부하도록 DNS를 가르치는 것이다. dnsmasq에는 오래전부터 --stop-dns-rebind가 있다. RFC 1918 대역, 127.0.0.0/8, 0.0.0.0/8이 담긴 상위 응답을 거부한다. 함정은 좋은 기본값 대부분을 망치는 그 함정과 같다: 기본으로 켜져 있지 않다. 가정용 공유기 대부분은 dnsmasq를 이 플래그 꺼진 채로 출하하고, 리졸버를 돌리는 사람 대부분은 이걸 켜지 않는다.
게다가 필터링은 그 목록만큼만 완전하다. 작년의 “0.0.0.0 day”가 그 점을 일깨웠다. 브라우저들은 127.0.0.1을 가리키는 응답을 막는 법을 배웠지만, 리눅스와 macOS에서는 0.0.0.0도 로컬호스트로 해석되는데 그건 아무도 막지 않았다. 1년 넘게 다져 온 로컬호스트 보호가, 같은 기계의 두 번째 주소 하나에 무너졌다.
진짜 교훈은 브라우저가 아니라 당신의 LAN에 있다
DNS 리바인딩을 브라우저 버그로 읽을 수 있다 — 벤더들이 계속 덧대고 있는, 동일 출처 정책의 오래된 구멍으로. Chrome의 Private Network Access 작업이 가장 최근의 덧댐이다. 공용 인터넷의 페이지가 사설 주소에 닿기 전에 CORS 스타일의 프리플라이트를 보내게 하고, 기기가 옵트인해야만 허용한다. 진짜 개선이다. 동시에 엉뚱한 경기장에서 수비하는 일이기도 하다.
불편한 부분은 리바인딩이 브라우저 뒤에 있는 모든 것에 관해 드러내는 사실이다. 공유기 관리자 페이지, 프린터의 설정 서버, 스마트 스피커의 제어 API, “어차피 로컬이니까”라며 localhost에 묶어 둔 데이터베이스 — 이들 하나하나가 요청이 어디서 왔는가를 이유로 인증을 건너뛰어도 안전하다고 판단했다. 내부 = 신뢰. DNS 리바인딩이 존재하는 이유는 바로 그 가정이 틀렸기 때문이다. 공용 인터넷의 공격자는 당신이 탭을 열 때마다 내부 자리를 빌릴 수 있다.
네트워크 위치 신뢰를 대체하기로 되어 있는 원칙에는 이름이 있고, 업계는 그 이름을 마케팅 자료에서 닳도록 써먹었다. 하지만 버즈워드를 걷어 내면, 리바인딩은 그 원칙이 왜 옳은지를 보여 주는 가장 깨끗한 증명이다. 네트워크는 경계(perimeter)가 아니다. “LAN에서만 닿을 수 있음”은 접근 제어가 아니다. 실제로 버티는 해법은 더 똑똑한 TTL 휴리스틱도, 안쪽을 가리키는 주소의 더 긴 차단 목록도 아니다 — LAN 위의 그 물건이, 시키는 대로 하기 전에 누가 부르는지를 물었어야 한다는 것이다.
로컬 네트워크가 벽이라고 가정한 모든 기기는 20년 넘게 틀려 왔다. 브라우저는 그걸 계속 증명하고 있을 뿐이다.