From 4198a019d3c371ee68a4b3920810049a437410cd Mon Sep 17 00:00:00 2001 From: Himadri Bhattacharjee Date: Tue, 2 May 2023 12:43:45 +0530 Subject: [PATCH] added functionality to use SSL key as client identity for mutual authentication --- Cargo.lock | 106 +++++++++++++++++++++++++++++ Cargo.toml | 2 +- shell_completions/_feroxbuster | 3 +- shell_completions/_feroxbuster.ps1 | 3 +- shell_completions/feroxbuster.bash | 8 ++- shell_completions/feroxbuster.elv | 3 +- src/banner/container.rs | 21 ++++-- src/client.rs | 34 ++++++++- src/config/container.rs | 35 +++++++--- src/extractor/container.rs | 13 +++- src/parser.rs | 14 +++- 11 files changed, 213 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ae429b..3ddb152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -2264,6 +2277,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -2273,20 +2287,39 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-socks", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rlimit" version = "0.9.1" @@ -2319,6 +2352,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.0", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -2363,6 +2417,16 @@ dependencies = [ "tendril", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "seahash" version = "4.1.0" @@ -2607,6 +2671,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -2844,6 +2914,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -3008,6 +3089,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.3.1" @@ -3182,6 +3269,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index f2da1f9..7c1bb77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ tokio = { version = "1.26", features = ["full"] } tokio-util = { version = "0.7", features = ["codec"] } log = "0.4" env_logger = "0.10" -reqwest = { version = "0.11", features = ["socks"] } +reqwest = { version = "0.11", features = ["socks", "rustls-tls"] } # uses feature unification to add 'serde' to reqwest::Url url = { version = "2.2", features = ["serde"] } serde_regex = "1.1" diff --git a/shell_completions/_feroxbuster b/shell_completions/_feroxbuster index df8d43d..db70e8d 100644 --- a/shell_completions/_feroxbuster +++ b/shell_completions/_feroxbuster @@ -53,7 +53,8 @@ _feroxbuster() { '*--status-codes=[Status Codes to include (allow list) (default\: All Status Codes)]:STATUS_CODE: ' \ '-T+[Number of seconds before a client'\''s request times out (default\: 7)]:SECONDS: ' \ '--timeout=[Number of seconds before a client'\''s request times out (default\: 7)]:SECONDS: ' \ -'--certificate=[Add a custom root certificate to connect to servers with a self-signed certificate]:PEM/DER:_files' \ +'--server-cert=[Add a custom root certificate to connect to servers with a self-signed certificate]:PEM/DER:_files' \ +'--client-cert=[Use a custom client SSL key file for mutual authentication]:PEM:_files' \ '-t+[Number of concurrent threads (default\: 50)]:THREADS: ' \ '--threads=[Number of concurrent threads (default\: 50)]:THREADS: ' \ '-d+[Maximum recursion depth, a depth of 0 is infinite recursion (default\: 4)]:RECURSION_DEPTH: ' \ diff --git a/shell_completions/_feroxbuster.ps1 b/shell_completions/_feroxbuster.ps1 index 4608e94..3934879 100644 --- a/shell_completions/_feroxbuster.ps1 +++ b/shell_completions/_feroxbuster.ps1 @@ -59,7 +59,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock { [CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)') [CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)') [CompletionResult]::new('--timeout', 'timeout', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)') - [CompletionResult]::new('--certificate', 'certificate', [CompletionResultType]::ParameterName, 'Add a custom root certificate to connect to servers with a self-signed certificate') + [CompletionResult]::new('--server-cert', 'server-cert', [CompletionResultType]::ParameterName, 'Add a custom root certificate to connect to servers with a self-signed certificate') + [CompletionResult]::new('--client-cert', 'client-cert', [CompletionResultType]::ParameterName, 'Use a custom client SSL key file for mutual authentication') [CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)') [CompletionResult]::new('--threads', 'threads', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)') [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)') diff --git a/shell_completions/feroxbuster.bash b/shell_completions/feroxbuster.bash index cd1a71b..340d592 100644 --- a/shell_completions/feroxbuster.bash +++ b/shell_completions/feroxbuster.bash @@ -19,7 +19,7 @@ _feroxbuster() { case "${cmd}" in feroxbuster) - opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --certificate --threads --no-recursion --depth --force-recursion --extract-links --dont-extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state --update --help --version" + opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --server-cert --client-cert --threads --no-recursion --depth --force-recursion --extract-links --dont-extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state --update --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -177,7 +177,11 @@ _feroxbuster() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - --certificate) + --server-cert) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + --client-cert) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; diff --git a/shell_completions/feroxbuster.elv b/shell_completions/feroxbuster.elv index 2320c27..17bd23d 100644 --- a/shell_completions/feroxbuster.elv +++ b/shell_completions/feroxbuster.elv @@ -56,7 +56,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words| cand --status-codes 'Status Codes to include (allow list) (default: All Status Codes)' cand -T 'Number of seconds before a client''s request times out (default: 7)' cand --timeout 'Number of seconds before a client''s request times out (default: 7)' - cand --certificate 'Add a custom root certificate to connect to servers with a self-signed certificate' + cand --server-cert 'Add a custom root certificate to connect to servers with a self-signed certificate' + cand --client-cert 'Use a custom client SSL key file for mutual authentication' cand -t 'Number of concurrent threads (default: 50)' cand --threads 'Number of concurrent threads (default: 50)' cand -d 'Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)' diff --git a/src/banner/container.rs b/src/banner/container.rs index 2f8851a..ff379f3 100644 --- a/src/banner/container.rs +++ b/src/banner/container.rs @@ -58,8 +58,11 @@ pub struct Banner { /// represents Configuration.proxy proxy: BannerEntry, - /// represents Configuration.certificate - certificate: BannerEntry, + /// represents Configuration.client_cert + client_cert: BannerEntry, + + /// represents Configuration.server_cert + server_cert: BannerEntry, /// represents Configuration.replay_proxy replay_proxy: BannerEntry, @@ -325,7 +328,8 @@ impl Banner { let auto_bail = BannerEntry::new("🙅", "Auto Bail", &config.auto_bail.to_string()); let cfg = BannerEntry::new("💉", "Config File", &config.config); let proxy = BannerEntry::new("💎", "Proxy", &config.proxy); - let certificate = BannerEntry::new("🏅", "Certificate", &config.certificate); + let server_cert = BannerEntry::new("🏅", "Server Certificate", &config.server_cert); + let client_cert = BannerEntry::new("🏅", "Client Certificate", &config.client_cert); let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string()); let wordlist = BannerEntry::new("📖", "Wordlist", &config.wordlist); let timeout = BannerEntry::new("💥", "Timeout (secs)", &config.timeout.to_string()); @@ -405,7 +409,8 @@ impl Banner { auto_bail, auto_tune, proxy, - certificate, + client_cert, + server_cert, replay_codes, replay_proxy, headers, @@ -560,8 +565,12 @@ by Ben "epi" Risher {} ver: {}"#, writeln!(&mut writer, "{}", self.proxy)?; } - if !config.certificate.is_empty() { - writeln!(&mut writer, "{}", self.certificate)?; + if !config.client_cert.is_empty() { + writeln!(&mut writer, "{}", self.client_cert)?; + } + + if !config.server_cert.is_empty() { + writeln!(&mut writer, "{}", self.server_cert)?; } if !config.replay_proxy.is_empty() { diff --git a/src/client.rs b/src/client.rs index fa4c5ea..d6c2ca8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,7 +16,8 @@ pub fn initialize( insecure: bool, headers: &HashMap, proxy: Option<&str>, - certificate: Option<&str>, + server_cert: Option<&str>, + client_cert: Option<&str>, ) -> Result { let policy = if redirects { Policy::limited(10) @@ -44,7 +45,7 @@ pub fn initialize( } } - if let Some(cert_path) = certificate { + if let Some(cert_path) = server_cert { let cert_path = Path::new(cert_path); let mut buf = Vec::new(); @@ -73,6 +74,32 @@ pub fn initialize( } } + if let Some(cert_path) = client_cert { + let cert_path = Path::new(cert_path); + let mut buf = Vec::new(); + + // if the root certificate path is not empty, open it + // and read it into a buffer + File::open(cert_path)?.read_to_end(&mut buf)?; + + // depending upon the extension of the file, create a + // certificate object from it using either the "pem" or "der" parser + + // in either case, add the root certificate to the client + if let Some(extension) = cert_path.extension() { + match extension.to_str() { + Some("pem") => { + let cert = reqwest::tls::Identity::from_pem(&buf)?; + client = client.identity(cert); + } + + // if we cannot determine the extension, do nothing + // or perhaps TODO: spew an error + _ => {} + } + } + } + Ok(client.build()?) } @@ -93,6 +120,7 @@ mod tests { &headers, Some("not a valid proxy"), None, + None, ) .unwrap(); } @@ -102,6 +130,6 @@ mod tests { fn client_with_good_proxy() { let headers = HashMap::new(); let proxy = "http://127.0.0.1:8080"; - initialize(0, "stuff", true, true, &headers, Some(proxy), None).unwrap(); + initialize(0, "stuff", true, true, &headers, Some(proxy), None, None).unwrap(); } } diff --git a/src/config/container.rs b/src/config/container.rs index 576c6a4..e365a62 100644 --- a/src/config/container.rs +++ b/src/config/container.rs @@ -106,7 +106,11 @@ pub struct Configuration { /// Path to a custom root certificate for connecting to servers with a self-signed certificate #[serde(default)] - pub certificate: String, + pub server_cert: String, + + /// Path to a custom client SSL key for mutual authentication with a server + #[serde(default)] + pub client_cert: String, /// The target URL #[serde(default)] @@ -336,6 +340,7 @@ impl Default for Configuration { &HashMap::new(), None, None, + None, ) .expect("Could not build client"); let replay_client = None; @@ -381,7 +386,8 @@ impl Default for Configuration { force_recursion: false, update_app: false, proxy: String::new(), - certificate: String::new(), + server_cert: String::new(), + client_cert: String::new(), config: String::new(), output: String::new(), debug_log: String::new(), @@ -845,7 +851,8 @@ impl Configuration { // organizational breakpoint; all options below alter the Client configuration //// update_config_if_present!(&mut config.proxy, args, "proxy", String); - update_config_if_present!(&mut config.certificate, args, "certificate", String); + update_config_if_present!(&mut config.server_cert, args, "server_cert", String); + update_config_if_present!(&mut config.client_cert, args, "client_cert", 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_with_num_type_if_present!(&mut config.timeout, args, "timeout", u64); @@ -933,10 +940,16 @@ impl Configuration { Some(configuration.proxy.as_str()) }; - let certificate = if configuration.certificate.is_empty() { + let server_cert = if configuration.server_cert.is_empty() { None } else { - Some(configuration.certificate.as_str()) + Some(configuration.server_cert.as_str()) + }; + + let client_cert = if configuration.client_cert.is_empty() { + None + } else { + Some(configuration.client_cert.as_str()) }; if proxy.is_some() @@ -946,7 +959,8 @@ impl Configuration { || configuration.insecure || !configuration.headers.is_empty() || configuration.resumed - || certificate.is_some() + || server_cert.is_some() + || client_cert.is_some() { configuration.client = client::initialize( configuration.timeout, @@ -955,7 +969,8 @@ impl Configuration { configuration.insecure, &configuration.headers, proxy, - certificate, + server_cert, + client_cert, ) .expect("Could not rebuild client"); } @@ -970,7 +985,8 @@ impl Configuration { configuration.insecure, &configuration.headers, Some(&configuration.replay_proxy), - certificate, + server_cert, + client_cert, ) .expect("Could not rebuild client"), ); @@ -1005,7 +1021,8 @@ impl Configuration { update_if_not_default!(&mut conf.target_url, new.target_url, ""); update_if_not_default!(&mut conf.time_limit, new.time_limit, ""); update_if_not_default!(&mut conf.proxy, new.proxy, ""); - update_if_not_default!(&mut conf.certificate, new.certificate, ""); + update_if_not_default!(&mut conf.server_cert, new.server_cert, ""); + update_if_not_default!(&mut conf.client_cert, new.client_cert, ""); update_if_not_default!(&mut conf.verbosity, new.verbosity, 0); update_if_not_default!(&mut conf.silent, new.silent, false); update_if_not_default!(&mut conf.quiet, new.quiet, false); diff --git a/src/extractor/container.rs b/src/extractor/container.rs index 2e626fa..b87e145 100644 --- a/src/extractor/container.rs +++ b/src/extractor/container.rs @@ -641,10 +641,16 @@ impl<'a> Extractor<'a> { Some(self.handles.config.proxy.as_str()) }; - let certificate = if self.handles.config.certificate.is_empty() { + let server_cert = if self.handles.config.server_cert.is_empty() { None } else { - Some(self.handles.config.certificate.as_str()) + Some(self.handles.config.server_cert.as_str()) + }; + + let client_cert = if self.handles.config.client_cert.is_empty() { + None + } else { + Some(self.handles.config.client_cert.as_str()) }; client = client::initialize( @@ -654,7 +660,8 @@ impl<'a> Extractor<'a> { self.handles.config.insecure, &self.handles.config.headers, proxy, - certificate, + server_cert, + client_cert, )?; } diff --git a/src/parser.rs b/src/parser.rs index 8b2b528..5ae1fd6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -392,8 +392,8 @@ pub fn initialize() -> Command { .help("Disables TLS certificate validation in the client"), ) .arg( - Arg::new("certificate") - .long("certificate") + Arg::new("server_cert") + .long("server-cert") .value_name("PEM/DER") .value_hint(ValueHint::FilePath) .num_args(1) @@ -401,6 +401,16 @@ pub fn initialize() -> Command { .help( "Add a custom root certificate to connect to servers with a self-signed certificate", ), + ).arg( + Arg::new("client_cert") + .long("client-cert") + .value_name("PEM") + .value_hint(ValueHint::FilePath) + .num_args(1) + .help_heading("Client settings") + .help( + "Use a custom client SSL key file for mutual authentication", + ), ); /////////////////////////////////////////////////////////////////////