9.8 KiB
퍼징(Fuzzing) 고급
어제 우리는 퍼징이 무엇인지, 그리고 퍼즈 테스트(퍼지 입력을 사용하는 단위 테스트)를 작성하는 방법을 배웠습니다. 하지만 퍼즈 테스트는 단순한 단위 테스트 그 이상입니다. 이 방법론을 사용하여 서버로 전송되는 요청을 퍼징하여 웹 애플리케이션을 테스트할 수 있습니다.
오늘은 웹 서버 퍼지 테스트에 대한 실용적인 접근 방식을 살펴보겠습니다.
이를 위해 다양한 도구가 도움이 될 수 있습니다.
Burp Intruder, SmartBear 등이 있습니다. 하지만 유료 라이선스가 필요한 독점 도구(proprietary tools)도 있습니다.
그렇기 때문에 오늘 데모에서는 Burp Intruder
에서 영감을 받아 유사한 기능을 제공하는 Go로 작성된 간단한 오픈 소스 CLI를 사용하려고 합니다.
이름은 httpfuzz입니다.
Getting started
이 도구는 아주 간단합니다.
요청에 대한 템플릿(퍼지 데이터에 대한 placeholder를 정의), 단어 목록(퍼지 데이터)을 제공하면 httpfuzz
가 요청을 렌더링하여 서버로 전송합니다.
먼저 요청에 대한 템플릿을 정의해야 합니다.
다음 내용으로 request.txt
라는 파일을 생성합니다.
POST / HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.3
Accept: */*
Cache-Control: no-cache
Host: localhost:8000
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: 35
{
"name": "`S9`",
}
이것은 JSON 본문이 있는 /
경로에 대한 유효한 HTTP POST
요청입니다.
본문의 "`" 기호는 우리가 제공하는 데이터로 대체될 플레이스홀더를 정의합니다.
httpfuzz
는 헤더, 경로, URL 매개변수를 퍼즈 처리할 수도 있습니다.
다음으로 요청에 넣을 입력의 단어 목록을 제공해야 합니다.
다음 내용으로 data.txt
라는 파일을 생성합니다:
SOME_NAME
Mozilla/5.0 (Linux; Android 7.0; SM-G930VC Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36
이 파일에서는 본문 내부에 대체될 두 개의 입력을 정의했습니다. 실제 시나리오에서는 적절한 퍼즈 테스트를 위해 훨씬 더 많은 데이터를 여기에 넣어야 합니다.
이제 템플릿과 입력이 준비되었으니 도구를 실행해 보겠습니다. 안타깝게도 이 도구는 바이너리로 배포되지 않으므로 소스에서 빌드해야 합니다. 리포지토리를 복제하고 실행합니다.
go build -o httpfuzz cmd/httpfuzz.go
(컴퓨터에 최신 버전의 Go가 설치되어 있어야 합니다.)
이제 바이너리를 얻었으니 실행해 보겠습니다.
./httpfuzz \
--wordlist data.txt \
--seed-request request.txt \
--target-header User-Agent \
--target-param fuzz \
--delay-ms 50 \
--skip-cert-verify \
--proxy-url http://localhost:8080 \
httpfuzz
는 우리가 호출하는 바이너리입니다.--wordlist data.txt
는 우리가 제공한 입력이 포함된 파일입니다.--seed-request requests.txt
는 요청 템플릿입니다.--target-header User-Agent
는httpfuzz
가User-Agent
헤더 대신에 제공된 입력을 사용하도록 지시합니다.--target-param fuzz
는 제공된 입력을fuzz
URL 매개변수의 값으로 사용하도록httpfuzz
에 지시합니다.--delay-ms 50
은 요청 사이에 50ms를 기다리도록httpfuzz
에 지시합니다.--skip-cert-verify
는httpfuzz
에게 TLS 확인을 수행하지 않도록 지시합니다.--proxy-url http://localhost:8080
는httpfuzz
에게 HTTP 서버의 위치를 알려줍니다.
2개의 입력과 3개의 위치(본문, User-Agent
헤더, fuzz
매개변수)에 배치할 수 있습니다.
즉, httpfuzz
는 6개의 요청을 생성하여 서버로 전송합니다.
실행해서 어떤 일이 일어나는지 봅시다. 서버로 들어오는 요청을 확인할 수 있도록 모든 요청을 기록하는 간단한 웹 서버를 작성했습니다.
$ ./httpfuzz \
--wordlist data.txt \
--seed-request request.txt \
--target-header User-Agent \
--target-param fuzz \
--delay-ms 50 \
--skip-cert-verify \
--proxy-url http://localhost:8080 \
httpfuzz: httpfuzz.go:164: Sending 6 requests
그리고 서버 로그입니다.
-----
Got request to http://localhost:8000/
User-Agent header = [SOME_NAME]
Name = S9
-----
Got request to http://localhost:8000/?fuzz=SOME_NAME
User-Agent header = [PostmanRuntime/7.26.3]
Name = S9
-----
Got request to http://localhost:8000/
User-Agent header = [PostmanRuntime/7.26.3]
Name = SOME_NAME
-----
Got request to http://localhost:8000/
User-Agent header = [Mozilla/5.0 (Linux; Android 7.0; SM-G930VC Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36]
Name = S9
-----
Got request to http://localhost:8000/?fuzz=Mozilla%2F5.0+%28Linux%3B+Android+7.0%3B+SM-G930VC+Build%2FNRD90M%3B+wv%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Version%2F4.083+Mobile+Safari%2F537.36
User-Agent header = [PostmanRuntime/7.26.3]
Name = S9
-----
Got request to http://localhost:8000/
User-Agent header = [PostmanRuntime/7.26.3]
Name = Mozilla/5.0 (Linux; Android 7.0; SM-G930VC Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.83 Mobile Safari/537.36
6개의 HTTP 요청이 수신된 것을 확인할 수 있습니다.
이 중 2개는 User-Agent
헤더에 대한 값 파일의 값을 가지고 있고, 4개는 템플릿의 기본 헤더를 가지고 있습니다.
그 중 2개는 fuzz
쿼리 매개변수에 대한 값 파일의 값을 가지고 있고, 4개는 템플릿의 기본 헤더를 가지고 있습니다.
그 중 2개는 Name
본문 속성에 대한 값 파일에서 값을 가져오고, 4개는 템플릿의 기본 헤더를 사용합니다.
이 도구를 약간 개선하면 이러한 요청의 순열을 다르게 만들 수 있습니다(예: 값 파일의 값으로 ?fuzz=
와 User-Agent
가 모두 있는 요청).
httpfuzz
가 요청의 결과에 대한 정보를 제공하지 않는다는 점에 주목하세요.
이를 파악하려면 서버에 대한 일종의 모니터링을 설정하거나 우리에게 의미 있는 방식으로 결과를 처리하는 httpfuzz
플러그인을 작성해야 합니다.
그렇게 해봅시다.
사용자 정의 플러그인을 작성하려면 Listener
인터페이스를 구현해야합니다.
// Listener must be implemented by a plugin to users to hook the request - response transaction.
// The Listen method will be run in its own goroutine, so plugins cannot block the rest of the program, however panics can take down the entire process.
type Listener interface {
Listen(results <-chan *Result)
}
package main
import (
"bytes"
"io/ioutil"
"log"
"github.com/joncooperworks/httpfuzz"
)
type logResponseCodePlugin struct {
logger *log.Logger
}
func (b *logResponseCodePlugin) Listen(results <-chan *httpfuzz.Result) {
for result := range results {
b.logger.Printf("Got %d response from the server\n", result.Response.StatusCode)
}
}
// New returns a logResponseCodePlugin plugin that simple logs the response code of the response.
func New(logger *log.Logger) (httpfuzz.Listener, error) {
return &logResponseCodePlugin{logger: logger}, nil
}
이제 플러그인을 빌드해야 합니다.
go build -buildmode=plugin -o log exampleplugins/log/log.go
그리고 --post-request
플래그를 통해 httpfuzz
에 연결할 수 있습니다:
$ ./httpfuzz \
--wordlist data.txt \
--seed-request request.txt \
--target-header User-Agent \
--target-param fuzz \
--delay-ms 50 \
--skip-cert-verify \
--proxy-url http://localhost:8080 \
--post-request log
httpfuzz: httpfuzz.go:164: Sending 6 requests
httpfuzz: log.go:15: Got 200 response from the server
httpfuzz: log.go:15: Got 200 response from the server
httpfuzz: log.go:15: Got 200 response from the server
httpfuzz: log.go:15: Got 200 response from the server
httpfuzz: log.go:15: Got 200 response from the server
httpfuzz: log.go:15: Got 200 response from the server
짜잔! 이제 최소한 서버의 응답 코드가 무엇인지 확인할 수 있습니다.
물론 훨씬 더 많은 데이터를 출력하는 훨씬 더 정교한 플러그인을 작성할 수도 있지만, 이 연습의 목적상 이 정도면 충분합니다.
요약
퍼징은 단위 테스트를 훨씬 뛰어넘는 매우 강력한 테스트 기법입니다.
퍼징은 유효한 HTTP 요청의 일부를 서버의 취약점이나 결함을 노출할 수 있는 데이터로 대체하여 HTTP 서버를 테스트하는 데 매우 유용할 수 있습니다.
웹 애플리케이션 퍼지 테스트에 도움이 되는 무료 및 유료 도구가 많이 있습니다.
리소스
Fuzzing the Stack for Fun and Profit at DefCamp 2019
HTTP Fuzzing Scan with SmartBear
Fuzzing Session: Finding Bugs and Vulnerabilities Automatically
18일차에 뵙겠습니다.