Our first target is a simple HTTP server implementation from https://github.com/fxsjy/httpstub.
First, we will navigate to the directory and make sure we are on the right commit. On the class VM:
mkdir -p ~/workshop/exercise1cd ~/workshop/exercise1git clone https://github.com/fxsjy/httpstub.gitcd httpstub/git rev-parse HEAD52ee2783b64a5fa826e0e5e217c00f2c3268d2f5git checkout 52ee278Next, we will build the target from source:
nano Makefile
Edit the second line and insert a -g flag right after the =. E.g.:CFLAGS=-g -O2 -W -Werror -Wall
This will include more debug information in coredumps when the program crashes.make:make
Finally, test the compiled executable (you should see help text):
> ./httpstub
usage: httpstub -p <port> -f <data file> -d <delay (ms)> [-q quiet]
index.html in your current directory (~/workshop/exercise1/httpstub). nano index.html<h1>Go away</h1>
./httpstub -p 8086 -f index.html127.0.0.1:8086/index.htmlnc -C -v 192.168.142.128 8086
-C parameter sends a pair of carriage return and newline characters instead of only a newline when pressing enter. This is necessary on Linux, as HTTP requires both carriage return and newline characters at the end of a line. For some versions of netcat, the argument is -c (lowercase c).-v parameter enables verbose output, which will give us feedback on the connection status.GET /index.html HTTP/1.1
(notice the two line breaks (seen here as an empty line) – hit enter twice)> nc -C -v 192.168.142.128 8086
localhost [127.0.0.1] 8086 open
GET /index.html HTTP/1.1
HTTP/1.1 200 OK
Content-Length: 21
Connection: Keep-Alive
Content-Type: text/plain
<h1>Go away</h1>
In the previous example, we sent a manually typed HTTP request and observed the raw response using netcat. But what if you don't know how a network protocol works?
Wireshark is a network protocol analyzer that can be used to observe traffic as it flies over the wire. We will use Wireshark to observe the HTTP protocol in action.
sudo wireshark
Note: Any "Lua" errors can be safely ignored; Wireshark will run just fine.tcp.port == 8086netcat.Your browser is likely to send a more complex request than is strictly necessary. Feel free to explore these parameters, but we will use a simpler request for our initial fuzzer.A skeleton file has been provided to decrease the amount of boilerplate you'll need to type for this exercise. The skeleton file is a typical setup when fuzzing a TCP protocol. We will copy the file and edit the hard-coded IP address. In your "fuzzing" terminal:
cd ~/workshop/exercise1http-fuzz.pyimport time
from boofuzz import *
def main():
target_host = "127.0.0.1"
target_port = 8086
session = Session(
target=Target(
connection=SocketConnection(target_host, target_port, proto="tcp"),
),
)
###########################################################################
# Protocol definition goes here
###########################################################################
session.feature_check()
# session.fuzz()
print(
"Test complete. Serving web page at http://127.0.0.1:26000 Hit Ctrl+C to quit."
)
while True: # Stay alive so that we can still view the Fuzz Control web page
time.sleep(0.001)
def http_check(target, fuzz_data_logger, session, sock, *args, **kwargs):
###########################################################################
# Failure detection code goes here
#
###########################################################################
pass
if __name__ == "__main__":
main()
nano http-fuzz.pymain() function, ensure the target_host and target_port variables match what your target server is running on.We will now define a simple HTTP request in boofuzz. Type the following lines just under the "Protocol definition goes here" comment.
boofuzz inherits a system of protocol definition that uses static function calls. The syntax may be awkard if you're used to a more object oriented style. Just stick with it for now.
Request, representing a single protocol message:req = Request("HTTP-Request", children=[
])
The Request type is called a "Block" in boofuzz – most blocks accept a children parameter, which defines the data in the Block. The first parameter is the name. You are free to choose any descriptive name for the message, but it should be unique.GET /index.html HTTP/1.1\r\n\r\n" Now we need to break this format up into a set of primitives that boofuzz will understand. To do this, we will use String and Delim.Our first primitive will look like:String(name="Method", default_value="GET"),
In context:req = Request("HTTP-Request", children=[
String(name="Method", default_value="/index.html"),
])
String is a primitve, representing an individual chunk of data in the protocol. The first argument, again, is the name. The default_value argument is the "normal", non-fuzzed, value of the protocol. `boofuzz`` will do the work of generating invalid values at runtime.Delim type for delimiters like this.Delim(name="method-space", default_value=" "),
The usage is very similar to String: we provide a default value and a name. The difference is in the strategy boofuzz uses when fuzzing strings vs. fuzzing delimiters.String(name="URI", default_value="/index.html"),
Delim(name="space-2", default_value=" "),
String(name="HTTP-Version", default_value="HTTP/1.1"),
Static(name="CRLF", default_value="\r\n"),
Note: The Static type is a special type that does not mutate.Static(name="CRLF-end", default_value="\r\n"),
Session object:session.connect(req)
Altogether, your code should look like:
req = Request("HTTP-Request", children=(
Group(name="Method", values=["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE"]),
Delim(name="space-1", default_value=" "),
String(name="URI", default_value="/index.html"),
Delim(name="space-2", default_value=" "),
String(name="HTTP-Version", default_value="HTTP/1.1"),
Static(name="CRLF", default_value="\r\n"),
Static(name="CRLF-end", default_value="\r\n"),
))
session.connect(req)
session.feature_check()
# session.fuzz()
We've finished our initial protocol definition. Before fuzzing, it is prudent to test what we have.
session.feature_check() line is uncommented and the session.fuzz() line is commented. You should see:session.feature_check()
# session.fuzz()
python http-fuzz.pypoetry shell -C ~/workshop/boofuzz/ to activate the build system's virtual environment.Transmitting 28 bytes: 47 45 54 20 2f 69 6e 64 65 78 2e 68 74 6d 6c 20 48 54 54 50 2f 31 2e 31 0d 0a 0d 0a 'GET /index.html HTTP/1.1\r\n\r\n'
The log entry tells you how many bytes boofuzz attempted to transmit. It then prints out the bytes, first as a hex string and then as a Python string. Received: 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 31 37 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 4b 65 65 70 2d 41 6c 69 76 65 0d 0a 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74 65 78 74 2f 70 6c 61 69 6e 0d 0a 0d 0a 3c 68 31 3e 47 6f 20 61 77 61 79 3c 2f 68 31 3e 0a 'HTTP/1.1 200 OK\r\nContent-Length: 17\r\nConnection: Keep-Alive\r\nContent-Type: text/plain\r\n\r\n<h1>Go away</h1>\n'
Again, we see the received data both in hex and string literal format.If you see an empty Received line: Received:
It may be that the message you're sending is not being interpreted as a valid HTTP request. In this case, inspect the sent data and compare it with a valid HTTP request (e.g. see the HTTP Basics section).This is the moment we've been waiting for. The culmination of years of open source engineering and several minutes of honest hard work. With your short program, you have leveraged the power of boofuzz, indeed, the cumulative power of ages and generations.
It is time, to fuzz.
session.fuzz() line and comment the session.feature_check() line. You should now see:# session.feature_check()
session.fuzz()
python http-fuzz.pyBy default, boofuzz will continually restart without logging any failures. We can use a failure detection method to detect the error on the correct test case, and even to detect subtle errors that might otherwise be missed. Furthermore, detecting failures will allow us to trigger a restart method more promptly.
The boilerplate has been prepared in a method called http_check.
Session, add this line at the end of your protocol definition, within your main function:session.register_post_test_case_callback(http_check)
http_check, start by opening a connection to the target (boofuzz automatically closes connections after fuzzing).target.open()
req = session.find_node("name", "HTTP-Request")
original_request_value = req.encode(None, None)
target.send(original_request_value)
response = target.recv()
fuzz_data_logger.log_check('Verifying response contains "200 OK"')
if '200 OK' in response:
fuzz_data_logger.log_pass()
else:
fuzz_data_logger.log_fail('Response did not contain 200 OK')
We use log_check to describe our test for the log output, and log_pass or log_fail to log the result. In boofuzz, test cases are considerd to pass by default until log_fail() is called. The messages passed to each method are optional. A quality checker would parse the entire response and verify its validity, but our quick-and-dirty aproach is to simply check for the string "200 OK".Note: The request/response approach to detecting failures is heavyweight and will in this case decrease your test cases per second. The request/response approach is useful in certain scenarios, but keep this limitation in mind. Also be aware that this callback method may be used to check failures using any approach that you can program.
You'll notice that, after causing one crash, the entire fuzz test stops. While the thrill of identifying a segmentation fault is a reward all its own, finding only one crash does not deliver the satisfying sensation of pervasive power which fuzzing facilitates. It's as if Alexander the Great conquered but one kingdom before falling off the edge of the world.
To address this void in our hearts, we need to be able to restart the target and continue winning.
One very effective technique in network protocol fuzzing, if available to you, is to monitor the target from its host. boofuzz includes a process_monitor_unix.py script for this purpose. The Windows version is process_monitor.py.
The boofuzz process monitor can:
We will need to start the procmon, and update our fuzzer to use it properly.
cd ~/workshop/boofuzzpython process_monitor_unix.pyNote: The procmon listens on port 26002 by default. The -P parameter can be used to specify a different port, but we will stick with the default.Session:procmon = ProcessMonitor(target_host, DEFAULT_PROCMON_PORT)
procmon.set_options(
start_commands=[
[
"/home/youruser/workshop/exercise1/httpstub/httpstub",
"-p",
"8086",
"-f",
"/home/youruser/workshop/exercise1/httpstub/index.html",
]
]
)
DEFAULT_PROCMON_PORT is a boofuzz constant set to 26002.session = Session(
target=Target(
connection=SocketConnection(target_host, target_port, proto="tcp"),
monitors=[procmon],
),
)
These two steps should leave your code looking like:
target_host = "127.0.0.1"
target_port = 8086
procmon = ProcessMonitor(target_host, DEFAULT_PROCMON_PORT)
procmon.set_options(
start_commands=[
[
"/home/julia_r_pereyda/workshop/exercise1/httpstub/httpstub",
"-p",
"8086",
"-f",
"/home/julia_r_pereyda/workshop/exercise1/httpstub/index.html",
]
]
)
session = Session(
target=Target(
connection=SocketConnection(target_host, target_port, proto="tcp"),
monitors=[procmon],
),
)
Session class constructor's crash_threshold_node and crash_threshold_element arguments.Congratulations! You are free to use this fuzzer against other HTTP servers and improve the protocol definition.