FeroxFuzz

repository·main·Indexed 42 days ago

https://github.com/epi052/feroxfuzz

A structure-aware, black box HTTP fuzzing library written in Rust. Designed as a modular, composable framework for building web fuzzers, it allows developers to combine Mutators, Deciders, Observers, Processors, and Schedulers. It features asynchronous execution for efficient HTTP request/response cycles and uses ShouldFuzz directives for targeted mutation. Version 1.0.0-rc.13.

Tokens
11.9K
Snippets
42
Records
46
Agent score
52%

What's inside FeroxFuzz

  1. How FeroxFuzz works: Core concepts and design

    main

    FeroxFuzz is a structure-aware, black-box HTTP fuzzing library designed for composability. Its design is heavily inspired by LibAFL, implementing modular components that can be composed to build custom fuzzers.

    Key architectural concepts include:

    • Composable Components: Users build fuzzers by combining Mutators, Deciders, Observers, Processors, and Schedulers.
    • Async Support: Unlike LibAFL, FeroxFuzz is optimized for asynchronous execution to handle HTTP request/response cycles efficiently.
    • Structure Awareness: The library uses ShouldFuzz directives to identify which specific parts of an HTTP request (e.g., URLParameterValues) should be mutated, leaving other parts static.
    • Fuzz-loop Execution: The library manages a lifecycle involving a SharedState (containing the Corpus), a Scheduler to fetch entries, a Client to send requests, and various hooks/processors to handle the results.
  2. Add FeroxFuzz to your Rust project

    main

    To use FeroxFuzz as a library in your Rust application, add it to your Cargo.toml dependencies. Note that the project is currently in a pre-release state (v1.0.0-rc), so ensure you use the appropriate version string.

    [dependencies]
    feroxfuzz = { version = "1.0.0-rc.13" }
  3. What is FeroxFuzz?

    main
    FeroxFuzz is a structure-aware HTTP fuzzing library designed for building web tooling and one-off web fuzzers in Rust. Its design is inspired by LibAFL, making it a composable fuzzing library, but it is specifically focused on black-box HTTP fuzzing and supports async code.
  4. Use `ResponseProcessor` to handle HTTP responses

    main

    A ResponseProcessor allows you to execute custom logic (side-effects like logging or printing) after a request has been sent and a response has been received. It provides access to three key components within its processing closure:

    1. &ResponseObserver<R>: An observer containing metadata about the response (e.g., status code, content length, URL, elapsed time).
    2. Option<&Action>: The Action returned by the fuzzer's deciders (e.g., Action::Keep), which tells you how the fuzzer decided to treat this specific response.
    3. &SharedState: The current state of the fuzzer.

    While you can instantiate it manually for testing, the standard way to use it is to pass it to the build_processors! macro and include the resulting processors in your Fuzzer implementation.

    // Example: Creating a processor that prints details of 'Keep' actions
    let mut response_printer = ResponseProcessor::new(
        |response_observer: &ResponseObserver<BlockingResponse>, action, _state| {
            if let Some(action) = action {
                if matches!(action, Action::Keep) {
                    println!(
                        "[{}] {} - {} - {:?}",
                        response_observer.status_code(),
                        response_observer.content_length(),
                        response_observer.url().to_string(),
                        response_observer.elapsed()
                    );
                }
            }
        },
    );
  5. Use UniqueProductScheduler for Cartesian product fuzzing

    main

    The UniqueProductScheduler generates a Cartesian product of multiple Corpus entries. It ensures that every combination of entries from the provided corpora is scheduled exactly once. This is particularly useful when you have multiple corpora (e.g., a list of usernames and a list of passwords) and want to ensure every possible pair is tested.

    Key Characteristics

    • Robustness: It is the most robust scheduler for ensuring each entry is scheduled exactly once, even if corpora are modified at runtime.
    • Efficiency: It is the least efficient scheduler because it maintains a HashSet of all scheduled combinations to prevent repeats.
    • Duplicate Handling: It prevents scheduling the same combination of entries more than once, but it does not prevent duplicate entries from being added to the underlying corpora. To prevent duplicates in the corpora themselves, use the .unique() method during corpus construction.

    Handling Runtime Corpus Changes

    If the length of a corpus changes between calls to fuzzer.fuzz_once(), you must manually update the scheduler so it recognizes the new entries:

    1. Call scheduler.update_length() to synchronize the scheduler with the new corpus lengths.
    2. Call fuzzer.fuzz_once() to allow the scheduler to begin processing the new combinations.
    // Example: Pairing users with IDs
    let users = Wordlist::new().words(["user", "admin"]).name("users").build();
    let ids = RangeCorpus::with_stop(5).name("ids").build()?;
    
    let state = SharedState::with_corpora([ids, users]);
    let order = ["users", "ids"];
    let mut scheduler = UniqueProductScheduler::new(order, state.clone())?;
    
    while Scheduler::next(&mut scheduler).is_ok() {
        // Each user is paired with each ID exactly once
    }
  6. Identify fuzzable parts of a request with ShouldFuzz

    main

    The ShouldFuzz enum defines directives that specify which components of an HTTP Request should be targeted for fuzzing. These directives can target specific byte slices (like a request body or a specific header) or entire categories of request components (like all URL query keys or all header values).

    Key Categories of Directives

    • Request Body & Method: Target the RequestBody, HTTPMethod, or HTTPVersion.
    • URL Components: Target specific parts of the URL such as URLScheme, URLUsername, URLPassword, URLHost, URLPort, URLPath, or URLFragment. Note that these variants do not take an initial value; the value is derived from the Request instance (typically created via Request::from_url).
    • User-Agent: Target the UserAgent value specifically.
    • URL Query Parameters:
      • Specific pairs: URLParameterKey, URLParameterValue, or URLParameterKeyAndValue (parsed on the first =).
      • Bulk targeting: URLParameterKeys, URLParameterValues, or URLParameterKeysAndValues (targets all keys, all values, or both in the query string).
    • HTTP Headers:
      • Specific lines: HeaderKey, HeaderValue, or HeaderKeyAndValue (parsed on the first :).
      • Generic targeting: Key, Value, or KeyAndValue for any header or query parameter.

    Usage Example

    When using Request::from_url, you can pass a slice of ShouldFuzz directives to indicate what parts of the URL should be fuzzed.

    // Example of bulk targeting URL parameter keys using Request::from_url
    // Request::from_url("http://example.com/stuff.php?FUZZ=derp", &[ShouldFuzz::URLParameterKeys])
  7. The `Data` enum for fuzzable and static inputs

    main

    The Data enum is the base-level input type in FeroxFuzz. It distinguishes between data that can be modified by a mutator and data that must remain constant.

    • Data::Fuzzable(Vec<u8>): Data that can be changed by a mutator during the fuzzing process.
    • Data::Static(Vec<u8>): Non-fuzzable data; its internal contents will not change during mutation.

    You can convert between these states using toggle_type() or create them from various types like &str, String, or Vec<u8> (which defaults to Static).

    use feroxfuzz::input::Data;
    use std::str::FromStr;
    
    // Create static data from a string
    let mut data = Data::from_str("AAA")?;
    assert_eq!(data, Data::Static(vec![0x41, 0x41, 0x41]));
    
    // Switch from Static to Fuzzable
    data.toggle_type();
    assert_eq!(data, Data::Fuzzable(vec![0x41, 0x41, 0x41]));
  8. Use ResponseObserver to analyze HTTP responses

    main

    The ResponseObserver is a specialized observer used to inspect and categorize HTTP responses during a fuzzing session. It wraps a type implementing the Response trait and provides high-level helper methods to identify redirects, directories, and file extensions.

    While you can create one manually using with_response, the standard pattern is to pass ResponseObserver instances to the build_observers! macro and provide the resulting collection to a Fuzzer implementation.

    // Standard usage pattern:
    // 1. Create observers (often via build_observers! macro)
    // 2. Pass to Fuzzer
    let observers = build_observers!(ResponseObserver::new());
    // ... pass observers to your Fuzzer implementation
  9. Create a custom `HttpMethodsCorpus` with specific methods

    main

    If predefined groups do not meet your needs, you can construct a custom HttpMethodsCorpus using the following builder methods:

    • new(): Starts an empty builder.
    • method(T): Adds a single method to the builder. T must be convertible into Data.
    • methods(I): Adds an iterator of methods to the builder.
    • with_methods(I): A constructor that initializes the builder with a collection of methods immediately.

    Example of building from a specific list:

    let corpus = HttpMethodsCorpus::with_methods(["GET", "POST"])
        .name("custom_methods")
        .build();
    use feroxfuzz::corpora::HttpMethodsCorpus;
    use feroxfuzz::prelude::*;
    
    // Create a corpus with specific methods
    let corpus = HttpMethodsCorpus::new()
        .method("GET")
        .method("POST")
        .name("custom_corpus")
        .build();
  10. Create a Wordlist using WordlistBuilder

    main

    The Wordlist is a generic container for fuzzing items. You can construct it using a type-safe builder pattern. To successfully call .build(), you must provide both a name (via .name()) and at least one item (via .word() or .words()).

    Common builder methods:

    • new(): Starts a new builder.
    • word(T): Adds a single item.
    • words(I): Adds an iterator of items.
    • name(&str): Sets the corpus name.
    • unique(): Ensures all items are unique. Note that this doubles the memory footprint because a HashSet is used alongside the Vec, and the original order of items will be lost.
    • build(): Finalizes the construction into a CorpusType.
    // Normal wordlist (allows duplicates)
    let wordlist = Wordlist::new()
        .word("1")
        .word("2")
        .name("words")
        .build();
    
    // Unique wordlist (removes duplicates, loses order, higher memory usage)
    let wordlist = Wordlist::new()
        .words(["one", "two", "three", "one", "two", "three"])
        .name("words")
        .unique()
        .build();
  11. Create an `HttpMethodsCorpus` using predefined groups

    main

    You can create an HttpMethodsCorpus using several predefined logical groupings of HTTP methods based on RFC 7231. These groupings are available via the HttpMethodsCorpus builder methods:

    • all(): Includes all methods (GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH).
    • safe(): Includes methods that do not alter server state (GET, HEAD, OPTIONS, TRACE).
    • idempotent(): Includes methods where identical requests have the same effect (GET, HEAD, OPTIONS, TRACE, PUT, DELETE).
    • cacheable(): Includes methods whose responses can be stored for reuse (GET, HEAD).

    Note: You must call .name("name") on the builder before calling .build() to successfully create the corpus.

    use feroxfuzz::corpora::HttpMethodsCorpus;
    use feroxfuzz::prelude::*;
    
    // Create a corpus of all HTTP methods
    let corpus = HttpMethodsCorpus::all().name("my_corpus").build();
  12. Implement a basic asynchronous fuzzer with FeroxFuzz

    main

    The following example demonstrates the minimal boilerplate required to create an asynchronous fuzzer. It involves setting up a Wordlist corpus, an AsyncClient (using reqwest), a ReplaceKeyword mutator, a StatusCodeDecider to filter results, and an AsyncFuzzer to orchestrate the process.

    Note: You must replace the target URL in Request::from_url with a valid endpoint for your testing.

    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        // create a new corpus from the given list of words
        let words = Wordlist::from_file("./examples/words")?
            .name("words")
            .build();
    
        // pass the corpus to the state object, which will be shared between all of the fuzzers and processors
        let mut state = SharedState::with_corpus(words);
    
        // bring-your-own client, this example uses the reqwest library
        let req_client = reqwest::Client::builder().build()?;
    
        // feroxfuzz provides both a blocking and an asynchronous client implementation
        // using reqwest. 
        let client = AsyncClient::with_client(req_client);
    
        // ReplaceKeyword mutators operate similar to how ffuf/wfuzz work
        let mutator = ReplaceKeyword::new(&"FUZZ", "words");
    
        // fuzz directives control which parts of the request should be fuzzed
        let request = Request::from_url(
            "http://localhost:8000/?admin=FUZZ",
            Some(&[ShouldFuzz::URLParameterValues]),
        )?;
    
        // StatusCodeDecider inspects response status codes to decide on an Action
        let decider = StatusCodeDecider::new(200, |status, observed, _state| {
            if status == observed {
                Action::Keep
            } else {
                Action::Discard
            }
        });
    
        // ResponseObserver gathers information from each response
        let response_observer: ResponseObserver<AsyncResponse> = ResponseObserver::new();
    
        // ResponseProcessor provides access to the observer and the decider's Action
        let response_printer = ResponseProcessor::new(
            |response_observer: &ResponseObserver<AsyncResponse>, action, _state| {
                if let Some(Action::Keep) = action {
                    println!(
                        "[{}] {} - {} - {:?}",
                        response_observer.status_code(),
                        response_observer.content_length(),
                        response_observer.url(),
                        response_observer.elapsed()
                    );
                }
            },
        );
    
        // Schedulers manage how the fuzzer gets entries from the corpus
        let scheduler = OrderedScheduler::new(state.clone())?;
    
        // Macros to build component collections
        let deciders = build_deciders!(decider);
        let mutators = build_mutators!(mutator);
        let observers = build_observers!(response_observer);
        let processors = build_processors!(response_printer);
    
        let threads = 40;
    
        // The Fuzzer is the main component that wraps all other components
        let mut fuzzer = AsyncFuzzer::new(threads)
            .client(client)
            .request(request)
            .scheduler(scheduler)
            .mutators(mutators)
            .observers(observers)
            .processors(processors)
            .deciders(deciders)
            .post_loop_hook(|state| {
                println!("\n•*´¨`*•.¸¸.•* Finished fuzzing loop •*´¨`*•.¸¸.•*\n");
                println!("{state:#}");
            })
            .build();
    
        fuzzer.fuzz_once(&mut state).await?;
    
        println!("{state:#}");
    
        Ok()
    }