7 분 소요

📦 DDoS 완전정복 6부작 — DDoS가 무엇이고 공격이 어떻게 분류되는지 큰 그림을 그리고(1편), 대역폭을 말려버리는 볼류메트릭·반사증폭 공격(2편), 연결 테이블을 터뜨리는 프로토콜·상태 고갈 공격(3편), 정상 요청처럼 굴며 서버를 태우는 애플리케이션 계층(L7) 공격(4편), 이 모든 걸 탐지하고 막아내는 방어·완화(5편), 그리고 세운 방어가 실제로 버티는지 내 사이트에서 합법적으로 검증하는 내성 테스트(6편)까지 이어집니다. 전체 6편.
  1. DDoS 완전정복 — 개념과 공격 지형도
  2. 볼류메트릭·반사증폭 공격 — 대역폭을 말려버리는 방법
  3. 프로토콜·상태 고갈 공격 — 연결 테이블을 터뜨리기
  4. 애플리케이션 계층(L7) 공격 — 정상 요청처럼 굴며 서버를 태우기지금 글
  5. DDoS 방어와 완화 — 탐지부터 스크러빙까지
  6. 내 사이트 DDoS 내성 테스트 — 합법적으로, 제대로

Summary

앞선 글들에서는 회선을 채워버리는 볼류메트릭 공격과 연결 테이블을 고갈시키는 프로토콜 공격을 살펴봤습니다. 이번에는 한 단계 위로 올라가서, 겉으로는 완벽하게 정상적인 HTTP 요청처럼 보이면서 서버의 CPU와 커넥션 슬롯을 조용히 태우는 애플리케이션 계층(L7) 공격을 정리해 보려고 합니다. L7 공격이 특히 성가신 이유는 대역폭이 거의 필요 없고, 진짜 사용자의 트래픽과 겨우 구분될 정도로 정교하기 때문이에요. 원리를 이해하면 왜 방화벽만으로는 막기 어려운지가 자연스럽게 보입니다.

💡 이 글에서 다루는 것

  • HTTP 플러드가 GET·POST로 서버를 어떻게 태우는지
  • 랜덤 쿼리스트링으로 캐시를 우회해 원본까지 관통하는 원리
  • Slowloris·RUDY·Slow Read 같은 low-and-slow의 커넥션 점유 방식
  • 2023년 HTTP/2 Rapid Reset(CVE-2023-44487)이 멀티플렉싱을 악용한 구조
  • 요청 비용과 처리 비용의 비대칭이 왜 공격을 효율적으로 만드는지



1. L7 공격이 까다로운 이유

볼류메트릭 공격이 “고속도로를 차로 꽉 막는” 그림이라면, L7 공격은 “정상 손님인 척 매장에 들어와 점원에게 계속 복잡한 주문만 시키는” 쪽에 가깝습니다. 회선을 물리적으로 막는 게 아니라, 서버가 요청을 하나하나 처리하느라 지치게 만드는 방식이에요.

이 공격이 방어자 입장에서 유독 까다로운 이유는 세 가지로 정리됩니다.

첫째, 대역폭이 거의 필요 없습니다. 초당 수백 Gbps를 쏟아붓는 볼류메트릭과 달리, L7은 초당 수만 건의 정상 크기 요청만으로도 서버를 무너뜨려요. 공격자 입장에서 훨씬 싸게 먹힙니다.

둘째, 정상 요청과 구분이 어렵습니다. 요청 자체는 규격에 맞는 HTTP예요. 패킷을 뜯어봐도 헤더가 멀쩡하고, 심지어 봇넷이 실제 브라우저의 User-Agent와 쿠키까지 흉내 냅니다. IP 하나만 보고 차단하기 어려워요.

셋째, 비용이 지독하게 비대칭입니다. 공격자가 요청 한 줄을 만드는 비용은 거의 0에 수렴하는데, 그 요청을 받은 서버는 데이터베이스 조회, 템플릿 렌더링, 암호화 연산까지 돌려야 할 수 있습니다. 이 비대칭이 L7 공격의 핵심 엔진이에요.



