Skip to content

Commit

Permalink
Merge pull request #11 from epi052/improve-api-for-malformed-urls
Browse files Browse the repository at this point in the history
improved api for malformed urls
  • Loading branch information
epi052 authored Oct 13, 2022
2 parents cb26b7d + 60b0b33 commit e9c5a19
Show file tree
Hide file tree
Showing 10 changed files with 644 additions and 86 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "feroxfuzz"
version = "1.0.0-rc.1"
version = "1.0.0-rc.2"
edition = "2021"
authors = ["Ben 'epi' Risher (@epi052)"]
license = "Apache-2.0"
Expand Down
29 changes: 24 additions & 5 deletions examples/cartesian-product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// fuzz directives control which parts of the request should be fuzzed
// anything not marked fuzzable is considered to be static and won't be mutated
let request = Request::from_url(
"http://localhost:8000/",
Some(&[
ShouldFuzz::URLParameterValue(b"user=USER", b"="),
ShouldFuzz::URLParameterValue(b"id=ID", b"="),
]),
"http://localhost:8000/?user=USER&id=ID",
Some(&[ShouldFuzz::URLParameterValues]),
)?;

// a RequestProcessor provides a way to inspect each request and decide upon some Action based on the
Expand Down Expand Up @@ -128,5 +125,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// the fuzzer will run until it iterates over the entire corpus once
fuzzer.fuzz_once(&mut state)?;

println!("{state:#}");

// example output:
//
// http://localhost:8000/?user=USER&id=ID?user=user&id=0
// http://localhost:8000/?user=USER&id=ID?user=user&id=2
// http://localhost:8000/?user=USER&id=ID?user=user&id=4
// http://localhost:8000/?user=USER&id=ID?user=user&id=6
// http://localhost:8000/?user=USER&id=ID?user=user&id=8
// http://localhost:8000/?user=USER&id=ID?user=admin&id=0
// http://localhost:8000/?user=USER&id=ID?user=admin&id=2
// http://localhost:8000/?user=USER&id=ID?user=admin&id=4
// http://localhost:8000/?user=USER&id=ID?user=admin&id=6
// http://localhost:8000/?user=USER&id=ID?user=admin&id=8
// SharedState::{
// Seed=24301
// Rng=RomuDuoJrRand { x_state: 97704, y_state: 403063 }
// Corpus[ids]=RangeCorpus::{start=0, stop=10, step=2},
// Corpus[users]=Wordlist::{len=2, top-2=[Static("user"), Static("admin")]},
// Statistics={"timeouts":0,"requests":10.0,"errors":3,"informatives":0,"successes":3,"redirects":4,"client_errors":1,"server_errors":2,"redirection_errors":0,"connection_errors":0,"request_errors":0,"start_time":{"secs":1665661333,"nanos":517789344},"avg_reqs_per_sec":610.5293851456247,"statuses":{"308":2,"203":1,"201":1,"403":1,"304":1,"204":1,"301":1,"500":2}}
// }

Ok(())
}
153 changes: 153 additions & 0 deletions examples/from-url-list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//! first, update the variable named target with a valid url to scan
//!
//! then run the example with the following command
//! cargo run --example from-url-list
use feroxfuzz::client::AsyncClient;
use feroxfuzz::corpora::Wordlist;
use feroxfuzz::fuzzers::AsyncFuzzer;
use feroxfuzz::mutators::ReplaceKeyword;
use feroxfuzz::observers::ResponseObserver;
use feroxfuzz::prelude::*;
use feroxfuzz::processors::{RequestProcessor, ResponseProcessor};
use feroxfuzz::responses::AsyncResponse;
use feroxfuzz::schedulers::OrderedScheduler;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// pretend that we have some code that converts
//
// http://google.com:80 <-- schemes[0], hosts[0], ports[0]
// https://google.com:443 <-- schemes[1], hosts[1], ports[1]
// http://localhost:9999 <-- schemes[2], hosts[2], ports[2]
// https://localhost:9999 <-- schemes[3], hosts[3], ports[3]
//
// into the following three wordlists
let schemes = Wordlist::new()
.words(["http", "https", "http", "https"])
.name("schemes")
.build();

let hosts = Wordlist::new()
.words(["google.com", "google.com", "localhost", "localhost"])
.name("hosts")
.build();

let ports = Wordlist::new()
.words(["80", "443", "9999", "9999"])
.name("ports")
.build();

// pass the corpus to the state object, which will be shared between all of the fuzzers and processors
let mut state = SharedState::with_corpora([schemes, hosts, ports]);

// bring-your-own client, this example uses the reqwest library
let req_client = reqwest::Client::builder().build()?;

// with some client that can handle the actual http request/response stuff
// we can build a feroxfuzz client, specifically an asynchronous client in this
// instance.
//
// 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, in that they'll
// put the current corpus item wherever the keyword is found, as long as its found
// in data marked fuzzable (see ShouldFuzz directives below)
let scheme_mutator = ReplaceKeyword::new(&"FUZZ_SCHEME", "schemes");
let host_mutator = ReplaceKeyword::new(&"FUZZ_HOST", "hosts");
let port_mutator = ReplaceKeyword::new(&"FUZZ_PORT", "ports");

