config file searched for in multiple locations

added some better error messaging
updated docs/readme
closes #51
This commit is contained in:
epi 2020-10-03 08:00:01 -05:00
parent 6d6069a5b8
commit e005537064
7 changed files with 176 additions and 44 deletions

View File

@ -30,6 +30,7 @@ ansi_term = "0.12"
indicatif = "0.15"
console = "0.12"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "3.0"
[dev-dependencies]
tempfile = "3.1"
@ -45,4 +46,4 @@ panic = 'abort'
[package.metadata.deb]
section = "utility"
license-file = ["LICENSE", "4"]
conf-files = ["ferox-config.toml"]
conf-files = ["~/.config/feroxbuster/ferox-config.toml"]

View File

@ -94,9 +94,18 @@ Configuration begins with with the following built-in default values baked into
### ferox-config.toml
After setting built-in default values, any values defined in a `ferox-config.toml` config file will override the
built-in defaults. If `ferox-config.toml` is not found in the **same directory** as `feroxbuster`, nothing happens at this stage.
built-in defaults.
For example, say that we prefer to use a different wordlist as our default when scanning; we can
`feroxbuster` searches for `ferox-config.toml` in the following locations (in the order shown):
- `CONFIG_DIR/ferxobuster/`
- The same directory as the `feroxbuster` executable
- The user's current working directory
If more than one valid configuration file is found, each one overwrites the values found previously.
If no configuration file is found, nothing happens at this stage.
As an example, let's say that we prefer to use a different wordlist as our default when scanning; we can
set the `wordlist` value in the config file to override the baked-in default.
Notes of interest:

View File