2. HTTP 플러드 — GET과 POST

가장 직관적인 L7 공격은 HTTP 요청을 대량으로 퍼붓는 HTTP 플러드입니다. 방식은 크게 GET과 POST로 나뉘어요.

GET 플러드는 페이지나 리소스를 반복해서 요청합니다. 정적 파일이라면 큰 부담이 아닐 수 있지만, 매번 데이터베이스를 조회해 렌더링하는 동적 페이지를 노리면 얘기가 달라져요. 검색 결과 페이지, 상품 목록, 개인화된 대시보드처럼 요청 한 번에 서버가 많은 일을 하는 곳이 표적이 됩니다.

POST 플러드는 한 걸음 더 나아갑니다. 폼 제출, 로그인, 검색 질의처럼 서버가 입력을 받아 무거운 처리를 하는 엔드포인트를 겨냥해요. POST는 본문을 파싱하고, 검증하고, 종종 쓰기 작업까지 유발하기 때문에 요청당 처리 비용이 GET보다 훨씬 큽니다.


두 방식 모두 핵심은 “무거운 엔드포인트를 골라서 때린다”는 점이에요. 공격자는 사이트를 미리 훑어보고 가장 느리게 응답하는 경로, 즉 서버 자원을 가장 많이 먹는 지점을 찾아냅니다.

아래는 방어자 관점에서 요청이 어떻게 보이는지를 개념적으로 표현한 의사코드예요.

정상 사용자:  페이지 열람 → 몇 초 읽기 → 다음 요청  (요청 간격 넉넉)
봇넷 노드:    무거운 검색 요청 → 즉시 반복 → 즉시 반복  (간격 거의 0)

관찰 포인트:
- 요청은 규격에 맞는 정상 HTTP
- User-Agent, 쿠키, Referer 모두 그럴듯함
- 그러나 "행동 패턴"이 사람 같지 않음 (읽는 시간이 없음)

봇넷이 진짜 브라우저 헤더를 흉내 내기 때문에, 단순히 요청 하나만 봐서는 정상인지 아닌지 판단하기 어려워요. 결국 개별 패킷이 아니라 시간에 걸친 행동 패턴을 봐야 구분이 됩니다.



3. 캐시 우회(cache-busting)

많은 서비스가 CDN이나 캐시 계층을 앞에 두어 원본 서버의 부담을 덜어 줍니다. 같은 페이지 요청이 여러 번 와도 캐시가 대신 응답해 주니 원본은 편해져요. L7 공격자는 바로 이 캐시를 무력화하려고 합니다.

방법의 핵심 아이디어는 매 요청마다 URL을 조금씩 다르게 만드는 것이에요. 예를 들어 같은 페이지라도 뒤에 의미 없는 랜덤 쿼리스트링을 붙이면, 캐시 입장에서는 매번 처음 보는 새 URL로 인식합니다.

캐시 히트되는 요청:   /product/123
캐시 우회 요청:       /product/123?_=8f3a1c
                     /product/123?_=b72e90
                     /product/123?_=1d40ff
→ 매번 다른 URL로 인식 → 캐시 miss → 원본 서버까지 요청 관통

이렇게 되면 캐시가 방패 역할을 못 하고, 모든 요청이 원본 서버로 흘러 들어갑니다. 원본은 매번 새로 페이지를 만들어 내야 하니 부하가 그대로 전달돼요. 개념 자체는 단순하지만, CDN을 믿고 원본 용량을 얇게 잡아 둔 서비스에게는 치명적입니다.

⚠️ 그래서 방어를 설계할 때 “캐시가 앞에 있으니 원본은 괜찮겠지”라는 가정은 위험합니다. 캐시 키 정책과 원본 보호를 함께 설계해야 해요.



4. Low-and-slow — 느리게 오래 붙잡기

지금까지가 “빠르게 많이”라면, low-and-slow는 정반대예요. 아주 적은 트래픽으로, 요청을 일부러 느리게 끌면서 서버의 커넥션을 오래 붙잡아 두는 방식입니다. 트래픽 양으로는 거의 눈에 띄지 않기 때문에 볼류메트릭 탐지에 걸리지 않아요.


