우리는 이메일 위에 인증을 20년 동안 쌓아 올렸다. 어느 서버가 도메인을 위해 발송할 수 있는지 말하는 SPF. 메시지에 서명해 변조가 드러나게 하는 DKIM. 둘을 묶어 서로 어긋날 때 수신자가 어떻게 할지 알려주는 DMARC. 기계 장치가 꽤 많고, 이게 다 맞아떨어질 때는 진짜 질문에 답한다: 이 메시지는 정말로 도메인 소유자가 승인한 서버에서, 변조 없이 왔는가?
2023년 12월, SEC Consult의 Timo Longin이 이 셋을 한꺼번에 지나쳐 버리는 기법을 공개했다. 그는 대형 제공업체의 주소를 사칭하는 메일을 보냈고, 그 메일은 SPF·DKIM·DMARC가 모두 pass로 찍힌 채 도착했다. “설정이 잘못됐다”가 아니다. “도메인 레코드가 약했다”도 아니다. 검사는 실행됐고, 올바르게 평가됐으며, 위조된 메시지에 pass를 돌려줬다. 그는 이걸 SMTP 스머글링이라 불렀고, 이게 통하는 이유는 뒤늦게 보면 뻔하지만 한 번 보고 나면 오싹한 그런 것이다: 그 모든 인증 기계 장치 중 어느 것도 한 이메일이 어디서 끝나고 다음이 어디서 시작하는지에 대해서는 발언권이 없다.
”나 끝났어”를 뜻하는 그 줄
SMTP는 대화다. 클라이언트가 접속해 메일이 누구에게서 왔고 누구에게 가는지 말한 뒤, DATA라고 말하고 메시지 본문을 스트리밍한다. 본문은 길이가 제각각이니 “메시지가 끝났다”는 합의된 신호가 있어야 한다. 그 신호는 점 하나만 있는 줄이고, 앞뒤로 캐리지리턴-라인피드가 감싼다: <CR><LF>.<CR><LF>. 서버는 정확히 그 시퀀스를 볼 때까지 바이트를 읽고, 그걸 보면 메시지가 끝났으며 그 뒤는 새 명령임을 안다.
그 end-of-data 표시가 승부의 전부다. 그 앞은 전부 한 이메일의 내용이다. 그 뒤는 전부 다음 SMTP 트랜잭션 — 새로운 MAIL FROM, RCPT TO, DATA — 이다. 두 서버가 그 표시가 무엇인지를 두고 언젠가 의견이 갈리면, 거기에 이음매가 생기고, 이음매는 스머글링하는 자리다.
의견 차이는 이렇다. 표준은 종료 표시가 <CR><LF>.<CR><LF>라고 말한다. 하지만 서버들은 수십 년간 줄바꿈에 관대해 왔는데, 세월이 흐르며 클라이언트들이 줄바꿈을 엉성하게 다뤄 왔기 때문이다. 어떤 서버는 홀로 된 <LF>.<LF>나 외로운 <CR>.<CR>도 end-of-data로 받아들인다. 다른 서버는 그러지 않고 — 정확한 정규 시퀀스를 기다린다. 어느 쪽도 그 자체로는 미친 짓이 아니다. “받아들일 때는 관대하라”는 사실상 인터넷 프로토콜의 비공식 좌우명이다. 하지만 발신 서버와 수신 서버가 그 선을 서로 다른 자리에 긋는 순간, 공격자는 둘이 서로 다른 조각으로 자르는 메시지를 회선에 올릴 수 있다.
서버 둘, 읽기 둘, 위조 메일 하나
공격자가 실제로 하는 일을 따라가 보자. 그는 발신 서버가 정확한 정규 종료 표시만을 고집하는 어떤 제공업체에 정상 계정을 갖고 있다 — 이게 발신 측이다. 그는 메시지 하나를 작성하고 그 본문 안에 <LF>.<LF> 같은 시퀀스를 파묻은 뒤, 그 뒤에 완전한 두 번째 SMTP 트랜잭션을 붙인다: 또 다른 MAIL FROM, 또 다른 RCPT TO, 그리고 공격자가 사칭하고 싶은 회사의 CEO 같은 이가 보낸 것으로 위조한 이메일이 담긴 DATA 블록.
발신 서버는 그 파묻힌 <LF>.<LF>를 보고 메시지의 끝으로 취급하지 않는다 — 엄격한 해석으로는 <CR><LF>.<CR><LF>만이 메시지를 끝내므로, 그건 본문 안의 그냥 바이트일 뿐이다. 그래서 발신 서버는 그 전체를 한 통의 이메일로, 자기의 신뢰받는 연결을 통해 수신 서버로 중계한다. 그리고 제공업체 자신의 정상 발신 서버가 그 연결을 맺으므로, 그 트랜잭션은 SPF(접속 IP가 승인됨), DKIM(제공업체가 서명함), DMARC(둘이 정렬됨)를 통과한다. 봉투는 진짜로 그 제공업체의 것이다.
그다음 수신 서버가 같은 바이트를 읽는다 — 그런데 이쪽은 홀로 된 <LF>.<LF>를 end-of-data로 인정하는 서버 중 하나다. 그래서 바로 그 바이트에서 첫 메시지가 끝났다고 결정한다. 그 뒤의 모든 것은, 이 서버 눈에는, 새로운 SMTP 명령 시퀀스가 된다: 스머글된 MAIL FROM, RCPT TO, DATA. 발신 제공업체는 자기가 보내는 줄도 몰랐던 두 번째 이메일을, 공격자가 쓴 그대로의 주소로 받아들이고 배달한다. 그 두 번째 메시지는 신뢰받는 연결을 타고 들어왔으니, 수신 서버 로그에선 같은 인증 맥락을 물려받는다. SPF pass. DKIM pass. DMARC pass. 위조물에 대해.
아무도 서명을 깨지 않았다. 아무도 키를 알아맞히지 않았다. 공격자는 “이 이메일은 어디서 끝나는가”가 파싱 결정이라는 사실, 그리고 두 파서가 그걸 다르게 내렸다는 사실을 이용했을 뿐이다.
인증 스택이 이걸 잡을 수 없었던 이유
이 대목을 자꾸 곱씹게 되는데, 이메일을 훌쩍 넘어서는 교훈이기 때문이다. SPF·DKIM·DMARC는 모두 하나의 메시지와 하나의 봉투 위에서 정의된다. 이들은 프레이밍 문제가 이미 해결됐다고 가정한다 — 즉, 자기들이 인증하는 메시지가 어느 바이트로 구성되는지에 모두가 동의한다고. 이들은 상자의 내용물을 검증한다. 상자의 벽이 어디인지 누가 정했는가에 대해서는 할 말이 없다.
SMTP 스머글링은 내용물이 아니라 벽을 공격한다. DKIM이 서명을 검사할 즈음이면, 수신 서버는 이미 이것을 별개의 메시지로 결정한 상태다. 그래서 스머글된 봉투(진짜다)와 그 안의 사기성 본문(서명은 애초에 그 위에서 계산된 적이 없다)을 인증한다. 모든 검사가 제 일을 완벽히 하는데도 합쳐진 답은 여전히 거짓이다. 이건 HTTP 리퀘스트 스머글링과 정확히 같은 모양인데, 그것도 같은 의견 차이 위에서 터졌다 — 프런트엔드와 백엔드 프록시가 Content-Length와 Transfer-Encoding을 다르게 파싱해 하나의 HTTP 스트림을 둘로 자른 것이다. 같은 부류의 버그, 다른 프로토콜. 체인의 두 시스템이 프레이밍을 다르게 파싱하면, 하류의 어떤 인증도 당신을 구하지 못한다. 애초에 같은 단위를 인증하고 있는 게 아니니까.
이 취약점들은 실제 소프트웨어에 실제 CVE로 붙었다: Postfix에 CVE-2023-51764, Sendmail에 CVE-2023-51765, Exim에 CVE-2023-51766 — 인터넷 메일의 아주 큰 몫을 담당하는 메일 전송 에이전트들이다. GMX, Microsoft, Cisco Secure Email을 포함한 제공업체들이 공개 시점에 취약함이 드러났다. 규모는 잘못 설정된 호스트 몇 개가 아니라, 대부분의 메일 서버가 돌리는 소프트웨어의 기본 동작이었다.
해결책은 관대함을 멈추는 것
처방은 화려하지 않고 정확히 옳다: 비표준 end-of-data를 그만 받아들이는 것. 서버들은 메시지 스트림 안의 홀로 된 줄바꿈을 거부하도록 패치됐다 — Postfix는 프로토콜이 <CR><LF>를 요구하는 자리의 외로운 <LF>를 거부하는 smtpd_forbid_bare_newline을 추가했고, 다른 MTA들도 동등한 변경을 했다. 다시 말해, 해결책은 메시지가 어디서 끝나는지 결정하는 그 한 시퀀스에 대해 엄격해지고, 수십 년의 관대한 파싱이 정상으로 만들어 버린 엉성한 지름길들을 그만 인정하는 것이다.
여기 불편한 교훈이 있다. 30년 동안 못된 클라이언트와도 이메일이 돌아가게 해 준 그 관대함 — 홀로 된 줄바꿈을 받아들여라, 캐리지리턴에 까다롭게 굴지 마라 — 이 바로 누군가가 인증 스택 전체를 지나쳐 위조하게 해 준 그 관대함이다. “받아들일 때는 관대하라”는 포스텔의 법칙은 지혜처럼 읽히지만, 두 시스템이 조금씩 다른 방식으로 관대한 모든 자리가 공격자가 비집어 벌릴 수 있는 자리라는 걸 알아채기 전까지만 그렇다. 프레이밍은 보안 계층 아래의 사소한 디테일이 아니다. 프레이밍 이 보안 계층이고, 그건 너무 저수준이라 중요하지 않게 느껴져서 아무도 지켜보지 않던 그 계층이다. 중요했다.