Our first target is a simple HTTP server implementation from https://github.com/fxsjy/httpstub.

Download

First, we will navigate to the directory and make sure we are on the right commit. On the class VM:

  1. Open the Terminal application
  2. mkdir -p ~/workshop/exercise1
  3. cd ~/workshop/exercise1
  4. git clone https://github.com/fxsjy/httpstub.git
  5. cd httpstub/
  6. git rev-parse HEAD
    1. The outut should be 52ee2783b64a5fa826e0e5e217c00f2c3268d2f5
    2. If it is not, run git checkout 52ee278

Build

Next, we will build the target from source:

  1. First, we will edit the Makefile to insert a debug flag:
    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.
  2. Run make:
    make
    

Test

Finally, test the compiled executable (you should see help text):

  1. > ./httpstub
    usage:  httpstub -p <port> -f <data file> -d <delay (ms)> [-q quiet] 
    

Start Target

  1. Create a file index.html in your current directory (~/workshop/exercise1/httpstub).
    1. nano index.html
    2. Type some basic HTML, e.g.
      <h1>Go away</h1>
      
  2. Start server: ./httpstub -p 8086 -f index.html
  3. Test Connection (browser):
    1. From a browser on the class VM, go to 127.0.0.1:8086/index.html

Protocol Observation (netcat):

  1. Open another Terminal window or tab. To open a new tab, press Ctrl+Shift+T. You can use Ctrl+PgUp and Ctrl+PgDown to toggle between tabs.
    1. Note: Typically, you will want your fuzz target to be running in a separate VM. To minimize resource usage for this class, we are running the fuzzer and target in one VM. You can think of these two terminal tabs as a rough equivalent of what you would see if you were running two separate VMs – one terminal is your target and the other is your fuzzer.
  2. Run netcat on the "fuzzing" terminal, e.g.:
    nc -C -v 192.168.142.128 8086
    
    1. The -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).
    2. The -v parameter enables verbose output, which will give us feedback on the connection status.
  3. Type:
    GET /index.html HTTP/1.1 
    
    
    (notice the two line breaks (seen here as an empty line) – hit enter twice)
  4. You should see something like:
    > 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> 
    
    1. This is an example of what is literally sent on the wire – this is the HTTP protocol, or at least an example of it. We will use this example to make our first fuzzing program.

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.

  1. In a Terminal, run:
    sudo wireshark
    
    Note: Any "Lua" errors can be safely ignored; Wireshark will run just fine.
  2. Double click the "Loopback" (or "localhost" or "lo") line to capture on the local interface only.
    1. Note: If you haven't used Wireshark before, double click "any", and be amazed!
  3. Add a displapy filter.
    1. Click on the text box toward the top of Wireshark.
    2. Type tcp.port == 8086
    3. Hit Enter.
  4. With Wireshark still running, go to your browser again and navigate to the server's address or hit Refresh.
  5. You should see at least two entries, one HTTP request and one response. Open these packets and explore the contents. You should be able to find strings that look similar to what you sent and received with netcat.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:

  1. cd ~/workshop/exercise1
  2. Copy this skeleton code into http-fuzz.py
    import 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()
    
  3. Edit: nano http-fuzz.py
  4. In the main() 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.

  1. First, we create a new 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.
  2. Recall the text we sent over netcat: "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.
  3. The next string in our sample message is the filename, but first there is a space in between. boofuzz has a 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.
  4. Next, add additional lines to define the filename and HTTP version:
    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.
  5. Recall that we need a second double carriage return and line feed to end an HTTP request:
    Static(name="CRLF-end", default_value="\r\n"),
    
  6. We will now connect the fully defined message to our 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.

  1. Ensure the session.feature_check() line is uncommented and the session.fuzz() line is commented. You should see:
    session.feature_check()
    # session.fuzz()
    
  2. Run your script from the Terminal: python http-fuzz.py
    1. If you haven't already, you may need to run poetry shell -C ~/workshop/boofuzz/ to activate the build system's virtual environment.
    2. Note: You should see "PASS" at the end of the output, but keep in mind that we have not added any failure checks, and "Pass" is the default result – so we won't know for sure without manually inspecting the output...
  3. Observe the script's output and verify that your message was properly sent. You should see a log line similar to the following:
    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.
  4. Verify that a reply was received. You should see a line similar to:
     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).
  5. Before stopping the Python script, use a browser to visit the Fuzz Control web page at http://localhost:26000.The web interface lets you review specific test case log entries (only 1 for now since this is just a feature check) and will also summarize any detected failures.
  6. In the Terminal, hit Ctrl+C to quit your fuzz script.

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.

  1. Uncomment the session.fuzz() line and comment the session.feature_check() line. You should now see:
    # session.feature_check()
    session.fuzz()
    
  2. Run: python http-fuzz.py
  3. Monitor your fuzzer from the Fuzz Control web page (make sure to refresh if you had the web page open from your previous run!): http://localhost:26000

