Skip to content

Commit

Permalink
Merge pull request #122 from evanrichter/length-filter
Browse files Browse the repository at this point in the history
Add wordcount and line count filtering to address #89
  • Loading branch information
epi052 authored Nov 17, 2020
2 parents 57be47d + 0d365c0 commit f9fe4d9
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap
args: --all-targets --all-features -- -D warnings -A clippy::unnecessary_unwrap -A clippy::deref_addrof
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "1.5.3"
version = "1.6.0"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ A pre-made configuration file with examples of all available settings can be fou
# extract_links = true
# depth = 1
# filter_size = [5174]
# filter_word_count = [993]
# filter_line_count = [35, 36]
# queries = [["name","value"], ["rick", "astley"]]

# headers can be specified on multiple lines or as an inline table
Expand Down Expand Up @@ -381,8 +383,10 @@ FLAGS:
OPTIONS:
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
-N, --filter-lines <LINES>... Filter out messages of a particular line count (ex: -N 20 -N 31,30)
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -C 401)
-W, --filter-words <WORDS>... Filter out messages of a particular word count (ex: -W 312 -W 91,82)
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
-o, --output <FILE> Output file to write results to (default: stdout)
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
Expand Down Expand Up @@ -555,7 +559,7 @@ a few of the use-cases in which feroxbuster may be a better fit:
| configuration file for default value override || ||
| can accept urls via STDIN as part of a pipeline || ||
| can accept wordlists via STDIN | |||
| filter by response size || ||
| filter based on response size, wordcount, and linecount || ||
| auto-filter wildcard responses || ||
| performs other scans (vhost, dns, etc) | |||
| time delay / rate limiting | |||
Expand Down
2 changes: 2 additions & 0 deletions ferox-config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
# extract_links = true
# depth = 1
# filter_size = [5174]
# filter_word_count = [993]
# filter_line_count = [35, 36]
# queries = [["name","value"], ["rick", "astley"]]

# headers can be specified on multiple lines or as an inline table
Expand Down
18 changes: 18 additions & 0 deletions src/banner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,24 @@ by Ben "epi" Risher {} ver: {}"#,
}
}

for filter in &config.filter_word_count {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a2}", "Word Count Filter", filter)
)
.unwrap_or_default(); // 💢
}

for filter in &config.filter_line_count {
writeln!(
&mut writer,
"{}",
format_banner_entry!("\u{1f4a2}", "Line Count Filter", filter)
)
.unwrap_or_default(); // 💢
}

