8.9 KiB
Fuzzing Advanced
Yesterday we learned what fuzzing is and how to write fuzz tests (unit tests with fuzzy inputs). However, fuzz testing goes beyond just unit testing. We can use this methodology to test our web application by fuzzing the requests sent to our server.
Today, we will take a practical approach to fuzzy testing a web server.
Different tools can help us do this.
Such tools are Burp Intruder and SmartBear. However, there are proprietary tools that require a paid license to use them.
That is why for our demonstration today we are going to use a simple open-source CLI written in Go that was inspired by Burp Intruder and provides similar functionality. It is called httpfuzz.
Getting started
This tool is quite simple.
We provide it a template for our requests (in which we have defined placeholders for the fuzzy data), a wordlist (the fuzzy data) and httpfuzz
will render the requests and send them to our server.
First, we need to define a template for our requests.
Create a file named request.txt
with the following content:
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`",
}
This is a valid HTTP POST
request to the /
route with JSON body.
The "`" symbol in the body defines a placeholder that will be substituted with the data we provide.
httpfuzz
can also fuzz the headers, path, and URL params.
Next, we need to provide a wordlist of inputs that will be placed in the request.
Create a file named data.txt
with the following content:
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
In this file, we defined two inputs that will be substituted inside the body. In a real-world scenario, you should put much more data here for proper fuzz testing.
Now that we have our template and our inputs, let's run the tool. Unfortunately, this tool is not distributed as a binary, so we will have to build it from source. Clone the repo and run:
go build -o httpfuzz cmd/httpfuzz.go
(requires to have a recent version of Go installed on your machine).
Now that we have the binary let's run it:
./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
is the binary we are invoking.--wordlist data.txt
is the file with inputs we provided.--seed-request requests.txt
is the request template.--target-header User-Agent
tellshttpfuzz
to use the provided inputs in the place of theUser-Agent
header.--target-param fuzz
tellshttpfuzz
to use the provided inputs as values for thefuzz
URL parameter.--delay-ms 50
tellshttpfuzz
to wait 50 ms between the requests.--skip-cert-verify
tellshttpfuzz
to not do any TLS verification.--proxy-url http://localhost:8080
tellshttpfuzz
where our HTTP server is.
We have 2 inputs and 3 places to place them (in the body, the User-Agent
header, and the fuzz
parameter).
This means that httpfuzz
will generate 6 requests and send them to our server.
Let's run it and see what happens. I wrote a simple web server that logs all requests so that we can see what is coming into our server:
$ ./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
and the server logs:
-----
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
We see that we have received 6 HTTP requests.
Two of them have a value from our values file for the User-Agent
header, and 4 have the default header from the template.
Two of them have a value from our values file for the fuzz
query parameter, and 4 have the default header from the template.
Two of them have a value from our values file for the Name
body property, and 4 have the default header from the template.
A slight improvement of the tool could be to make different permutations of these requests (for example, a request that has both ?fuzz=
and User-Agent
as values from the values file).
Notice how httpfuzz
does not give us any information about the outcome of the requests.
To figure that out, we need to either set up some sort of monitoring for our server or write a httpfuzz
plugin that will process the results in a meaningful for us way.
Let's do that.
To write a custom plugin, we need to implement the Listener
interface:
// 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
}
Now we need to build our plugin first:
go build -buildmode=plugin -o log exampleplugins/log/log.go
and then we can plug it into httpfuzz
via the --post-request
flag:
$ ./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
Voila! Now we can at least see what the response code from the server was.
Of course, we can write much more sophisticated plugins that output much more data, but for the purpose of this exercise, that is enough.
Summary
Fuzzing is a really powerful testing technique that goes way beyond unit testing.
Fuzzing can be extremely useful for testing HTTP servers by substituting parts of valid HTTP requests with data that could potentially expose vulnerabilities or deficiencies in our server.
There are many tools that can help us in fuzzy testing our web applications, both free and paid ones.
Resources
Fuzzing the Stack for Fun and Profit at DefCamp 2019
HTTP Fuzzing Scan with SmartBear
Fuzzing Session: Finding Bugs and Vulnerabilities Automatically
See you on Day 18.