대표적인 세 가지 결이 있습니다.

Slowloris는 HTTP 요청 헤더를 끝내지 않고 찔끔찔끔 보냅니다. HTTP 요청은 빈 줄이 와야 “헤더 끝”으로 인정되는데, 그 빈 줄을 절대 보내지 않고 가끔 헤더 한 줄씩만 흘려보내요. 서버는 “아직 요청이 오는 중”이라고 여겨 커넥션을 계속 열어 둡니다. 이런 연결을 수백 개 만들어 두면 워커 슬롯이 전부 대기 상태로 묶여요.

정상 요청:
  GET / HTTP/1.1
  Host: example.com
  (빈 줄)  ← 여기서 헤더 완성, 서버가 처리 시작

Slowloris 개념:
  GET / HTTP/1.1
  Host: example.com
  X-a: 1        ← 몇 초 뒤
  X-b: 2        ← 또 몇 초 뒤
  ...           ← 빈 줄을 영원히 안 보냄
  → 서버는 "헤더 수신 중"으로 커넥션을 계속 유지

RUDY(R-U-Dead-Yet)는 같은 아이디어를 POST 본문에 적용합니다. Content-Length로 “본문이 이만큼 온다”고 예고해 놓고, 실제 본문은 한 바이트씩 아주 느리게 흘려보내요. 서버는 본문이 다 올 때까지 기다리며 커넥션을 붙잡습니다.

Slow Read는 방향을 뒤집습니다. 요청은 정상적으로 보내되, 서버의 응답을 받아 가는 속도를 극단적으로 늦춰요. TCP 수신 윈도우를 아주 작게 광고해서, 서버가 응답을 다 못 보내고 버퍼를 쥔 채 대기하게 만듭니다.


세 방식 모두 스레드·프로세스 기반 서버, 즉 커넥션 하나당 워커 하나를 붙잡아 두는 구조에서 특히 위력적이에요. 워커 수는 유한한데 그 워커들이 전부 “느린 손님”을 기다리느라 묶여 버리면, 정작 진짜 사용자는 빈 워커가 없어 응답을 못 받습니다. 이벤트 기반 비동기 서버는 상대적으로 견디지만, 그래도 커넥션 슬롯과 타임아웃 설정이 허술하면 무너질 수 있어요.



5. HTTP/2 Rapid Reset (CVE-2023-44487)

2023년 가을에 관측된 Rapid Reset은 HTTP/2의 설계를 정면으로 파고든 공격입니다. 당시 여러 사업자가 초당 수억 건 규모의 요청을 기록했고, 그때까지의 L7 공격 최대치를 크게 갈아치웠어요.

원리를 이해하려면 HTTP/2의 멀티플렉싱을 먼저 떠올려야 합니다. HTTP/1.1은 커넥션 하나에서 요청을 대체로 순차 처리했지만, HTTP/2는 하나의 커넥션 안에서 여러 스트림을 동시에 열어 요청을 병렬로 주고받아요. 여기에 클라이언트가 언제든 스트림을 취소할 수 있는 RST_STREAM 프레임이 있습니다.

Rapid Reset은 이 둘을 결합합니다.

공격 개념:
  1. 스트림 열기 (HEADERS 프레임으로 요청 시작)
  2. 즉시 취소 (RST_STREAM 프레임 전송)
  3. 취소하자마자 새 스트림 열기 → 다시 즉시 취소
  4. 하나의 커넥션에서 이 사이클을 초당 수천~수만 회 반복

핵심:
- 클라이언트는 스트림을 열고 바로 버려서 비용이 거의 없음
- 서버는 각 요청을 실제로 처리하기 시작한 뒤 취소를 받음
- 취소가 와도 서버 쪽 작업은 이미 자원을 소모한 뒤