if config.extract_links {
writeln!(
&mut writer,
Expand Down
168 changes: 95 additions & 73 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ pub struct Configuration {
#[serde(default)]
pub filter_size: Vec<u64>,

/// Filter out messages of a particular line count
#[serde(default)]
pub filter_line_count: Vec<usize>,

/// Filter out messages of a particular word count
#[serde(default)]
pub filter_word_count: Vec<usize>,

/// Don't auto-filter wildcard responses
#[serde(default)]
pub dont_filter: bool,
Expand Down Expand Up @@ -240,6 +248,8 @@ impl Default for Configuration {
queries: Vec::new(),
extensions: Vec::new(),
filter_size: Vec::new(),
filter_line_count: Vec::new(),
filter_word_count: Vec::new(),
filter_status: Vec::new(),
headers: HashMap::new(),
depth: depth(),
Expand Down Expand Up @@ -270,6 +280,8 @@ impl Configuration {
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
/// - **extensions**: `None`
/// - **filter_size**: `None`
/// - **filter_word_count**: `None`
/// - **filter_line_count**: `None`
/// - **headers**: `None`
/// - **queries**: `None`
/// - **no_recursion**: `false` (recursively scan enumerated sub-directories)
Expand Down Expand Up @@ -352,36 +364,30 @@ impl Configuration {

let args = parser::initialize().get_matches();

// the .is_some appears clunky, but it allows default values to be incrementally
// overwritten from Struct defaults, to file config, to command line args, soooo ¯\_(ツ)_/¯
if args.value_of("threads").is_some() {
let threads = value_t!(args.value_of("threads"), usize).unwrap_or_else(|e| e.exit());
config.threads = threads;
}

if args.value_of("depth").is_some() {
let depth = value_t!(args.value_of("depth"), usize).unwrap_or_else(|e| e.exit());
config.depth = depth;
}

if args.value_of("scan_limit").is_some() {
let scan_limit =
value_t!(args.value_of("scan_limit"), usize).unwrap_or_else(|e| e.exit());
config.scan_limit = scan_limit;
}

if args.value_of("wordlist").is_some() {
config.wordlist = String::from(args.value_of("wordlist").unwrap());
macro_rules! update_config_if_present {
($c:expr, $m:ident, $v:expr, $t:ty) => {
match value_t!($m, $v, $t) {
Ok(value) => *$c = value, // Update value
Err(clap::Error {
kind: clap::ErrorKind::ArgumentNotFound,
message: _,
info: _,
}) => {
// Do nothing if argument not found
}
Err(e) => e.exit(), // Exit with error on parse error
}
};
}

if args.value_of("output").is_some() {
config.output = String::from(args.value_of("output").unwrap());
}
update_config_if_present!(&mut config.threads, args, "threads", usize);
update_config_if_present!(&mut config.depth, args, "depth", usize);
update_config_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output", String);

if args.values_of("status_codes").is_some() {
config.status_codes = args
.values_of("status_codes")
.unwrap() // already known good
if let Some(arg) = args.values_of("status_codes") {
config.status_codes = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
Expand All @@ -390,11 +396,9 @@ impl Configuration {
.collect();
}

if args.values_of("replay_codes").is_some() {
if let Some(arg) = args.values_of("replay_codes") {
// replay codes passed in by the user
config.replay_codes = args
.values_of("replay_codes")
.unwrap() // already known good
config.replay_codes = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
Expand All @@ -406,10 +410,8 @@ impl Configuration {
config.replay_codes = config.status_codes.clone();
}

if args.values_of("filter_status").is_some() {
config.filter_status = args
.values_of("filter_status")
.unwrap() // already known good
if let Some(arg) = args.values_of("filter_status") {
config.filter_status = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
Expand All @@ -418,35 +420,47 @@ impl Configuration {
.collect();
}

if args.values_of("extensions").is_some() {
config.extensions = args
.values_of("extensions")
.unwrap()
.map(|val| val.to_string())
.collect();
if let Some(arg) = args.values_of("extensions") {
config.extensions = arg.map(|val| val.to_string()).collect();
}

if args.values_of("filter_size").is_some() {
config.filter_size = args
.values_of("filter_size")
.unwrap() // already known good
if let Some(arg) = args.values_of("filter_size") {
config.filter_size = arg
.map(|size| {
size.parse::<u64>()
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
})
.collect();
}

if let Some(arg) = args.values_of("filter_words") {
config.filter_word_count = arg
.map(|size| {
size.parse::<usize>()
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
})
.collect();
}

if let Some(arg) = args.values_of("filter_lines") {
config.filter_line_count = arg
.map(|size| {
size.parse::<usize>()
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
})
.collect();
}

if args.is_present("quiet") {
// the reason this is protected by an if statement:
// consider a user specifying quiet = true in ferox-config.toml
// if the line below is outside of the if, we'd overwrite true with
// false if no -q is used on the command line
config.quiet = args.is_present("quiet");
config.quiet = true;
}

if args.is_present("dont_filter") {
config.dont_filter = args.is_present("dont_filter");
config.dont_filter = true;
}

if args.occurrences_of("verbosity") > 0 {
Expand All @@ -456,53 +470,41 @@ impl Configuration {
}

if args.is_present("no_recursion") {
config.no_recursion = args.is_present("no_recursion");
config.no_recursion = true;
}

if args.is_present("add_slash") {
config.add_slash = args.is_present("add_slash");
config.add_slash = true;
}

if args.is_present("extract_links") {
config.extract_links = args.is_present("extract_links");
config.extract_links = true;
}

if args.is_present("stdin") {
config.stdin = args.is_present("stdin");
config.stdin = true;
} else {
config.target_url = String::from(args.value_of("url").unwrap());
}

////
// organizational breakpoint; all options below alter the Client configuration
////
if args.value_of("proxy").is_some() {
config.proxy = String::from(args.value_of("proxy").unwrap());
}

if args.value_of("replay_proxy").is_some() {
config.replay_proxy = String::from(args.value_of("replay_proxy").unwrap());
}

if args.value_of("user_agent").is_some() {
config.user_agent = String::from(args.value_of("user_agent").unwrap());
}

if args.value_of("timeout").is_some() {
let timeout = value_t!(args.value_of("timeout"), u64).unwrap_or_else(|e| e.exit());
config.timeout = timeout;
}
update_config_if_present!(&mut config.proxy, args, "proxy", String);
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
update_config_if_present!(&mut config.timeout, args, "timeout", u64);

if args.is_present("redirects") {
config.redirects = args.is_present("redirects");
config.redirects = true;
}

if args.is_present("insecure") {
config.insecure = args.is_present("insecure");
config.insecure = true;
}

if args.values_of("headers").is_some() {
for val in args.values_of("headers").unwrap() {
if let Some(headers) = args.values_of("headers") {
for val in headers {
let mut split_val = val.split(':');

// explicitly take first split value as header's name
Expand All @@ -515,8 +517,8 @@ impl Configuration {
}
}

if args.values_of("queries").is_some() {
for val in args.values_of("queries").unwrap() {
if let Some(queries) = args.values_of("queries") {
for val in queries {
// same basic logic used as reading in the headers HashMap above
let mut split_val = val.split('=');

Expand Down Expand Up @@ -616,6 +618,8 @@ impl Configuration {
settings.stdin = settings_to_merge.stdin;
settings.depth = settings_to_merge.depth;
settings.filter_size = settings_to_merge.filter_size;
settings.filter_word_count = settings_to_merge.filter_word_count;
settings.filter_line_count = settings_to_merge.filter_line_count;
settings.filter_status = settings_to_merge.filter_status;
settings.dont_filter = settings_to_merge.dont_filter;
settings.scan_limit = settings_to_merge.scan_limit;
Expand Down Expand Up @@ -678,6 +682,8 @@ mod tests {
extract_links = true
depth = 1
filter_size = [4120]
filter_word_count = [994, 992]
filter_line_count = [34]
filter_status = [201]
"#;
let tmp_dir = TempDir::new().unwrap();
Expand Down Expand Up @@ -714,6 +720,8 @@ mod tests {
assert_eq!(config.queries, Vec::new());
assert_eq!(config.extensions, Vec::<String>::new());
assert_eq!(config.filter_size, Vec::<u64>::new());
assert_eq!(config.filter_word_count, Vec::<usize>::new());
assert_eq!(config.filter_line_count, Vec::<usize>::new());
assert_eq!(config.filter_status, Vec::<u16>::new());
assert_eq!(config.headers, HashMap::new());
}
Expand Down Expand Up @@ -865,6 +873,20 @@ mod tests {
assert_eq!(config.filter_size, vec![4120]);
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_filter_word_count() {
let config = setup_config_test();
assert_eq!(config.filter_word_count, vec![994, 992]);
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_filter_line_count() {
let config = setup_config_test();
assert_eq!(config.filter_line_count, vec![34]);
}

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_filter_status() {
Expand Down
Loading

0 comments on commit f9fe4d9

Please sign in to comment.