Skip to content

Commit

Permalink
Merge pull request #222 from epi052/213-add-parallel-option
Browse files Browse the repository at this point in the history
add --parallel option
  • Loading branch information
epi052 authored Feb 18, 2021
2 parents 13222bf + 4ee1439 commit 030b588
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
- [ ] Your PR description references the associated issue (i.e. fixes #123456)
- [ ] Code is in its own branch
- [ ] Branch name is related to the PR contents
- [ ] PR targets master
- [ ] PR targets main

## Static analysis checks
- [ ] All rust files are formatted using `cargo fmt`
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.1.0"
version = "2.2.0"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"
Expand All @@ -22,7 +22,7 @@ lazy_static = "1.4"

[dependencies]
futures = { version = "0.3"}
tokio = { version = "1.0", features = ["full"] }
tokio = { version = "1.2.0", features = ["full"] }
tokio-util = {version = "0.6.3", features = ["codec"]}
log = "0.4"
env_logger = "0.8.3"
Expand All @@ -31,7 +31,7 @@ clap = "2.33"
lazy_static = "1.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
serde_json = "1.0.62"
uuid = { version = "0.8", features = ["v4"] }
indicatif = "0.15"
console = "0.14"
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ Enumeration.
- [Limit Number of Requests per Second (Rate Limiting) (new in `v2.0.0`)](#limit-number-of-requests-per-second-rate-limiting-new-in-v200)
- [Silence all Output or Be Kinda Quiet (new in `v2.0.0`)](#silence-all-output-or-be-kinda-quiet-new-in-v200)
- [Auto-tune or Auto-bail from Scans (new in `v2.1.0`)](#auto-tune-or-auto-bail-from-scans-new-in-v210)
- [Run Scans in Parallel (new in `v2.2.0`)](#run-scans-in-parallel-new-in-v220)
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
- [No file descriptors available](#no-file-descriptors-available)
Expand Down Expand Up @@ -370,6 +371,7 @@ A pre-made configuration file with examples of all available settings can be fou
# status_codes = [200, 500]
# filter_status = [301]
# threads = 1
# parallel = 2
# timeout = 5
# auto_tune = true
# auto_bail = true
Expand Down Expand Up @@ -463,6 +465,9 @@ OPTIONS:
-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 (use w/ --json for JSON entries)
--parallel <PARALLEL_SCANS>
Run parallel feroxbuster instances (one child process per url passed via stdin)
-p, --proxy <PROXY>
Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)
Expand Down Expand Up @@ -898,6 +903,29 @@ The AutoBail policy aborts individual directory scans when one of the criteria a

![auto-bail](img/auto-bail-demo.gif)

### Run Scans in Parallel (new in `v2.2.0`)

Version 2.2.0 introduces the `--parallel` option. If you're one of those people who use `feroxbuster` to scan 100s of hosts at a time, this is the option for you! `--parallel` spawns a child process per target passed in over stdin (recursive directories are still async within each child).

The number of parallel scans is limited to whatever you pass to `--parallel`. When one child finishes its scan, the next child will be spawned.

Unfortunately, using `--parallel` limits terminal output such that only discovered URLs are shown. No amount of `-v`'s will help you here. I imagine this isn't too big of a deal, as folks that need `--parallel` probably aren't sitting there watching the output... 🙃

Example Command:
```
cat large-target-list | ./feroxbuster --stdin --parallel 10 --extract-links --auto-bail
```

Resuling Process List (illustrative):
```
\_ target/debug/feroxbuster --stdin --parallel 10
\_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-one
\_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-two
\_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-three
\_ ...
\_ target/debug/feroxbuster --silent --extract-links --auto-bail -u https://target-ten
```

## 🧐 Comparison w/ Similar Tools

There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
Expand Down Expand Up @@ -947,6 +975,7 @@ few of the use-cases in which feroxbuster may be a better fit:
| hide progress bars or be silent (or some variation) (`v2.0.0`) ||||
| automatically tune scans based on errors/403s/429s (`v2.1.0`) || | |
| automatically stop scans based on errors/403s/429s (`v2.1.0`) || ||
| run scans in parallel (1 process per target) (`v2.2.0`) || | |
| **huge** number of other options | | ||

Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
Expand Down
1 change: 1 addition & 0 deletions ferox-config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# replay_proxy = "http://127.0.0.1:8081"
# replay_codes = [200, 302]
# verbosity = 1
# parallel = 8
# scan_limit = 6
# rate_limit = 250
# quiet = true
Expand Down
1 change: 1 addition & 0 deletions shell_completions/_feroxbuster
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ _feroxbuster() {
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]' \
'-L+[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
'--scan-limit=[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
'--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]' \
'(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default: 0, i.e. no limit)]' \
'--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]' \
'(--silent)*-v[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \
Expand Down
1 change: 1 addition & 0 deletions shell_completions/_feroxbuster.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--filter-similar-to', 'filter-similar-to', [CompletionResultType]::ParameterName, 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)')
[CompletionResult]::new('-L', 'L', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[CompletionResult]::new('--scan-limit', 'scan-limit', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
[CompletionResult]::new('--parallel', 'parallel', [CompletionResultType]::ParameterName, 'Run parallel feroxbuster instances (one child process per url passed via stdin)')
[CompletionResult]::new('--rate-limit', 'rate-limit', [CompletionResultType]::ParameterName, 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)')
[CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
Expand Down
6 changes: 5 additions & 1 deletion shell_completions/feroxbuster.bash
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ _feroxbuster() {

case "${cmd}" in
feroxbuster)
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --rate-limit --time-limit "
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
Expand Down Expand Up @@ -199,6 +199,10 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--parallel)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--rate-limit)
COMPREPLY=($(compgen -f "${cur}"))
return 0
Expand Down
1 change: 1 addition & 0 deletions shell_completions/feroxbuster.fish
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -s N -l filter-lines -d 'Filt
complete -c feroxbuster -n "__fish_use_subcommand" -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
complete -c feroxbuster -n "__fish_use_subcommand" -l filter-similar-to -d 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)'
complete -c feroxbuster -n "__fish_use_subcommand" -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)'
complete -c feroxbuster -n "__fish_use_subcommand" -l parallel -d 'Run parallel feroxbuster instances (one child process per url passed via stdin)'
complete -c feroxbuster -n "__fish_use_subcommand" -l rate-limit -d 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)'
complete -c feroxbuster -n "__fish_use_subcommand" -l time-limit -d 'Limit total run time of all scans (ex: --time-limit 10m)'
complete -c feroxbuster -n "__fish_use_subcommand" -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)'
Expand Down
9 changes: 9 additions & 0 deletions src/banner/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ pub struct Banner {
/// represents Configuration.rate_limit
rate_limit: BannerEntry,

/// represents Configuration.parallel
parallel: BannerEntry,

/// represents Configuration.auto_tune
auto_tune: BannerEntry,

Expand Down Expand Up @@ -281,6 +284,7 @@ impl Banner {
BannerEntry::new("🤪", "Filter Wildcards", &(!config.dont_filter).to_string());
let add_slash = BannerEntry::new("🪓", "Add Slash", &config.add_slash.to_string());
let time_limit = BannerEntry::new("🕖", "Time Limit", &config.time_limit);
let parallel = BannerEntry::new("🛤", "Parallel Scans", &config.parallel.to_string());
let rate_limit =
BannerEntry::new("🚧", "Requests per Second", &config.rate_limit.to_string());

Expand All @@ -304,6 +308,7 @@ impl Banner {
filter_line_count,
filter_regex,
extract_links,
parallel,
json,
queries,
output,
Expand Down Expand Up @@ -518,6 +523,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.scan_limit)?;
}

if config.parallel > 0 {
writeln!(&mut writer, "{}", self.parallel)?;
}

if config.rate_limit > 0 {
writeln!(&mut writer, "{}", self.rate_limit)?;
}
Expand Down
10 changes: 9 additions & 1 deletion src/config/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ pub struct Configuration {
#[serde(default)]
pub scan_limit: usize,

/// Number of parallel scans permitted; a limit of 0 means no limit is imposed
#[serde(default)]
pub parallel: usize,

/// Number of requests per second permitted (per directory); a limit of 0 means no limit is imposed
#[serde(default)]
pub rate_limit: usize,
Expand Down Expand Up @@ -280,6 +284,7 @@ impl Default for Configuration {
json: false,
verbosity: 0,
scan_limit: 0,
parallel: 0,
rate_limit: 0,
add_slash: false,
insecure: false,
Expand Down Expand Up @@ -350,7 +355,8 @@ impl Configuration {
/// - **dont_filter**: `false` (auto filter wildcard responses)
/// - **depth**: `4` (maximum recursion depth)
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
/// - **rate_limit**: `0` (no limit on concurrent scans imposed)
/// - **parallel**: `0` (no limit on parallel scans imposed)
/// - **rate_limit**: `0` (no limit on requests per second imposed)
/// - **time_limit**: `None` (no limit on length of scan imposed)
/// - **replay_proxy**: `None` (no limit on concurrent scans imposed)
/// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
Expand Down Expand Up @@ -486,6 +492,7 @@ impl Configuration {
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.parallel, args, "parallel", usize);
update_config_if_present!(&mut config.rate_limit, args, "rate_limit", usize);
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
update_config_if_present!(&mut config.output, args, "output", String);
Expand Down Expand Up @@ -793,6 +800,7 @@ impl Configuration {
);
update_if_not_default!(&mut conf.dont_filter, new.dont_filter, false);
update_if_not_default!(&mut conf.scan_limit, new.scan_limit, 0);
update_if_not_default!(&mut conf.parallel, new.parallel, 0);
update_if_not_default!(&mut conf.rate_limit, new.rate_limit, 0);
update_if_not_default!(&mut conf.replay_proxy, new.replay_proxy, "");
update_if_not_default!(&mut conf.debug_log, new.debug_log, "");
Expand Down
8 changes: 8 additions & 0 deletions src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fn setup_config_test() -> Configuration {
auto_bail = true
verbosity = 1
scan_limit = 6
parallel = 14
rate_limit = 250
time_limit = "10m"
output = "/some/otherpath"
Expand Down Expand Up @@ -146,6 +147,13 @@ fn config_reads_scan_limit() {
assert_eq!(config.scan_limit, 6);
}

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

#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_rate_limit() {
Expand Down
81 changes: 80 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
use std::{
collections::HashSet,
env::args,
fs::File,
io::{stderr, BufRead, BufReader},
ops::Index,
process::Command,
sync::{atomic::Ordering, Arc},
};

use anyhow::{bail, Context, Result};
use futures::StreamExt;
use tokio::{io, sync::oneshot};
use tokio::{
io,
sync::{oneshot, Semaphore},
};
use tokio_util::codec::{FramedRead, LinesCodec};

use feroxbuster::{
Expand All @@ -26,6 +32,13 @@ use feroxbuster::{
};
#[cfg(not(target_os = "windows"))]
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
/// Limits the number of parallel scans active at any given time when using --parallel
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0);
}

/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<HashSet<String>>> {
Expand Down Expand Up @@ -226,6 +239,72 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
}
};

// --parallel branch
if config.parallel > 0 {
log::trace!("enter: parallel branch");

PARALLEL_LIMITER.add_permits(config.parallel);

let invocation = args();

let para_regex =
Regex::new("--stdin|-q|--quiet|--silent|--verbosity|-v|-vv|-vvv|-vvvv").unwrap();

// remove stdin since only the original process will process targets
// remove quiet and silent so we can force silent later to normalize output
let mut original = invocation
.filter(|s| !para_regex.is_match(s))
.collect::<Vec<String>>();

original.push("--silent".to_string()); // only output modifier allowed

// we need remove --parallel from command line so we don't hit this branch over and over
// but we must remove --parallel N manually; the filter above never sees --parallel and the
// value passed to it at the same time, so can't filter them out in one pass

// unwrap is fine, as it has to be in the args for us to be in this code branch
let parallel_index = original.iter().position(|s| *s == "--parallel").unwrap();

// remove --parallel
original.remove(parallel_index);

// remove N passed to --parallel (it's the same index again since everything shifts
// from removing --parallel)
original.remove(parallel_index);

// unvalidated targets fresh from stdin, just spawn children and let them do all checks
for target in targets {
// add the current target to the provided command
let mut cloned = original.clone();
cloned.push("-u".to_string());
cloned.push(target);

let bin = cloned.index(0).to_owned(); // user's path to feroxbuster
let args = cloned.index(1..).to_vec(); // and args

let permit = PARALLEL_LIMITER.acquire().await?;

log::debug!("parallel exec: {} {}", bin, args.join(" "));

tokio::task::spawn_blocking(move || {
let result = Command::new(bin)
.args(&args)
.spawn()
.expect("failed to spawn a child process")
.wait()
.expect("child process errored during execution");

drop(permit);
result
});
}

clean_up(handles, tasks).await?;

log::trace!("exit: parallel branch && wrapped main");
return Ok(());
}

if matches!(config.output_level, OutputLevel::Default) {
// only print banner if output level is default (no banner on --quiet|--silent)
let std_stderr = stderr(); // std::io::stderr
Expand Down
8 changes: 8 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,14 @@ pub fn initialize() -> App<'static, 'static> {
.takes_value(true)
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
)
.arg(
Arg::with_name("parallel")
.long("parallel")
.value_name("PARALLEL_SCANS")
.takes_value(true)
.requires("stdin")
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
)
.arg(
Arg::with_name("rate_limit")
.long("rate-limit")
Expand Down
Loading

0 comments on commit 030b588

Please sign in to comment.