@ -99,6 +99,13 @@ by Ben "epi" Risher {} ver: {}"#,
); // 🦡
// followed by the maybe printed or variably displayed values
if !CONFIGURATION.config.is_empty() {
eprintln!(
"{}",
format_banner_entry!("\u{1f489}", "Config File", CONFIGURATION.config)
); // 💉
}
if !CONFIGURATION.proxy.is_empty() {
eprintln!(
"{}",

View File

@ -1,4 +1,5 @@
use crate::utils::status_colorizer;
use ansi_term::Color::Cyan;
use reqwest::header::HeaderMap;
use reqwest::{redirect::Policy, Client, Proxy};
use std::collections::HashMap;
@ -25,8 +26,9 @@ pub fn initialize(
Ok(map) => map,
Err(e) => {
eprintln!(
"[{}] - Client::initialize: {}",
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("Client::initialize"),
e
);
exit(1);
@ -45,13 +47,15 @@ pub fn initialize(
Ok(proxy_obj) => client.proxy(proxy_obj),
Err(e) => {
eprintln!(
"[{}] - Could not add proxy ({:?}) to Client configuration",
"{} {} Could not add proxy ({:?}) to Client configuration",
status_colorizer("ERROR"),
Cyan.paint("Client::initialize"),
proxy
);
eprintln!(
"[{}] - Client::initialize: {}",
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("Client::initialize"),
e
);
exit(1);
@ -65,10 +69,16 @@ pub fn initialize(
Ok(client) => client,
Err(e) => {
eprintln!(
"[{}] - Could not create a Client with the given configuration, exiting.",
status_colorizer("ERROR")
"{} {} Could not create a Client with the given configuration, exiting.",
status_colorizer("ERROR"),
Cyan.paint("Client::build")
);
eprintln!(
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("Client::build"),
e
);
eprintln!("[{}] - Client::build: {}", status_colorizer("ERROR"), e);
exit(1);
}
}

View File

@ -1,15 +1,16 @@
use crate::utils::status_colorizer;
use crate::{client, parser, progress};
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
use ansi_term::Color::Cyan;
use clap::value_t;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
use lazy_static::lazy_static;
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use std::collections::HashMap;
use std::env::current_exe;
use std::env::{current_dir, current_exe};
use std::fs::read_to_string;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
lazy_static! {
@ -39,6 +40,10 @@ pub struct Configuration {
#[serde(default = "wordlist")]
pub wordlist: String,
/// Path to the config file used
#[serde(default)]
pub config: String,
/// Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
#[serde(default)]
pub proxy: String,
@ -181,6 +186,7 @@ impl Default for Configuration {
norecursion: false,
redirects: false,
proxy: String::new(),
config: String::new(),
output: String::new(),
target_url: String::new(),
queries: Vec::new(),
@ -202,6 +208,7 @@ impl Configuration {
/// - **timeout**: `5` seconds
/// - **redirects**: `false`
/// - **wordlist**: [`DEFAULT_WORDLIST`](constant.DEFAULT_WORDLIST.html)
/// - **config**: `None`
/// - **threads**: `50`
/// - **timeout**: `7` seconds
/// - **verbosity**: `0` (no logging enabled)
@ -225,6 +232,13 @@ impl Configuration {
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
/// built-in defaults.
///
/// `ferox-config.toml` can be placed in any of the following locations (in the order shown):
/// - `CONFIG_DIR/ferxobuster/`
/// - The same directory as the `feroxbuster` executable
/// - The user's current working directory
///
/// If more than one valid configuration file is found, each one overwrites the values found previously.
///
/// Finally, any options/arguments given on the commandline will override both built-in and
/// config-file specified values.
///
@ -239,33 +253,37 @@ impl Configuration {
// therein to overwrite our default values. Deserialized defaults are specified
// in the Configuration struct so that we don't change anything that isn't
// actually specified in the config file
//
// search for a config using the following order of precedence
// - CONFIG_DIR/ferxobuster/
// - same directory as feroxbuster executable
// - current directory
// merge a config found at ~/.config/feroxbuster/ferox-config.toml
if let Some(config_dir) = dirs::config_dir() {
// config_dir() resolves to one of the following
// - linux: $XDG_CONFIG_HOME or $HOME/.config
// - macOS: $HOME/Library/Application Support
// - windows: {FOLDERID_RoamingAppData}
let config_file = config_dir.join("feroxbuster").join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
};
// merge a config found in same the directory as feroxbuster executable
if let Ok(exe_path) = current_exe() {
if let Some(bin_dir) = exe_path.parent() {
if let Some(settings) = Self::parse_config(bin_dir) {
config.threads = settings.threads;
config.wordlist = settings.wordlist;
config.statuscodes = settings.statuscodes;
config.proxy = settings.proxy;
config.timeout = settings.timeout;
config.verbosity = settings.verbosity;
config.quiet = settings.quiet;
config.output = settings.output;
config.useragent = settings.useragent;
config.redirects = settings.redirects;
config.insecure = settings.insecure;
config.extensions = settings.extensions;
config.headers = settings.headers;
config.queries = settings.queries;
config.norecursion = settings.norecursion;
config.addslash = settings.addslash;
config.stdin = settings.stdin;
config.depth = settings.depth;
config.sizefilters = settings.sizefilters;
config.dontfilter = settings.dontfilter;
}
let config_file = bin_dir.join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
};
};
// merge a config found in the user's current working directory
if let Ok(cwd) = current_dir() {
let config_file = cwd.join(DEFAULT_CONFIG_NAME);
Self::parse_and_merge_config(config_file, &mut config);
}
let args = parser::initialize().get_matches();
// the .is_some appears clunky, but it allows default values to be incrementally
@ -441,23 +459,64 @@ impl Configuration {
config
}
/// If present, read in `/path/to/binary's/parent/DEFAULT_CONFIG_NAME` and deserialize the specified values
/// Given a configuration file's location and an instance of `Configuration`, read in
/// the config file if found and update the current settings with the settings found therein
fn parse_and_merge_config(config_file: PathBuf, mut config: &mut Self) {
if config_file.exists() {
// save off a string version of the path before it goes out of scope
let conf_str = match config_file.to_str() {
Some(cs) => String::from(cs),
None => String::new(),
};
if let Some(settings) = Self::parse_config(config_file) {
// set the config used for viewing in the banner
config.config = conf_str;
// update the settings
Self::merge_config(&mut config, settings);
}
}
}
/// Given two Configurations, overwrite `settings` with the fields found in `settings_to_merge`
fn merge_config(settings: &mut Self, settings_to_merge: Self) {
settings.threads = settings_to_merge.threads;
settings.wordlist = settings_to_merge.wordlist;
settings.statuscodes = settings_to_merge.statuscodes;
settings.proxy = settings_to_merge.proxy;
settings.timeout = settings_to_merge.timeout;
settings.verbosity = settings_to_merge.verbosity;
settings.quiet = settings_to_merge.quiet;
settings.output = settings_to_merge.output;
settings.useragent = settings_to_merge.useragent;
settings.redirects = settings_to_merge.redirects;
settings.insecure = settings_to_merge.insecure;
settings.extensions = settings_to_merge.extensions;
settings.headers = settings_to_merge.headers;
settings.queries = settings_to_merge.queries;
settings.norecursion = settings_to_merge.norecursion;
settings.addslash = settings_to_merge.addslash;
settings.stdin = settings_to_merge.stdin;
settings.depth = settings_to_merge.depth;
settings.sizefilters = settings_to_merge.sizefilters;
settings.dontfilter = settings_to_merge.dontfilter;
}
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
///
/// uses serde to deserialize the toml into a `Configuration` struct
///
/// If toml cannot be parsed a `Configuration::default` instance is returned
fn parse_config(directory: &Path) -> Option<Self> {
let directory = directory.join(DEFAULT_CONFIG_NAME);
if let Ok(content) = read_to_string(directory) {
fn parse_config(config_file: PathBuf) -> Option<Self> {
if let Ok(content) = read_to_string(config_file) {
match toml::from_str(content.as_str()) {
Ok(config) => {
return Some(config);
}
Err(e) => {
println!(
"[{}] - config::parse_config {}",
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("config::parse_config"),
e
);
}
@ -473,6 +532,7 @@ mod tests {
use std::fs::write;
use tempfile::TempDir;
/// creates a dummy configuration file for testing
fn setup_config_test() -> Configuration {
let data = r#"
wordlist = "/some/path"
@ -497,16 +557,18 @@ mod tests {
"#;
let tmp_dir = TempDir::new().unwrap();
let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME);
write(file, data).unwrap();
Configuration::parse_config(tmp_dir.path()).unwrap()
write(&file, data).unwrap();
Configuration::parse_config(file).unwrap()
}
#[test]
/// test that all default config values meet expectations
fn default_configuration() {
let config = Configuration::default();
assert_eq!(config.wordlist, wordlist());
assert_eq!(config.proxy, String::new());
assert_eq!(config.target_url, String::new());
assert_eq!(config.config, String::new());
assert_eq!(config.statuscodes, statuscodes());
assert_eq!(config.threads, threads());
assert_eq!(config.depth, depth());
@ -526,108 +588,126 @@ mod tests {
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_wordlist() {
let config = setup_config_test();
assert_eq!(config.wordlist, "/some/path");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_statuscodes() {
let config = setup_config_test();
assert_eq!(config.statuscodes, vec![201, 301, 401]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_threads() {
let config = setup_config_test();
assert_eq!(config.threads, 40);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_depth() {
let config = setup_config_test();
assert_eq!(config.depth, 1);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_timeout() {
let config = setup_config_test();
assert_eq!(config.timeout, 5);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_proxy() {
let config = setup_config_test();
assert_eq!(config.proxy, "http://127.0.0.1:8080");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_quiet() {
let config = setup_config_test();
assert_eq!(config.quiet, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_verbosity() {
let config = setup_config_test();
assert_eq!(config.verbosity, 1);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_output() {
let config = setup_config_test();
assert_eq!(config.output, "/some/otherpath");
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_redirects() {
let config = setup_config_test();
assert_eq!(config.redirects, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_insecure() {
let config = setup_config_test();
assert_eq!(config.insecure, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_norecursion() {
let config = setup_config_test();
assert_eq!(config.norecursion, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_stdin() {
let config = setup_config_test();
assert_eq!(config.stdin, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_dontfilter() {
let config = setup_config_test();
assert_eq!(config.dontfilter, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_addslash() {
let config = setup_config_test();
assert_eq!(config.addslash, true);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_extensions() {
let config = setup_config_test();
assert_eq!(config.extensions, vec!["html", "php", "js"]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_sizefilters() {
let config = setup_config_test();
assert_eq!(config.sizefilters, vec![4120]);
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_headers() {
let config = setup_config_test();
let mut headers = HashMap::new();
@ -637,6 +717,7 @@ mod tests {
}
#[test]
/// parse the test config and see that the values parsed are correct
fn config_reads_queries() {
let config = setup_config_test();
let mut queries = vec![];

View File

@ -258,6 +258,12 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
if good_urls.is_empty() {
log::error!("Could not connect to any target provided, exiting.");
log::trace!("exit: connectivity_test");
eprintln!(
"{} {} Could not connect to any target provided",
status_colorizer("ERROR"),
Cyan.paint("heuristics::connectivity_test"),
);
process::exit(1);
}

View File

@ -1,11 +1,13 @@
use ansi_term::Color::Cyan;
use feroxbuster::config::{CONFIGURATION, PROGRESS_PRINTER};
use feroxbuster::scanner::scan_url;
use feroxbuster::utils::get_current_depth;
use feroxbuster::utils::{get_current_depth, status_colorizer};
use feroxbuster::{banner, heuristics, logger, FeroxResult};
use futures::StreamExt;
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::process;
use std::sync::Arc;
use tokio::io;
use tokio_util::codec::{FramedRead, LinesCodec};
@ -17,6 +19,12 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
let file = match File::open(&path) {
Ok(f) => f,
Err(e) => {
eprintln!(
"{} {} {}",
status_colorizer("ERROR"),
Cyan.paint("main::get_unique_words_from_wordlist"),
e
);
log::error!("Could not open wordlist: {}", e);
log::trace!("exit: get_unique_words_from_wordlist -> {}", e);
@ -56,6 +64,16 @@ async fn scan(targets: Vec<String>) -> FeroxResult<()> {
tokio::spawn(async move { get_unique_words_from_wordlist(&CONFIGURATION.wordlist) })
.await??;
if words.len() == 0 {
eprintln!(
"{} {} Did not find any words in {}",
status_colorizer("ERROR"),
Cyan.paint("main::scan"),
CONFIGURATION.wordlist
);
process::exit(1);
}
let mut tasks = vec![];
for target in targets {