HTTP/2 H2C Smuggling

Hi hackers and bugbounty hunter, This is written in Korean for Koreans. if you use english, look at the bishopfox’s original post is the best!

최근에 bishopfox에서 h2c smuggling 에 대한 이야기를 공유하였습니다. 작년에 나왔었던 WebSocket Connection Smuggling과 비슷하기도 하고 재미있는 부분들이 있어서 한글로 풀어봅니다.

HTTP/2 프로토콜과 H2C Switching

h2c는 웹 프로토콜인 HTTP/2로의 Switching 단계에서 사용하는 헤더로 먼저 HTTP/2에 대한 내용을 하나 알고가야합니다. HTTP/2 프로토콜은 클라이언트와 서버 모두 HTTP/2를 지원해야 사용할 수 있습니다. (RFC-7540)

Request

GET / HTTP/1.1
Host: test.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c

Response - HTTP/2 지원하지 않는 서버

서버가 무시함

Response - HTTP/2 지원하는 서버

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
[ HTTP/2 connection ...

RFC-7540을 읽어보면 아시겠지만, HTTP/2 통신은 기본적으로 웹 소켓 통신과 많이 유사합니다. 클라이언트(웹브라우저)는 웹 서버와의 HTTP/2 통신을 하기 위해 서버에서 HTTP/2 지원 여부를 물어보고, 그에 따라서 HTTP/2를 통신을 사용할지, HTTP/1.x 통신을 사용할지 결정합니다.

여기서 먼저 하나 알고가야할 것은 HTTP/2 통신은 7 Layer(Application) 에서 수행되는 프로토콜이고, TCP Connection을 사용합니다. 그래서 기존 HTTP 통신과는 다르기 때문에 웹 소켓과 같이 지원여부 체크 후 프로토콜 변환인 101 Switching protocol을 사용합니다. (uri는 http와 동일하게 http->80, https->443을 사용합니다.)

An HTTP/2 connection is an application-layer protocol running on top
   of a TCP connection ([TCP]).  The client is the TCP connection
   initiator.

   HTTP/2 uses the same "http" and "https" URI schemes used by HTTP/1.1.
   HTTP/2 shares the same default port numbers: 80 for "http" URIs and
   443 for "https" URIs.  As a result, implementations processing
   requests for target resource URIs like "http://example.org/foo" or
   "https://example.com/bar" are required to first discover whether the
   upstream server (the immediate peer to which the client wishes to
   establish a connection) supports HTTP/2.

   The means by which support for HTTP/2 is determined is different for
   "http" and "https" URIs.  Discovery for "http" URIs is described in
   Section 3.2.  Discovery for "https" URIs is described in Section 3.3.

http/1.x -> http/2로 업그레이드를 위해선 http request의 Upgrade 헤더에 indicator와 HTTP2-Settings 헤더를 전달합니다. indicator 종류는 HTTP는 h2c HTTPS는 h2 를 의미합니다. 즉 Upgrade: h2c 는 HTTP/2를 평문 통신하겠다는 의미죠.

GET / HTTP/1.1
Host: test.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

이후 HTTP/2를 지원하게 되면 클라이언트에겐 101 Switching protocol을 전달하고 TLS Connection(HTTP/2)을 사용하여 클라이언트와 통신합니다. 이 때 프로토콜에 대한 합의는 TLS-ALPN(Application-Layer-Protocol Negotiation)을 사용합니다. 이 과정에서 APLN extension으로 클라이언트가 서버에게 버전의 리스트를 제공하고, 서버는 하나를 선택합니다. https를 사용하는 HTTP/2의 값은 h2이구요.

물론 HTTP/2를 바로 사용하는 케이스의 경우 TLS-ALPN을 통해 바로 프로토콜 협상 후 TLS 커넥션을 사용하게 됩니다.

H2C Smuggling

많은 웹 서비스들은 Reverse Proxy를 사용하고 있습니다. 이러한 과정에서 101 Switching 을 사용해야하는 상황이 오면, 프록시 서버는 별도의 처리없이 중재자 역할을 수행하게 됩니다. 여기서 bishopfox는 HTTP/2 프로토콜에 대한 연구를 진행했고, 결국 재미있는 결함을 발견하게 됩니다.

RFC-7540의 3.2.1 부분에 명시된 내용과 TLS 내 HTTP/2 설정에선 h2c Upgrade는 cleartext connection에서만 허용되고 이 때 HTTP2-Settings 헤더는 전달하지 말아야한다고 명시되어 있습니다.

HTTP2-Settings    = token68

A server MUST NOT upgrade the connection to HTTP/2 if this header
field is not present or if more than one is present.  A server MUST
NOT send this header field.

https://tools.ietf.org/html/rfc7540#section-3.2.1

http2-spec에도 이렇게 명시되어 있습니다.

3.3 Starting HTTP/2 for "https" URIs
A client that makes a request to an "https" URI uses TLS [TLS12] with the application-layer protocol negotiation (ALPN) extension [TLS-ALPN].

HTTP/2 over TLS uses the "h2" protocol identifier. The "h2c" protocol identifier MUST NOT be sent by a client or selected by a server; the "h2c" protocol identifier describes a protocol that does not use TLS.

Once TLS negotiation is complete, both the client and the server MUST send a connection preface (Section 3.5).

TLS를 통한 HTTP/2를 사용할 땐 h2c가 아닌 h2 프로토콜 식별자를 사용하라고 되어있습니다. 맨 위에서 이야기드렸듯이 h2c는 http, h2는 https를 사용하기 위해 고안된 indicator이기 때문이죠. 만약 proxy가 껴있는 구조에서 cleartext가 아닌 TLS 상에서 proxy가 h2c 를 백엔드에 전달하여 upgrade가 발생하면 어떻게 될까요?

Proxy가 있는 hops 환경에서 백엔드 서버는 클라이언트가 Cleartext인지 TLS인지 알 수 있는 방법이 h2c,h2 등의 indicator 뿐이라 웹이 아닌 TLS Connection을 HTTP로 판단하여 TLS Connection 위에 TCP Tunnel 을 만듭니다. 이 때 클라이언트는 HTTP가 아니기 때문에 기존 커넥션을 그대로 사용합니다. (Over TLS)

즉 이미 연결되어 있는 커넥션이고 HTTP 통신이 아니기 떄문에 Proxy의 ACL 정책에 영향을 받지 않지만, TCP Tunnel에서 발생한 요청이 HTTP로 동작할 수 있기 때문에 차단된 리소스에 접근할 수 있는 포인트가 생깁니다.

** 사실 완벽하게 이해가 된 부분은 아니라, 혹시나 잘못 이해했다면 댓글로 알려주세요! **

전반적인 동작 방식을 보면 WebSocket Connection Smuggling과 많이 유사합니다. WebSocket Connection Smuggling은 제가 작년에 썼던 글을 참조하시면 될 것 같습니다.

아무튼 플로우는 이렇습니다.

  1. 클라이언트가 서버(프록시)로 HTTP/1.1 Upgrade 요청 전송 (잘못된 헤더를 전송)
  2. 프록시가 해당 요청을 백엔드에 전달하고 101 Swiching protocol로 response를 내려줌
  3. 백엔드 서버에서 101을 받으면 TCP Tunnel을 생성함
  4. 클라이언트가 프록시로부터 101을 받으면 기존 커넥션을 재사용하고, HTTP/2 초기화를 진행
  5. 클라이언트가 HTTP/2 multiplexing을 사용하여 priavte한 경로에 대해 추가 요청을 전송함
  6. 프록시는 TCP 통신을 감시하지 않기 때문에(정책상 HTTP) 제한된 페이지에 추가 요청을 전송하고, 서버는 응답을 TLS Tunnel로 전달함

How to Easy testing

정말 다행스럽게도, 점검 도구와 환경을 다 만들어두셨습니다.

https://github.com/BishopFox/h2csmuggler

set-up

git clone https://github.com/BishopFox/h2csmuggler
cd h2csmuggler
pip3 install h2

scanning

python3 h2csmuggler.py --scan-list urls.txt --threads 5

get internal endpoint

python3 h2csmuggler.py -x https://edgeserver -X POST -d '{"user":128457 "role": "admin"}' -H "Content-Type: application/json" -H "X-SYSTEM-USER: true" http://backend/api/internal/user/permissions

bruteforce endpoint

python3 h2csmuggler.py -x https://edgeserver -i dirs.txt http://localhost/

이 과정에선 HTTP/2의 multiplexing을 사용한다고 하네요. multiplexing 이란 HTTP/2의 주요 기능으로 동시에 여러 리소스를 요청하는걸 의미합니다. Connection: keep-alive , pipeline의 개선버전이죠

get aws metadata api

python3 h2csmuggler.py -x https://edgeserver -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" http://169.254.169.254/latest/api/token`

more.. https://github.com/BishopFox/h2csmuggler

How to protect

HTTP Request Smuggling / WebSocket Connection Smuggling과 같이 여러가지 대응방안이 있겠지만, 원리적인 측면에서 보면 RFC 문서에서 이야기된대로 tls connection에서 h2c upgrade를 사용할 수 없도록 제한하는게 가장 정확한 것 같습니다. 물론 스펙적으로 가능한다면(이 문제조차 없었겠죠) 좋겠지만, 안되는건 안되는거기 때문에 프록시 서버에서 헤더를 무조건 전달하지 않고 서비스에서 사용할 헤더만 처리하는 방식으로 위험을 완화시킬 수 있습니다. 다만 HTTP Request Smuggling에서 몇몇 벤더사들이 선택한 방식인 Request 가공/처리하지 않고 그대로 백엔드로 넘겨주는 방식 때문에 사실상 조치가 굉장히 어려워질 수도 있습니다. (둘다 존재하면 누굴 선택해야하는가?)

그냥 제 개인적인 생각으론, 사용하지 않는 헤더패턴을 막는 것도 중요하지만, private 한 경로를 직접 접근할 수 없도록 host, x-forwarded-for 등의 다른 호스트를 바라볼 수 있는 헤더를 제한하고, 중요한 API나 경로를 호출할 수 없도록 제한하는 방식도 필요할 것 같네요.

모든 Smuggling 그렇듯, 패치에 의존하는 것보다 hop간의 간격 차이를 이해하고 거기서 조치방안을 찾는게 더 유리해보입니다 :D

Conclusion

원리는 무척 단순하지만, 환경은 단순하지 않습니다. HTTP Request Smuggling과 WebSocket Connection Smuggling과 다르게 서버의 설정에 따라 발생하는 부분이고, 생각보단 흔하지 않은 케이스일 것 같다는 생각이 드네요. 다만 이 기법을 만든 bishopfox가 이야기했듯이 Burp extension과 Nuclei template으로 만들어진다면 식별 자체는 굉장히 쉬울테니, 아마 광범위한 영역에 테스팅이 이루어질 것 같습니다. (Nuclei가 정말 대단하죠..ㅋㅋ)

References