First crash

  1. You should identify an error condition in your first 3 or so test cases. If you look at the terminal output, you will see that boofuzz is continually trying to reconnect to the target.
  2. Go check the output of the HTTP server in the terminal that is running the target. What error does it give?
  3. If you got something about a core dump, or free, or segmentation fault, etc., congratulations! You have identified a vulnerability. If you're interested in taking a closer look, finish out this exercise and then see the "Failure Analysis" appendix.

By 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.

  1. First, to register your method as a callback for the Session, add this line at the end of your protocol definition, within your main function:
    session.register_post_test_case_callback(http_check)
    
  2. In http_check, start by opening a connection to the target (boofuzz automatically closes connections after fuzzing).
    target.open()
    
  3. We will now send a valid request. Use the following lines:
    req = session.find_node("name", "HTTP-Request")
    original_request_value = req.encode(None, None)
    target.send(original_request_value)
    
    1. Note: This method of getting the request's original value is a bit hacky, and may change in future releases.
  4. We will now attempt to receive a response from the target and check its validity.
    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".
  5. Before fuzzing, let's run the feature check to test.
    1. Go back and switch from fuzz to feature check.
    2. Start the HTTP server again.
    3. Run your script again.
    4. You should be able to identify the callback method's output in your script's log output.
  6. With everything working, switch back from feature check to fuzz and run your script again!
    1. What do you observe?
    2. You should see an indication of a failure, though the specific error message may vary.
    3. You should also see output indiciating that boofuzz is not able to restart the target. This is what we will address in the next section.

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.

Just one crash? :'(

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.

The Process Monitor

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:

  1. Restart the target after a crash, allowing the fuzzer to continue running.
  2. Provide another way to detect crashes by monitoring the fuzzer directly.
  3. Collect core dumps, which can be used for failure analysis.

We will need to start the procmon, and update our fuzzer to use it properly.

Start the Process Monitor

  1. Stop your HTTP server if it is still running.
  2. cd ~/workshop/boofuzz
  3. python 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.

Update Your Fuzzer

  1. Add the following lines right before you create your 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.
  2. Then, pass the procmon object into your Target, like so:
    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],
    ),
)

Fuzz again

  1. Run your fuzz script again.
  2. Monitor progress:
    1. In the Fuzz Control web page (make sure to refresh!), you should see the same crash from the previous steps, but the test will continue running and you should see more crashes. If you look at the failing cases closely, you may see the failure reason as reported by the process monitor, e.g. "Segmentation fault".
    2. As the test progresses, you may notice that some test cases are being skipped. In order to decrease noise, boofuzz will skip after a certain number of failures occur within a single element/primitive (3 by default) or message (12 by default). Thresholds can be controlled through the Session class constructor's crash_threshold_node and crash_threshold_element arguments.
    3. After your test is completed, the fuzz control web server should continue to run until you kill the fuzzing process.
  3. Bonus step: Try removing your callback method and see if you can still detect the crash using just the process monitor.

Congratulations! You are free to use this fuzzer against other HTTP servers and improve the protocol definition.