// fuzz directives control which parts of the request should be fuzzed
// anything not marked fuzzable is considered to be static and won't be mutated
//
// ShouldFuzz directives map to the various components of an HTTP request
let request = Request::from_url(
"FUZZ_SCHEME://FUZZ_HOST:FUZZ_PORT",
Some(&[
ShouldFuzz::URLHost,
ShouldFuzz::URLPort,
ShouldFuzz::URLScheme,
]),
)?;

// a `ResponseObserver` is responsible for gathering information from each response and providing
// that information to later fuzzing components, like Processors. It knows things like the response's
// status code, content length, the time it took to receive the response, and a bunch of other stuff.
let response_observer: ResponseObserver<AsyncResponse> = ResponseObserver::new();

// a `ResponseProcessor` provides access to the fuzzer's instance of `ResponseObserver`
// as well as the `Action` returned from calling `Deciders` (like the `StatusCodeDecider` above).
// Those two objects may be used to produce side-effects, such as printing, logging, calling out to
// some other service, or whatever else you can think of.
let response_printer = ResponseProcessor::new(
|response_observer: &ResponseObserver<AsyncResponse>, _action, _state| {
println!(
"[{}] {} - {} - {:?}",
response_observer.status_code(),
response_observer.content_length(),
response_observer.url(),
response_observer.elapsed()
);
},
);

// a `RequestProcessor` provides access to the fuzzer's mutated `Request` that is about to be
// sent to the target, as well as the `Action` returned from calling `Deciders` (like the
// `StatusCodeDecider` above). Those two objects may be used to produce side-effects, such as
// printing, logging, calling out to some other service, or whatever else you can think of.
let request_printer = RequestProcessor::new(|request, _action, _state| {
println!("Built request: {}", request.url_to_string().unwrap());
});

// `Scheduler`s manage how the fuzzer gets entries from the corpus. The `OrderedScheduler` provides
// in-order access of the associated `Corpus` (`Wordlist` in this example's case)
let scheduler = OrderedScheduler::new(state.clone())?;

// the macro calls below are essentially boilerplate. Whatever observers, deciders, mutators,
// and processors you want to use, you simply pass them to the appropriate macro call and
// eventually to the Fuzzer constructor.
let mutators = build_mutators!(scheme_mutator, host_mutator, port_mutator);
let observers = build_observers!(response_observer);
let processors = build_processors!(request_printer, response_printer);

let threads = 40; // number of threads to use for the fuzzing process

// the `Fuzzer` is the main component of the feroxfuzz library. It wraps most of the other components
// and takes care of the actual fuzzing process.
let mut fuzzer = AsyncFuzzer::new(
threads,
client,
request,
scheduler,
mutators,
observers,
processors,
(), // no deciders
);

// the fuzzer will run until it iterates over the entire corpus once
fuzzer.fuzz_once(&mut state).await?;

println!("{state:#}");

// example output:
//
// Built request: http://google.com:80
// Built request: https://google.com:443
// Built request: http://localhost:9999
// Built request: https://localhost:9999
// [200] 922 - http://localhost:9999/ - 23.310749ms
// [200] 54709 - http://www.google.com/ - 216.350722ms
// [200] 54738 - https://www.google.com/ - 223.503601ms
// SharedState::{
// Seed=24301
// Rng=RomuDuoJrRand { x_state: 97704, y_state: 403063 }
// Corpus[schemes]=Wordlist::{len=4, top-3=[Static("http"), Static("https"), Static("http")]},
// Corpus[hosts]=Wordlist::{len=4, top-3=[Static("google.com"), Static("google.com"), Static("localhost")]},
// Corpus[ports]=Wordlist::{len=4, top-3=[Static("80"), Static("443"), Static("9999")]},
// Statistics={"timeouts":0,"requests":4.0,"errors":1,"informatives":0,"successes":3,"redirects":0,"client_errors":0,"server_errors":0,"redirection_errors":0,"connection_errors":1,"request_errors":0,"start_time":{"secs":1665525829,"nanos":48677016},"avg_reqs_per_sec":15.911714551924089,"statuses":{"200":3}}
// }