포인트는 취소의 타이밍이에요. 클라이언트가 RST_STREAM을 보내면 스트림은 닫히지만, 서버는 이미 그 요청을 백엔드로 넘겨 처리를 시작한 상태입니다. 게다가 스트림이 곧바로 닫히니 HTTP/2의 동시 스트림 수 제한에도 걸리지 않아, 한 커넥션에서 요청을 사실상 무제한으로 찍어낼 수 있었어요. 적은 커넥션과 대역폭으로 어마어마한 요청량을 만들어 낸 배경이 여기에 있습니다.

이 취약점은 HTTP/2 구현들이 취소된 스트림의 처리 비용과 생성 속도를 제대로 제한하지 못한 데서 비롯됐고, 이후 각 서버·프록시 구현이 “취소 폭주”를 감지해 커넥션을 끊는 방식으로 패치됐습니다.



6. DNS 질의 플러드와 기타

L7 공격은 웹 서버에만 국한되지 않습니다. 애플리케이션 계층에서 무거운 처리를 유발할 수 있는 곳이라면 어디든 표적이 돼요. 몇 가지만 짧게 짚어 보겠습니다.

  • 권한 DNS 질의 플러드: 특정 도메인의 권한 DNS 서버에 질의를 폭주시킵니다. 특히 존재하지 않는 무작위 서브도메인을 계속 물으면(NXDOMAIN 유발) 캐시가 안 먹혀 권한 서버가 매번 응답하느라 지쳐요.
  • WordPress pingback 반사: 취약하게 설정된 워드프레스 사이트들의 pingback 기능을 악용해, 다수의 정상 사이트가 표적에게 요청을 보내도록 유도하는 반사 공격입니다.
  • TLS 재협상 부하: TLS 핸드셰이크와 재협상은 서버 쪽 연산 비용이 큽니다. 이 협상을 반복 요청해 CPU를 소모시키는 방식도 있어요.

공통점은 역시 비대칭이에요. 공격자가 유발하는 요청은 가볍지만, 서버가 감당해야 하는 연산은 무겁습니다.



7. 비대칭성 정리

L7 공격을 관통하는 하나의 원리는 “요청을 만드는 비용 ≪ 요청을 처리하는 비용”이라는 비대칭입니다. 대표적인 사례를 요청 쪽과 서버 쪽으로 나눠 보면 확 와닿아요.

요청 쪽 비용 (공격자) 서버 쪽 비용 (방어자)
GET 한 줄 전송 DB 조회 + 렌더링
랜덤 쿼리스트링 붙이기 캐시 miss → 원본 재생성
헤더 한 줄 흘리기 워커 슬롯 장시간 점유
스트림 열고 즉시 취소 백엔드 처리 착수 후 낭비
TLS 협상 요청 서버 측 암호 연산 폭증

이 표가 말해 주는 건 명확합니다. 공격자는 값싼 행동 하나로 서버에게 몇 배, 몇십 배의 일을 떠넘길 수 있어요. 대역폭 싸움이 아니라 “누가 더 효율적으로 상대를 지치게 하느냐”의 싸움이라서, 볼류메트릭보다 훨씬 적은 자원으로도 서비스를 무너뜨릴 수 있는 것입니다. L7 공격이 꾸준히 진화하는 이유이기도 해요.



마치며

L7 공격은 정상 요청의 탈을 쓴 채 서버의 CPU와 커넥션을 태우는, 비대칭성을 극한까지 활용한 방식이었어요. HTTP 플러드부터 캐시 우회, low-and-slow, 그리고 HTTP/2 Rapid Reset까지 훑어보면서 왜 방화벽 하나로는 막기 어려운지가 조금 선명해졌으리라 생각합니다. 핵심은 개별 요청이 아니라 행동과 패턴을 봐야 한다는 점이에요.

일단 오늘은 여기까지…..
다음 글에서는 지금까지의 모든 공격을 어떻게 탐지하고 막아내는지, 방어와 완화를 총정리할게요.



← 이전 글: (3/6) 프로토콜·상태 고갈 공격 — 연결 테이블을 터뜨리기다음 글 →: (5/6) DDoS 방어와 완화 — 탐지부터 스크러빙까지


참고: Cloudflare Learning — HTTP flood attack · Cloudflare — HTTP/2 Rapid Reset (CVE-2023-44487) · Cloudflare — Low and slow attack