Go에서 HTTP gzip response 처리하기

최근 dalfox에 독특한 이슈가 제보됬는데(오프라인으로도 한번 제보받은 사항이라 이미 삽질을 좀 헀던 상태였네요) 오늘 이를 해결하고 어떻게 해결했는지 간략하게 공유할까 합니다. 특별한 내용은 아니지만, golang에서 http 기반 개발을 진행할 때 알고 있으면 실수를 예방할 수 있는 부분이죠.

먼저 간략히 이슈에 대해 설명하자면 dalfox의 file 모드와 --rawdata flag를 이용해 스캔 시 탐지되지 않는데, --proxy flag를 추가해서 burpsuite나 zap을 거쳐 스캔하면 결과나 나오는 특이한 이슈입니다.

Detective and Problem

오프라인으로 제보받았을 때 발생하는 http request를 비교했지만 차이점이 없었습니다. 그래서 바로 해결 가능한 문제는 아닐 것 같아 천천히 해결하려고 냅뒀던 상태였죠.

제보받은 raw http request

GET /xss.php HTTP/1.1
Host: brutelogic.com.br
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://brutelogic.com.br/knoxss.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

최근 동일한 이슈를 제보받고 다시 확인하기 위해 각 케이스 별로 wireshark로 패킷 자체를 비교해봤더니 req와 res에 미묘한 차이가 있었습니다. 바로 gzip encoding 이였죠.

HTTP/1.1 200 OK
Server: Sucuri/Cloudproxy
Date: Wed, 01 Dec 2021 14:48:58 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 774
Connection: close
X-Sucuri-ID: 16005
Access-Control-Allow-Origin: *
Vary: Accept-Encoding
Content-Encoding: gzip
X-Sucuri-Cache: HIT

req만 자세히 봤어도 알 수 있었을텐데.. 왜 놓친거지 😱

문제는 명확합니다. wireshark의 http packet을 tcp stream으로 보면 아래와 같이 response body 부분이 인코딩된 상태로 나타납니다. 기존에 burpsuite와 zap등 도구를 거쳤을 때는 이 도구들이 gzip encoding을 처리해줘서 정상적으로 스캔됬고, 거치지 않았을 때는 gzip decoding 처리를 하지 않았기 때문에 비정상적인 response body로 결과를 분석해서 결과가 없었던 겁니다.

HTTP/1.1 200 OK
Server: Sucuri/Cloudproxy
Date: Wed, 01 Dec 2021 14:48:58 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 774
Connection: close
X-Sucuri-ID: 16005
Access-Control-Allow-Origin: *
Vary: Accept-Encoding
Content-Encoding: gzip
X-Sucuri-Cache: HIT

...........U.R.0.}..b.K`.X8\......:..&.i..{..d.........../......gm.9GgW.p...w......\.^..H..5.....d.A....A)......-.0.h.i....BBqBqBqR.. .....x..KgQ.a4"t..P.\....Km..D.....k..,Jq)..5..tH'...D(.......Na\...up!...5.5.C8.....}...,.....uk.6C$...<b.....61.t`M......q...._h.P(Ji.D..=....._..5..`..v.....$....1x!....t].....Q.{.;....jw-...P..M.zO,~}..\..&.W[...K.p2.|&..OP)=.EEd[^.b..AR.*..J........,....m ..|%..#h).z~....I].5.k..hnD......d@..t.../.).8...w+.......~.....Zi.v...EY9p......
.6.,`.....lsS....A.	........@..&..G.D......j.....;.(am.
5.+.l.9'.....\.T,.mQ\..?....-.htU>..Y......y'.V.\R....^c.n.N^.K..b....6.8.,.U:.._..b).M..`....W.C.wM.R.....1.R..B...e.......{....e....o..i...>....>.<.}/.a.}Xs.c....+.Y..3....&.;3#,........Y.....\..........L..F)0r.9K.....S?.e...8o.?.....D....

Handling gzip response

해결방법은 간단합니다. response 처리 로직에서 Response Header를 읽어 Content-Encodinggzip인 경우 gzip 패키지를 이용해서 response.Body를 읽어서 사용하면 gzip decoding 처리된 결과를 사용할 수 있습니다.

var reader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
  reader, err = gzip.NewReader(resp.Body)
  defer reader.Close()
default:
  reader = resp.Body
}
bytes, _ := ioutil.ReadAll(reader)
str := string(bytes)

iocompress/gzip 패키지 import가 필요하며, 자세한건 아래 커밋을 참고해주세요 😁 https://github.com/hahwul/dalfox/commit/fdb9d7405922aebc33d8a811d184a3733ce24e80

Conclusion

golang의 http는 개발자에게 많은 처리 부분을 맡기기 때문에 이런 사소한 포인트를 잘 기억하고 처리해야하는 것 같습니다. (비슷한 예로 timeout)

덕분에 오늘도 재미있는 것 하나 배웠네요 :D