Ok(())
}
8 changes: 4 additions & 4 deletions src/deciders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// discard if path matches '1'
let decider1 = RequestRegexDecider::new("1", |regex, request, _state| {
Expand Down Expand Up @@ -352,7 +352,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

let decider1 = RequestRegexDecider::new("1", |regex, request, _state| {
if regex.is_match(request.path().inner()) {
Expand Down Expand Up @@ -437,7 +437,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// discard if response's status code matches 401
let decider1 = StatusCodeDecider::new(401, |status, observed, _state| {
Expand Down Expand Up @@ -537,7 +537,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// keep if response's status code matches 200
let decider1 = StatusCodeDecider::new(200, |status, observed, _state| {
Expand Down
4 changes: 2 additions & 2 deletions src/fuzzers/async_fuzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// stop fuzzing if path matches '1'
let decider = RequestRegexDecider::new("1", |regex, request, _state| {
Expand Down Expand Up @@ -519,7 +519,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// stop fuzzing if path matches '1'
let decider = ResponseRegexDecider::new("derp", |regex, response, _state| {
Expand Down
4 changes: 2 additions & 2 deletions src/fuzzers/blocking_fuzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// stop fuzzing if path matches '1'
let decider = RequestRegexDecider::new("1", |regex, request, _state| {
Expand Down Expand Up @@ -341,7 +341,7 @@ mod tests {

let mutator = ReplaceKeyword::new(&"FUZZ", "range");

let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath(b"/FUZZ")]))?;
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;

// stop fuzzing if path matches '1'
let decider = ResponseRegexDecider::new("derp", |regex, response, _state| {
Expand Down
86 changes: 80 additions & 6 deletions src/requests/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,34 @@ pub enum ShouldFuzz<'a> {
HTTPVersion(&'a [u8]),

/// directive associated with a fuzzable URL scheme, ex: http/https
URLScheme(&'a [u8]),
///
/// # Note
///
/// this variant doesn't accept a starting value due to the fact that a [`Request`]
/// can only be instantiated through its `from_url` method. In order to provide
/// an initial value to the `Request`'s `host` field, simply use the first
/// parameter of the [`Request::from_url`] method
URLScheme,

/// directive associated with a fuzzable URL username
URLUsername(&'a [u8]),
///
/// # Note
///
/// this variant doesn't accept a starting value due to the fact that a [`Request`]
/// can only be instantiated through its `from_url` method. In order to provide
/// an initial value to the `Request`'s `host` field, simply use the first
/// parameter of the [`Request::from_url`] method
URLUsername,

/// directive associated with a fuzzable URL password
URLPassword(&'a [u8]),
///
/// # Note
///
/// this variant doesn't accept a starting value due to the fact that a [`Request`]
/// can only be instantiated through its `from_url` method. In order to provide
/// an initial value to the `Request`'s `host` field, simply use the first
/// parameter of the [`Request::from_url`] method
URLPassword,

/// directive associated with a fuzzable URL ip address/domain
///
Expand All @@ -46,13 +67,34 @@ pub enum ShouldFuzz<'a> {
URLHost,

/// directive associated with a fuzzable URL port
URLPort(&'a [u8]),
///
/// # Note
///
/// this variant doesn't accept a starting value due to the fact that a [`Request`]
/// can only be instantiated through its `from_url` method. In order to provide
/// an initial value to the `Request`'s `host` field, simply use the first
/// parameter of the [`Request::from_url`] method
URLPort,

/// directive associated with a fuzzable URL path
URLPath(&'a [u8]),
///
/// # Note
///
/// this variant doesn't accept a starting value due to the fact that a [`Request`]
/// can only be instantiated through its `from_url` method. In order to provide
/// an initial value to the `Request`'s `host` field, simply use the first
/// parameter of the [`Request::from_url`] method
URLPath,

/// directive associated with a fuzzable URL fragment
URLFragment(&'a [u8]),
///
/// # Note
///
/// this variant doesn't accept a starting value due to the fact that a [`Request`]
/// can only be instantiated through its `from_url` method. In order to provide
/// an initial value to the `Request`'s `host` field, simply use the first
/// parameter of the [`Request::from_url`] method
URLFragment,

/// directive associated with a fuzzable User-Agent header where only the
/// the value is fuzzable; the `User-Agent` key is static and does not
Expand All @@ -70,6 +112,38 @@ pub enum ShouldFuzz<'a> {
/// directive associated with a URL query where both the key and value are fuzzable
URLParameterKeyAndValue(&'a [u8], &'a [u8]),

/// directive associated with all URL queries where all query keys are marked
/// fuzzable; unmarked values remain static
///
/// # Note
///
/// this variant can be used when specifying parameters in the `url` field of
/// the [`Request::from_url`] method.
///
/// ex: `Request::from_url("http://example.com/stuff.php?FUZZ=derp", &[ShouldFuzz::URLParameterKeys])`
URLParameterKeys,

/// directive associated with all URL queries where all query values are marked
/// fuzzable; unmarked keys remain static
///
/// # Note
///
/// this variant can be used when specifying parameters in the `url` field of
/// the [`Request::from_url`] method.
///
/// ex: `Request::from_url("http://example.com/stuff.php?derp=FUZZ", &[ShouldFuzz::URLParameterValues])`
URLParameterValues,

/// directive associated with all URL queries where both all keys and values are fuzzable
///
/// # Note
///
/// this variant can be used when specifying parameters in the `url` field of
/// the [`Request::from_url`] method.
///
/// ex: `Request::from_url("http://example.com/stuff.php?FUZZ1=FUZZ2", &[ShouldFuzz::URLParameterKeysAndValues])`
URLParameterKeysAndValues,

/// directive associated with an http header where only the key is fuzzable; the
/// value is static
HeaderKey(&'a [u8], &'a [u8]),
Expand Down
Loading

0 comments on commit e9c5a19

Please sign in to comment.