HTTP/2 Rapid Reset CVE-2023-44487

HTTP/2 Rapid Reset 공격

HTTP/2 Rapid Reset 공격에 대해서 요약하자면 다음과 같다.

HTTP/1.1에서와 다르게 단일 TCP 연결을 통해 병렬 스트림 형태로 요청을 다중화하는, 동시 스트림 형태로 나타난다.
또한, 요청을 중단하려는 클라이언트는 데이터 교환을 중지하기 위해 RST_STREAM 프레임을 발행한다.

따라서 HTTP/2 Rapid Rest 공격은 연속으로 요청을 보내고 취소함으로써 서버의 동시 스트림 최대치를 우회하고 구성된 임계값에 도달하지 않고 서버를 과부화로 만든다.

HTTP/1.1 and HTTP/2 request and response pattern (출처)

RST

HTTP/2에서는 각 HTTP 메세지에 다음과 같이 프레임 조합으로 직렬화한다.

typelengthflagstream idpayload

클라이언트가 1개의 HEADERS 프레임을 보내면 서버는 1개의 Headers 프레임과 뒤이어 1개 이상의 DATA 프레임으로 응답한다.
클라이언트가 요청하는 스트림 ID는 항상 홀수로, 응답은 순서와 관계없이 전달되고 다른 스트림의 프레임이 인터리빙 될 수 있다.

DATA Frame {
    Length (24),
    Type (8) = 0x00,
    
    Unused Flags (4),
    PADDED Flag (1),
    Unused Flags (2),
    END_STREAM Flag (1),
    
    Reserved (1),
    Stream Identifier (31),
    
    [Pad Length (8)],
    Data (..),
    Padding (..2040),
}

HEADERS Frame {
    Length (24),
    Type (8) = 0x01,
    
    Unused Flags (2),
    PRIORITY Flag (1),
    Unused Flag (1),
    PADDED Flag (1),
    END_HEADERS Flag (1),
    Unused Flag (1),
    END_STREAM Flag (1),
    
    Reserved (1),
    Stream Identifier (31),
    
    [Pad Length (8)],
    [Exclusive (1)],
    [Stream Dependency (31)],
    [Weight (8)],
    Field Block Fragment (..),
    Padding (..2040),
}
HTTP/2 Stream (출처)

이러한 스트림의 다중화와 동시성, Prioritization을 통해서 클라이언트의 대규모 병렬 작업을 가능하게 한다. 따라서 클라이언트에 비해 서버 리소스가 엄청 쓰일 수 있다는 부분이 DoS 공격에 취약할 수 있다.
이러한 부분을 방어하기 위해서 MAX_CONCURRENT_STREAMS 설정을 통해서 서버의 최대 동시성을 제시하며, 그 이상의 요청이 오게되면 RST_STREAM을 사용하는 서버 측에서 거부하게 된다.

HTTP/2 Server, Client Stream View (출처)

위와 같이 클라이언트와 서버에서 스트림 상태를 뷰로 관리하게 되는데 HEADERS, DATA, RST_STREAM 프레임이 전송되거나 수신할 때 상태가 변하게 된다.
여기서 Half-closed란, 클라이언트와 서버가 다른 상태로 다음과 같다.

  • Server-side half-closed: 어떤 프레임이든 보낼 수 있으나, WINDOW_UPDATE, PRIORITY, RST_STREAM만 받을 수 있는 상태
  • Client-side half-closed: 어떤 프레임이든 받을 수 있으나, 더 이상 HEADERS, DATA 프레임는 못 보내며 WINDOW_UPDATE, PRIORITY 또는 RST_STREAM 프레임만 보낼 수 있는 상태

open, half-closed 상태의 스트림은 엔드포인트에서 정의된 즉, 서버에서 정의된 최대 스트림 수에 포함이 된다. closed 상태까지 더하여 위에서 이야기한 스트림의 상태는 모두 MAX_CONCURRENT_STREAMS 설정 한도에 포함된다.

HTTP/2 요청 취소

HTTP/2 RST Process (출처)

클라이언트에서 전체 연결을 끊지 않고 단일 스트림에 대한 RST_STREAM 프레임을 보낼 수 있다. 해당 스트림에 대해서만 서버 입장에서 요청 처리와 응답을 중단하여 리소스와 대역폭 낭비를 방지할 수 있다.

HTTP/2 Stream (출처)

예를 들어, 스트림 1, 3, 5에 대해서 요청을 하고 스트림 1을 취소했다는 가정을 해보자. 서버는 응답 준비하기 전 RST_STREAM 프레임을 읽어보고 스트림 3, 5에 대해서만 응답한다.
이렇게 되면 END_STREAM 플래그가 1로 설정된 HEADERS가 클라이언트를 idle -> open, half-closed로 전환 시킨 뒤, RST_STREAM이 closed로 전환시킨다.

즉, 클라이언트가 스트림을 취소하고 다른 스트림을 즉시 열어서 요청을 보낼 수 있게 된다. 이 로직이 HTTP/2 Rapid Reset 공격을 가능하게 한다.

DoS (Denial of Service)

서버가 클라이언트가 보낸 RST_STREAM 프레임을 충분히 처리할 수 있으면 문제가 되지 않지만, 서버 입장의 너무 많은 백로그를 만들어서 리소스 소모로 이어질 수 있다.

클라우드를 이용해서 배포하게되면 일반적으로, HTTP 트래픽은 게이트웨이와 같은 프록시 혹은 부하분산기를 통해서 서버에 전달되는데, 대부분 중간에 버퍼를 두게 된다.
이 때, 요청이 들어오면 버퍼에 넣고, 순서대로 HEADERS와 DATA 프레임을 서버에 전달하게 된다. 만약 RST_STREAM 프레임을 받게 된다면, 해당 프레임 ID에 대한 요청 처리를 중지하고 서버에 알림을 보낸다.
이러한 과정을 버퍼를 비울 때까지 반복하게 된다.

악의적인 클라이언트에게서 많은 양의 요청과 취소를 보내게 되면, 서버는 버퍼에서 넘어오는 트래픽을 읽다가 취소하는 행위를 반복하게 된다. 가장 큰 문제점은 HTTP/2의 MAX_CONCURRENT_STREAMS 설정 값과 무관하게 많은 트래픽을 생성할 수 있다는 점이다.

야기되는 문제점

트래픽이 서버의 한계 이상으로 들어가게 된다면, 요청 처리를 못하게 되는 현상이 일어나겠지만, 클라우드를 통해서 배포된 서비스의 경우에는 버퍼에서 여러가지 로직을 구현하게 된다. 이 때, 각 역할의 버퍼들이 가득차게 되면 다음과 같은 에러를 야기할 수 있다.

  • 502 Bad Gateway
  • 499 Client Closed Request

대응

대부분 오픈소스들의 경우 2023/10/10에 hotfix로 업데이트를 진행했다. 사용하고 있는 HTTP/2 라이브러리들을 체크한 뒤 업데이트를 진행할 필요가 있다.

물론 클라우드를 사용하게 된다면, DDoS에 대해 대비가 되어있어서 어느정도 방지는 가능하겠으나 다른 방식으로 취약점을 악용할 수 있으므로 업데이트를 권장한다.

Github Reviewed CVE-2023-44487를 통해서 더 많은 프로젝트들의 업데이트를 볼 수 있다.


이전에 존재했던 HTTP/2 관련 CVE들에 대해서는 이후에 HTTP/2를 공부하기 위해서 같이 풀어볼려고 한다.

Ref

이 시리즈의 게시물

댓글