mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-22 20:31:13 -03:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
185808b289 | ||
|
|
f676f56d71 | ||
|
|
fbffb57db3 | ||
|
|
26e27c340b | ||
|
|
530672f45f | ||
|
|
2f26187f61 | ||
|
|
4515e6a516 | ||
|
|
2e8f05883d | ||
|
|
aa7871cca8 | ||
|
|
40e803ef07 | ||
|
|
86199002c9 | ||
|
|
29abef6386 | ||
|
|
d9271f6fe7 | ||
|
|
9881d65cc3 | ||
|
|
11f7a7e6f7 | ||
|
|
f64c5a8fdb | ||
|
|
3cf278a77a | ||
|
|
5327f3931e | ||
|
|
4cf8f030de | ||
|
|
2a8ebd0e04 | ||
|
|
8d335d7e90 | ||
|
|
ec1458cdc3 | ||
|
|
109d38f2ea | ||
|
|
2751bb844a | ||
|
|
74b0065ce2 | ||
|
|
caa3674bba | ||
|
|
4f557511b4 | ||
|
|
238f071d0a | ||
|
|
d19c7bfe17 | ||
|
|
65c0138e1a | ||
|
|
db0e56bee2 | ||
|
|
71649d1296 | ||
|
|
a89f2be37b | ||
|
|
572e5b7a95 | ||
|
|
2e71d91960 | ||
|
|
f9cdd91da9 | ||
|
|
003b7f39f7 | ||
|
|
39dfe442e8 | ||
|
|
7d75a2cfd4 | ||
|
|
9657385282 | ||
|
|
4c1094b59c | ||
|
|
63ce5787d7 | ||
|
|
5af8812929 | ||
|
|
d5c508bc28 | ||
|
|
603004a5bd | ||
|
|
a906b9731e | ||
|
|
f173147352 | ||
|
|
bb1532e459 |
3
.github/actions-rs/grcov.yml
vendored
3
.github/actions-rs/grcov.yml
vendored
@@ -1,7 +1,8 @@
|
||||
branch: true
|
||||
branch: false
|
||||
ignore-not-existing: true
|
||||
llvm: true
|
||||
output-type: lcov
|
||||
output-path: ./lcov.info
|
||||
# excl-br-line: "^\\s*((debug_)?assert(_eq|_ne)?!|#\\[derive\\(|log::)"
|
||||
ignore:
|
||||
- "../*"
|
||||
|
||||
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@@ -41,10 +41,19 @@ jobs:
|
||||
use-cross: true
|
||||
command: build
|
||||
args: --release --target=${{ matrix.target }}
|
||||
- name: Build tar.gz for homebrew installs
|
||||
if: matrix.type == 'ubuntu-x64'
|
||||
run: |
|
||||
tar czf ${{ matrix.name }}.tar.gz -C target/x86_64-unknown-linux-musl/release feroxbuster
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: ${{ matrix.path }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: matrix.type == 'ubuntu-x64'
|
||||
with:
|
||||
name: ${{ matrix.name }}.tar.gz
|
||||
path: ${{ matrix.name }}.tar.gz
|
||||
|
||||
build-deb:
|
||||
needs: [build-nix]
|
||||
@@ -59,18 +68,40 @@ jobs:
|
||||
name: feroxbuster_amd64.deb
|
||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
|
||||
build-rest:
|
||||
build-macos:
|
||||
runs-on: macos-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: x86_64-apple-darwin
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: true
|
||||
command: build
|
||||
args: --release --target=x86_64-apple-darwin
|
||||
- name: Build tar.gz for homebrew installs
|
||||
run: |
|
||||
tar czf x86_64-macos-feroxbuster.tar.gz -C target/x86_64-apple-darwin/release feroxbuster
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: x86_64-macos-feroxbuster
|
||||
path: target/x86_64-apple-darwin/release/feroxbuster
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: x86_64-macos-feroxbuster.tar.gz
|
||||
path: x86_64-macos-feroxbuster.tar.gz
|
||||
|
||||
build-windows:
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.ref == 'refs/heads/master'
|
||||
strategy:
|
||||
matrix:
|
||||
type: [windows-x64, windows-x86, macos]
|
||||
type: [windows-x64, windows-x86]
|
||||
include:
|
||||
- type: macos
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
name: x86_64-macos-feroxbuster
|
||||
path: target/x86_64-apple-darwin/release/feroxbuster
|
||||
- type: windows-x64
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
@@ -97,3 +128,4 @@ jobs:
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
path: ${{ matrix.path }}
|
||||
|
||||
|
||||
4
.github/workflows/coverage.yml
vendored
4
.github/workflows/coverage.yml
vendored
@@ -20,8 +20,8 @@ jobs:
|
||||
args: --all-features --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
|
||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort'
|
||||
RUSTDOCFLAGS: '-Cpanic=abort'
|
||||
- uses: actions-rs/grcov@v0.1
|
||||
- name: Convert lcov to xml
|
||||
run: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
|
||||
66
README.md
66
README.md
@@ -62,6 +62,7 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
||||
- [Downloads](#-downloads)
|
||||
- [Installation](#-installation)
|
||||
- [Download a Release](#download-a-release)
|
||||
- [Homebrew on MacOS and Linux](#homebrew-on-macos-and-linux)
|
||||
- [Cargo Install](#cargo-install)
|
||||
- [apt Install](#apt-install)
|
||||
- [Docker Install](#docker-install)
|
||||
@@ -83,13 +84,62 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
||||
|
||||
### Download a Release
|
||||
|
||||
Releases for multiple architectures can be found in the [Releases](https://github.com/epi052/feroxbuster/releases) section. Builds for the following systems are currently supported:
|
||||
Releases for multiple architectures can be found in the [Releases](https://github.com/epi052/feroxbuster/releases) section. The latest release for each of the following systems can be downloaded and executed as shown below.
|
||||
|
||||
- Linux x86
|
||||
- Linux x86_64
|
||||
- MacOS x86_64
|
||||
- Windows x86
|
||||
- Windows x86_64
|
||||
#### Linux x86
|
||||
```
|
||||
wget -sLO https://github.com/epi052/feroxbuster/releases/latest/download/x86-linux-feroxbuster.zip
|
||||
unzip x86-linux-feroxbuster.zip
|
||||
./feroxbuster -V
|
||||
```
|
||||
#### Linux x86_64
|
||||
|
||||
```
|
||||
wget -sLO https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip
|
||||
unzip x86_64-linux-feroxbuster.zip
|
||||
./feroxbuster -V
|
||||
```
|
||||
|
||||
#### MacOS x86_64
|
||||
```
|
||||
wget -sLO https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-macos-feroxbuster.zip
|
||||
unzip x86_64-macos-feroxbuster.zip
|
||||
./feroxbuster -V
|
||||
```
|
||||
|
||||
#### Windows x86
|
||||
|
||||
```
|
||||
https://github.com/epi052/feroxbuster/releases/latest/download/x86-windows-feroxbuster.exe.zip
|
||||
Expand-Archive .\feroxbuster.zip
|
||||
.\feroxbuster\feroxbuster.exe -V
|
||||
```
|
||||
|
||||
#### Windows x86_64
|
||||
|
||||
```
|
||||
Invoke-WebRequest https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-windows-feroxbuster.exe.zip -OutFile feroxbuster.zip
|
||||
Expand-Archive .\feroxbuster.zip
|
||||
.\feroxbuster\feroxbuster.exe -V
|
||||
```
|
||||
|
||||
### Homebrew on MacOS and Linux
|
||||
|
||||
Installable by Homebrew throughout own formulas:
|
||||
|
||||
🍏 [MacOS](https://github.com/TGotwig/homebrew-feroxbuster/blob/main/feroxbuster.rb)
|
||||
|
||||
```shell
|
||||
brew tap tgotwig/feroxbuster
|
||||
brew install feroxbuster
|
||||
```
|
||||
|
||||
🐧 [Linux](https://github.com/TGotwig/homebrew-linux-feroxbuster/blob/main/feroxbuster.rb)
|
||||
|
||||
```shell
|
||||
brew tap tgotwig/linux-feroxbuster
|
||||
brew install feroxbuster
|
||||
```
|
||||
|
||||
### Cargo Install
|
||||
|
||||
@@ -101,9 +151,11 @@ cargo install feroxbuster
|
||||
|
||||
### apt Install
|
||||
|
||||
Head to the [Releases](https://github.com/epi052/feroxbuster/releases) section and download `feroxbuster_amd64.deb`. After that, use your favorite package manager to install the .deb.
|
||||
Download `feroxbuster_amd64.deb` from the [Releases](https://github.com/epi052/feroxbuster/releases) section. After that, use your favorite package manager to install the `.deb`.
|
||||
|
||||
```
|
||||
wget -sLO https://github.com/epi052/feroxbuster/releases/latest/download/feroxbuster_amd64.deb.zip
|
||||
unzip feroxbuster_amd64.deb.zip
|
||||
sudo apt install ./feroxbuster_amd64.deb
|
||||
```
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::utils::{module_colorizer, status_colorizer};
|
||||
use console::style;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::{redirect::Policy, Client, Proxy};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
#[cfg(not(test))]
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -22,18 +22,8 @@ pub fn initialize(
|
||||
Policy::none()
|
||||
};
|
||||
|
||||
let header_map: HeaderMap = match headers.try_into() {
|
||||
Ok(map) => map,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Client::initialize"),
|
||||
e
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
// try_into returns infallible as its error, unwrap is safe here
|
||||
let header_map: HeaderMap = headers.try_into().unwrap();
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::new(timeout, 0))
|
||||
@@ -55,9 +45,13 @@ pub fn initialize(
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
style("Client::initialize").cyan(),
|
||||
module_colorizer("Client::initialize"),
|
||||
e
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
panic!();
|
||||
#[cfg(not(test))]
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +73,32 @@ pub fn initialize(
|
||||
module_colorizer("Client::build"),
|
||||
e
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
panic!();
|
||||
#[cfg(not(test))]
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
/// create client with a bad proxy, expect panic
|
||||
fn client_with_bad_proxy() {
|
||||
let headers = HashMap::new();
|
||||
initialize(0, "stuff", true, false, &headers, Some("not a valid proxy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// create client with a proxy, expect no error
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,12 @@ impl Configuration {
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("[!] Error encountered: {}", e);
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Configuration::new"),
|
||||
e
|
||||
);
|
||||
exit(1)
|
||||
})
|
||||
.as_u16()
|
||||
@@ -347,7 +352,12 @@ impl Configuration {
|
||||
.unwrap() // already known good
|
||||
.map(|size| {
|
||||
size.parse::<u64>().unwrap_or_else(|e| {
|
||||
eprintln!("[!] Error encountered: {}", e);
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Configuration::new"),
|
||||
e
|
||||
);
|
||||
exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ use console::style;
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::Response;
|
||||
use std::process;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// length of a standard UUID, used when determining wildcard responses
|
||||
@@ -21,7 +22,11 @@ const UUID_LENGTH: u64 = 32;
|
||||
/// configuration and any static wildcard lengths.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WildcardFilter {
|
||||
/// size of the response that will later be combined with the length of the path of the url
|
||||
/// requested
|
||||
pub dynamic: u64,
|
||||
|
||||
/// size of the response that should be included with filters passed via runtime configuration
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
@@ -48,8 +53,17 @@ fn unique_string(length: usize) -> String {
|
||||
///
|
||||
/// In the event that url returns a wildcard response, a
|
||||
/// [WildcardFilter](struct.WildcardFilter.html) is created and returned to the caller.
|
||||
pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<WildcardFilter> {
|
||||
log::trace!("enter: wildcard_test({:?})", target_url);
|
||||
pub async fn wildcard_test(
|
||||
target_url: &str,
|
||||
bar: ProgressBar,
|
||||
tx_file: UnboundedSender<String>,
|
||||
) -> Option<WildcardFilter> {
|
||||
log::trace!(
|
||||
"enter: wildcard_test({:?}, {:?}, {:?})",
|
||||
target_url,
|
||||
bar,
|
||||
tx_file
|
||||
);
|
||||
|
||||
if CONFIGURATION.dontfilter {
|
||||
// early return, dontfilter scans don't need tested
|
||||
@@ -57,7 +71,10 @@ pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<Wildcar
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(resp_one) = make_wildcard_request(&target_url, 1).await {
|
||||
let clone_req_one = tx_file.clone();
|
||||
let clone_req_two = tx_file.clone();
|
||||
|
||||
if let Some(resp_one) = make_wildcard_request(&target_url, 1, clone_req_one).await {
|
||||
bar.inc(1);
|
||||
|
||||
// found a wildcard response
|
||||
@@ -72,7 +89,7 @@ pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<Wildcar
|
||||
|
||||
// content length of wildcard is non-zero, perform additional tests:
|
||||
// make a second request, with a known-sized (64) longer request
|
||||
if let Some(resp_two) = make_wildcard_request(&target_url, 3).await {
|
||||
if let Some(resp_two) = make_wildcard_request(&target_url, 3, clone_req_two).await {
|
||||
bar.inc(1);
|
||||
|
||||
let wc2_length = resp_two.content_length().unwrap_or(0);
|
||||
@@ -83,29 +100,43 @@ pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<Wildcar
|
||||
let url_len = get_url_path_length(&resp_one.url());
|
||||
|
||||
if !CONFIGURATION.quiet {
|
||||
ferox_print(
|
||||
&format!(
|
||||
"{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}",
|
||||
let msg = format!(
|
||||
"{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n",
|
||||
status_colorizer("WLD"),
|
||||
wc_length - url_len,
|
||||
style("auto-filtering").yellow(),
|
||||
style(wc_length - url_len).cyan(),
|
||||
style("--dontfilter").yellow()
|
||||
), &PROGRESS_PRINTER
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
|
||||
try_send_message_to_file(
|
||||
&msg,
|
||||
tx_file.clone(),
|
||||
!CONFIGURATION.output.is_empty(),
|
||||
);
|
||||
}
|
||||
|
||||
wildcard.dynamic = wc_length - url_len;
|
||||
} else if wc_length == wc2_length {
|
||||
if !CONFIGURATION.quiet {
|
||||
ferox_print(&format!(
|
||||
"{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}",
|
||||
let msg = format!(
|
||||
"{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n",
|
||||
status_colorizer("WLD"),
|
||||
wc_length,
|
||||
style("auto-filtering").yellow(),
|
||||
style(wc_length).cyan(),
|
||||
style("--dontfilter").yellow()
|
||||
), &PROGRESS_PRINTER);
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
|
||||
try_send_message_to_file(
|
||||
&msg,
|
||||
tx_file.clone(),
|
||||
!CONFIGURATION.output.is_empty(),
|
||||
);
|
||||
}
|
||||
wildcard.size = wc_length;
|
||||
}
|
||||
@@ -127,8 +158,17 @@ pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<Wildcar
|
||||
/// Once the unique url is created, the request is sent to the server. If the server responds
|
||||
/// back with a valid status code, the response is considered to be a wildcard response. If that
|
||||
/// wildcard response has a 3xx status code, that redirection location is displayed to the user.
|
||||
async fn make_wildcard_request(target_url: &str, length: usize) -> Option<Response> {
|
||||
log::trace!("enter: make_wildcard_request({}, {})", target_url, length);
|
||||
async fn make_wildcard_request(
|
||||
target_url: &str,
|
||||
length: usize,
|
||||
tx_file: UnboundedSender<String>,
|
||||
) -> Option<Response> {
|
||||
log::trace!(
|
||||
"enter: make_wildcard_request({}, {}, {:?})",
|
||||
target_url,
|
||||
length,
|
||||
tx_file
|
||||
);
|
||||
|
||||
let unique_str = unique_string(length);
|
||||
|
||||
@@ -160,44 +200,60 @@ async fn make_wildcard_request(target_url: &str, length: usize) -> Option<Respon
|
||||
let content_len = response.content_length().unwrap_or(0);
|
||||
|
||||
if !CONFIGURATION.quiet {
|
||||
ferox_print(
|
||||
&format!(
|
||||
"{} {:>10} Got {} for {} (url length: {})",
|
||||
wildcard,
|
||||
content_len,
|
||||
status_colorizer(&response.status().as_str()),
|
||||
response.url(),
|
||||
url_len
|
||||
),
|
||||
&PROGRESS_PRINTER,
|
||||
let msg = format!(
|
||||
"{} {:>10} Got {} for {} (url length: {})\n",
|
||||
wildcard,
|
||||
content_len,
|
||||
status_colorizer(&response.status().as_str()),
|
||||
response.url(),
|
||||
url_len
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
|
||||
try_send_message_to_file(
|
||||
&msg,
|
||||
tx_file.clone(),
|
||||
!CONFIGURATION.output.is_empty(),
|
||||
);
|
||||
}
|
||||
|
||||
if response.status().is_redirection() {
|
||||
// show where it goes, if possible
|
||||
if let Some(next_loc) = response.headers().get("Location") {
|
||||
if let Ok(next_loc_str) = next_loc.to_str() {
|
||||
if !CONFIGURATION.quiet {
|
||||
ferox_print(
|
||||
&format!(
|
||||
"{} {:>10} {} redirects to => {}",
|
||||
wildcard,
|
||||
content_len,
|
||||
response.url(),
|
||||
next_loc_str
|
||||
),
|
||||
&PROGRESS_PRINTER,
|
||||
);
|
||||
}
|
||||
} else if !CONFIGURATION.quiet {
|
||||
ferox_print(
|
||||
&format!(
|
||||
"{} {:>10} {} redirects to => {:?}",
|
||||
let msg = format!(
|
||||
"{} {:>10} {} redirects to => {}\n",
|
||||
wildcard,
|
||||
content_len,
|
||||
response.url(),
|
||||
next_loc
|
||||
),
|
||||
&PROGRESS_PRINTER,
|
||||
next_loc_str
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
|
||||
try_send_message_to_file(
|
||||
&msg,
|
||||
tx_file.clone(),
|
||||
!CONFIGURATION.output.is_empty(),
|
||||
);
|
||||
}
|
||||
} else if !CONFIGURATION.quiet {
|
||||
let msg = format!(
|
||||
"{} {:>10} {} redirects to => {:?}\n",
|
||||
wildcard,
|
||||
content_len,
|
||||
response.url(),
|
||||
next_loc
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
|
||||
try_send_message_to_file(
|
||||
&msg,
|
||||
tx_file.clone(),
|
||||
!CONFIGURATION.output.is_empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -274,15 +330,87 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
||||
good_urls
|
||||
}
|
||||
|
||||
/// simple helper to keep DRY; sends a message using the transmitter side of the given mpsc channel
|
||||
/// the receiver is expected to be the side that saves the message to CONFIGURATION.output.
|
||||
fn try_send_message_to_file(msg: &str, tx_file: UnboundedSender<String>, save_output: bool) {
|
||||
log::trace!("enter: try_send_message_to_file({}, {:?})", msg, tx_file);
|
||||
|
||||
if save_output {
|
||||
match tx_file.send(msg.to_string()) {
|
||||
Ok(_) => {
|
||||
log::trace!(
|
||||
"sent message from heuristics::try_send_message_to_file to file handler"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("heuristics::try_send_message_to_file"),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: try_send_message_to_file");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::FeroxChannel;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
/// request a unique string of 32bytes * a value returns correct result
|
||||
fn unique_string_returns_correct_length() {
|
||||
fn heuristics_unique_string_returns_correct_length() {
|
||||
for i in 0..10 {
|
||||
assert_eq!(unique_string(i).len(), i * 32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// simply test the default values for wildcardfilter, expect 0, 0
|
||||
fn heuristics_wildcardfilter_dafaults() {
|
||||
let wcf = WildcardFilter::default();
|
||||
assert_eq!(wcf.size, 0);
|
||||
assert_eq!(wcf.dynamic, 0);
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// tests that given a message and transmitter, the function sends the message across the
|
||||
/// channel
|
||||
async fn heuristics_try_send_message_to_file_sends_when_true() {
|
||||
let (tx, mut rx): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||
let msg = "It really tied the room together.";
|
||||
let should_save = true;
|
||||
try_send_message_to_file(&msg, tx, should_save);
|
||||
|
||||
assert_eq!(rx.recv().await.unwrap(), msg);
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
#[should_panic]
|
||||
/// tests that when save_output is false, nothing is sent to the receiver
|
||||
async fn heuristics_try_send_message_to_file_sends_when_false() {
|
||||
let (tx, mut rx): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||
let msg = "I'm the Dude, so that's what you call me.";
|
||||
let should_save = false;
|
||||
try_send_message_to_file(&msg, tx, should_save);
|
||||
|
||||
assert_ne!(rx.recv().await.unwrap(), msg);
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// tests that when save_output is true, but the receiver is closed, nothing is sent to the receiver
|
||||
/// this test doesn't assert anything, but reaches the error block of the given function and
|
||||
/// can be verified with --nocapture and RUST_LOG being set
|
||||
async fn heuristics_try_send_message_to_file_sends_with_closed_receiver() {
|
||||
env_logger::init();
|
||||
let (tx, mut rx): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||
let msg = "Hey, nice marmot.";
|
||||
let should_save = true;
|
||||
rx.close();
|
||||
try_send_message_to_file(&msg, tx, should_save);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,20 @@ pub mod heuristics;
|
||||
pub mod logger;
|
||||
pub mod parser;
|
||||
pub mod progress;
|
||||
pub mod reporter;
|
||||
pub mod scanner;
|
||||
pub mod utils;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
/// Generic Result type to ease error handling in async contexts
|
||||
pub type FeroxResult<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
|
||||
|
||||
/// Generic mpsc::unbounded_channel type to tidy up some code
|
||||
pub type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
|
||||
|
||||
/// Version pulled from Cargo.toml at compile time
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config::PROGRESS_PRINTER;
|
||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||
use crate::reporter::{get_cached_file_handle, safe_file_write};
|
||||
use console::{style, Color};
|
||||
use env_logger::Builder;
|
||||
use std::env;
|
||||
@@ -27,6 +28,19 @@ pub fn initialize(verbosity: u8) {
|
||||
let start = Instant::now();
|
||||
let mut builder = Builder::from_default_env();
|
||||
|
||||
// I REALLY wanted the logger to also use the reporting channels found in the `reporter`
|
||||
// module. However, in order to properly clean up the channels, all references to the
|
||||
// transmitter side of a channel need to go out of scope, then you can await the future into
|
||||
// which the receiver was moved.
|
||||
//
|
||||
// The problem was that putting a transmitter reference in this closure, which gets initialized
|
||||
// as part of the global logger, made it so that I couldn't destroy/leak/take/swap the last
|
||||
// reference to allow the channels to gracefully close.
|
||||
//
|
||||
// The workaround was to have a RwLock around the file and allow both the logger and the
|
||||
// file handler to both write independent of each other.
|
||||
let locked_file = get_cached_file_handle(&CONFIGURATION.output);
|
||||
|
||||
builder
|
||||
.format(move |_, record| {
|
||||
let t = start.elapsed().as_secs_f32();
|
||||
@@ -41,13 +55,18 @@ pub fn initialize(verbosity: u8) {
|
||||
};
|
||||
|
||||
let msg = format!(
|
||||
"{} {:10.03} {}",
|
||||
"{} {:10.03} {}\n",
|
||||
style(level_name).bg(level_color).black(),
|
||||
style(t).dim(),
|
||||
style(record.args()).dim(),
|
||||
);
|
||||
|
||||
PROGRESS_PRINTER.println(msg);
|
||||
PROGRESS_PRINTER.println(&msg);
|
||||
|
||||
if let Some(buffered_file) = locked_file.clone() {
|
||||
safe_file_write(&msg, buffered_file);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.init();
|
||||
|
||||
66
src/main.rs
66
src/main.rs
@@ -1,14 +1,16 @@
|
||||
use feroxbuster::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||
use feroxbuster::scanner::scan_url;
|
||||
use feroxbuster::utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer};
|
||||
use feroxbuster::{banner, heuristics, logger, FeroxResult};
|
||||
use feroxbuster::{banner, heuristics, logger, reporter, FeroxResult};
|
||||
use futures::StreamExt;
|
||||
use reqwest::Response;
|
||||
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::sync::mpsc::UnboundedSender;
|
||||
use tokio_util::codec::{FramedRead, LinesCodec};
|
||||
|
||||
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
|
||||
@@ -48,8 +50,12 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
|
||||
}
|
||||
|
||||
/// Determine whether it's a single url scan or urls are coming from stdin, then scan as needed
|
||||
async fn scan(targets: Vec<String>) -> FeroxResult<()> {
|
||||
log::trace!("enter: scan");
|
||||
async fn scan(
|
||||
targets: Vec<String>,
|
||||
tx_term: UnboundedSender<Response>,
|
||||
tx_file: UnboundedSender<String>,
|
||||
) -> FeroxResult<()> {
|
||||
log::trace!("enter: scan({:?}, {:?}, {:?})", targets, tx_term, tx_file);
|
||||
// cloning an Arc is cheap (it's basically a pointer into the heap)
|
||||
// so that will allow for cheap/safe sharing of a single wordlist across multi-target scans
|
||||
// as well as additional directories found as part of recursion
|
||||
@@ -70,11 +76,13 @@ async fn scan(targets: Vec<String>) -> FeroxResult<()> {
|
||||
let mut tasks = vec![];
|
||||
|
||||
for target in targets {
|
||||
let wordclone = words.clone();
|
||||
let word_clone = words.clone();
|
||||
let term_clone = tx_term.clone();
|
||||
let file_clone = tx_file.clone();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let base_depth = get_current_depth(&target);
|
||||
scan_url(&target, wordclone, base_depth).await;
|
||||
scan_url(&target, word_clone, base_depth, term_clone, file_clone).await;
|
||||
});
|
||||
|
||||
tasks.push(task);
|
||||
@@ -112,11 +120,18 @@ async fn get_targets() -> FeroxResult<Vec<String>> {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// setup logging based on the number of -v's used
|
||||
logger::initialize(CONFIGURATION.verbosity);
|
||||
|
||||
// can't trace main until after logger is initialized
|
||||
log::trace!("enter: main");
|
||||
log::debug!("{:#?}", *CONFIGURATION);
|
||||
|
||||
let save_output = !CONFIGURATION.output.is_empty(); // was -o used?
|
||||
|
||||
let (tx_term, tx_file, term_handle, file_handle) =
|
||||
reporter::initialize(&CONFIGURATION.output, save_output);
|
||||
|
||||
// get targets from command line or stdin
|
||||
let targets = match get_targets().await {
|
||||
Ok(t) => t,
|
||||
@@ -144,14 +159,49 @@ async fn main() {
|
||||
// discard non-responsive targets
|
||||
let live_targets = heuristics::connectivity_test(&targets).await;
|
||||
|
||||
match scan(live_targets).await {
|
||||
// kick off a scan against any targets determined to be responsive
|
||||
match scan(live_targets, tx_term.clone(), tx_file.clone()).await {
|
||||
Ok(_) => {
|
||||
log::info!("Done");
|
||||
log::info!("All scans complete!");
|
||||
}
|
||||
Err(e) => log::error!("An error occurred: {}", e),
|
||||
};
|
||||
|
||||
PROGRESS_PRINTER.finish();
|
||||
// manually drop tx in order for the rx task's while loops to eval to false
|
||||
drop(tx_term);
|
||||
log::trace!("dropped terminal output handler's transmitter");
|
||||
|
||||
log::trace!("awaiting terminal output handler's receiver");
|
||||
// after dropping tx, we can await the future where rx lived
|
||||
match term_handle.await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("error awaiting terminal output handler's receiver: {}", e);
|
||||
}
|
||||
}
|
||||
log::trace!("done awaiting terminal output handler's receiver");
|
||||
|
||||
log::trace!("tx_file: {:?}", tx_file);
|
||||
// the same drop/await process used on the terminal handler is repeated for the file handler
|
||||
// we drop the file transmitter every time, because it's created no matter what
|
||||
drop(tx_file);
|
||||
|
||||
log::trace!("dropped file output handler's transmitter");
|
||||
if save_output {
|
||||
// but we only await if -o was specified
|
||||
log::trace!("awaiting file output handler's receiver");
|
||||
match file_handle.unwrap().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("error awaiting file output handler's receiver: {}", e);
|
||||
}
|
||||
}
|
||||
log::trace!("done awaiting file output handler's receiver");
|
||||
}
|
||||
|
||||
log::trace!("exit: main");
|
||||
|
||||
// clean-up function for the MultiProgress bar; must be called last in order to still see
|
||||
// the final trace message above
|
||||
PROGRESS_PRINTER.finish();
|
||||
}
|
||||
|
||||
@@ -229,3 +229,15 @@ EXAMPLES:
|
||||
./feroxbuster -u http://127.1 -t 200
|
||||
"#)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
/// initalize parser, expect a clap::App returned
|
||||
fn parser_initialize_gives_defaults() {
|
||||
let app = initialize();
|
||||
assert_eq!(app.get_name(), "feroxbuster");
|
||||
}
|
||||
}
|
||||
|
||||
230
src/reporter.rs
Normal file
230
src/reporter.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||
use crate::utils::{ferox_print, status_colorizer};
|
||||
use crate::FeroxChannel;
|
||||
use console::strip_ansi_codes;
|
||||
use reqwest::Response;
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, Once, RwLock};
|
||||
use std::{fs, io};
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Singleton buffered file behind an Arc/RwLock; used for file writes from two locations:
|
||||
/// - [logger::initialize](../logger/fn.initialize.html) (specifically a closure on the global logger instance)
|
||||
/// - `reporter::spawn_file_handler`
|
||||
pub static mut LOCKED_FILE: Option<Arc<RwLock<io::BufWriter<fs::File>>>> = None;
|
||||
|
||||
/// An initializer Once variable used to create `LOCKED_FILE`
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
// Accessing a `static mut` is unsafe much of the time, but if we do so
|
||||
// in a synchronized fashion (e.g., write once or read all) then we're
|
||||
// good to go!
|
||||
//
|
||||
// This function will only call `open_file` once, and will
|
||||
// otherwise always return the value returned from the first invocation.
|
||||
pub fn get_cached_file_handle(filename: &str) -> Option<Arc<RwLock<io::BufWriter<fs::File>>>> {
|
||||
unsafe {
|
||||
INIT.call_once(|| {
|
||||
LOCKED_FILE = open_file(&filename);
|
||||
});
|
||||
LOCKED_FILE.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates all required output handlers (terminal, file) and returns
|
||||
/// the transmitter sides of each mpsc along with each receiver's future's JoinHandle to be awaited
|
||||
///
|
||||
/// Any other module that needs to write a Response to stdout or output results to a file should
|
||||
/// be passed a clone of the appropriate returned transmitter
|
||||
pub fn initialize(
|
||||
output_file: &str,
|
||||
save_output: bool,
|
||||
) -> (
|
||||
UnboundedSender<Response>,
|
||||
UnboundedSender<String>,
|
||||
JoinHandle<()>,
|
||||
Option<JoinHandle<()>>,
|
||||
) {
|
||||
log::trace!("enter: initialize({}, {})", output_file, save_output);
|
||||
|
||||
let (tx_rpt, rx_rpt): FeroxChannel<Response> = mpsc::unbounded_channel();
|
||||
let (tx_file, rx_file): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||
|
||||
let file_clone = tx_file.clone();
|
||||
|
||||
let term_reporter =
|
||||
tokio::spawn(async move { spawn_terminal_reporter(rx_rpt, file_clone, save_output).await });
|
||||
|
||||
let file_reporter = if save_output {
|
||||
// -o used, need to spawn the thread for writing to disk
|
||||
let file_clone = output_file.to_string();
|
||||
Some(tokio::spawn(async move {
|
||||
spawn_file_reporter(rx_file, &file_clone).await
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"exit: initialize -> ({:?}, {:?}, {:?}, {:?})",
|
||||
tx_rpt,
|
||||
tx_file,
|
||||
term_reporter,
|
||||
file_reporter
|
||||
);
|
||||
(tx_rpt, tx_file, term_reporter, file_reporter)
|
||||
}
|
||||
|
||||
/// Spawn a single consumer task (sc side of mpsc)
|
||||
///
|
||||
/// The consumer simply receives responses and prints them if they meet the given
|
||||
/// reporting criteria
|
||||
async fn spawn_terminal_reporter(
|
||||
mut resp_chan: UnboundedReceiver<Response>,
|
||||
file_chan: UnboundedSender<String>,
|
||||
save_output: bool,
|
||||
) {
|
||||
log::trace!(
|
||||
"enter: spawn_terminal_reporter({:?}, {:?}, {})",
|
||||
resp_chan,
|
||||
file_chan,
|
||||
save_output
|
||||
);
|
||||
|
||||
while let Some(resp) = resp_chan.recv().await {
|
||||
log::debug!("received {} on reporting channel", resp.url());
|
||||
|
||||
if CONFIGURATION.statuscodes.contains(&resp.status().as_u16()) {
|
||||
let report = if CONFIGURATION.quiet {
|
||||
// -q used, just need the url
|
||||
format!("{}\n", resp.url())
|
||||
} else {
|
||||
// normal printing with status and size
|
||||
let status = status_colorizer(&resp.status().as_str());
|
||||
format!(
|
||||
// example output
|
||||
// 200 3280 https://localhost.com/FAQ
|
||||
"{} {:>10} {}\n",
|
||||
status,
|
||||
resp.content_length().unwrap_or(0),
|
||||
resp.url()
|
||||
)
|
||||
};
|
||||
|
||||
// print to stdout
|
||||
ferox_print(&report, &PROGRESS_PRINTER);
|
||||
|
||||
if save_output {
|
||||
// -o used, need to send the report to be written out to disk
|
||||
match file_chan.send(report.to_string()) {
|
||||
Ok(_) => {
|
||||
log::debug!("Sent {} to file handler", resp.url());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Could not send {} to file handler: {}", resp.url(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("report complete: {}", resp.url());
|
||||
}
|
||||
log::trace!("exit: spawn_terminal_reporter");
|
||||
}
|
||||
|
||||
/// Spawn a single consumer task (sc side of mpsc)
|
||||
///
|
||||
/// The consumer simply receives responses and writes them to the given output file if they meet
|
||||
/// the given reporting criteria
|
||||
async fn spawn_file_reporter(mut report_channel: UnboundedReceiver<String>, output_file: &str) {
|
||||
let buffered_file = match get_cached_file_handle(&CONFIGURATION.output) {
|
||||
Some(file) => file,
|
||||
None => {
|
||||
log::trace!("exit: spawn_file_reporter");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"enter: spawn_file_reporter({:?}, {})",
|
||||
report_channel,
|
||||
output_file
|
||||
);
|
||||
|
||||
log::info!("Writing scan results to {}", output_file);
|
||||
|
||||
while let Some(report) = report_channel.recv().await {
|
||||
safe_file_write(&report, buffered_file.clone());
|
||||
}
|
||||
|
||||
log::trace!("exit: spawn_file_reporter");
|
||||
}
|
||||
|
||||
/// Given the path to a file, open the file in append mode (create it if it doesn't exist) and
|
||||
/// return a reference to the file that is buffered and locked
|
||||
fn open_file(filename: &str) -> Option<Arc<RwLock<io::BufWriter<fs::File>>>> {
|
||||
log::trace!("enter: open_file({})", filename);
|
||||
|
||||
match fs::OpenOptions::new() // std fs
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(filename)
|
||||
{
|
||||
Ok(file) => {
|
||||
let writer = io::BufWriter::new(file); // std io
|
||||
|
||||
let locked_file = Some(Arc::new(RwLock::new(writer)));
|
||||
|
||||
log::trace!("exit: open_file -> {:?}", locked_file);
|
||||
locked_file
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
log::trace!("exit: open_file -> None");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a string and a reference to a locked buffered file, write the contents and flush
|
||||
/// the buffer to disk.
|
||||
pub fn safe_file_write(contents: &str, locked_file: Arc<RwLock<io::BufWriter<fs::File>>>) {
|
||||
// note to future self: adding logging of anything other than error to this function
|
||||
// is a bad idea. we call this function while processing records generated by the logger.
|
||||
// If we then call log::... while already processing some logging output, it results in
|
||||
// the second log entry being injected into the first.
|
||||
|
||||
let contents = strip_ansi_codes(&contents);
|
||||
|
||||
if let Ok(mut handle) = locked_file.write() {
|
||||
// write lock acquired
|
||||
match handle.write(contents.as_bytes()) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("could not write report to disk: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match handle.flush() {
|
||||
// this function is used within async functions/loops, so i'm flushing so that in
|
||||
// the event of a ctrl+c or w/e results seen so far are saved instead of left lying
|
||||
// around in the buffer
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("error writing to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
/// asserts that an empty string for a filename returns None
|
||||
fn reporter_get_cached_file_handle_without_filename_returns_none() {
|
||||
let _used = get_cached_file_handle(&"").unwrap();
|
||||
}
|
||||
}
|
||||
351
src/scanner.rs
351
src/scanner.rs
@@ -1,122 +1,62 @@
|
||||
use crate::config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER};
|
||||
use crate::config::{CONFIGURATION, PROGRESS_BAR};
|
||||
use crate::heuristics::WildcardFilter;
|
||||
use crate::utils::{
|
||||
ferox_print, format_url, get_current_depth, get_url_path_length, make_request, status_colorizer,
|
||||
};
|
||||
use crate::{heuristics, progress};
|
||||
use crate::utils::{format_url, get_current_depth, get_url_path_length, make_request};
|
||||
use crate::{heuristics, progress, FeroxChannel};
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::{Response, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
use tokio::io::{self, AsyncWriteExt};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Single atomic number that gets incremented once, used to track first scan vs. all others
|
||||
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Spawn a single consumer task (sc side of mpsc)
|
||||
///
|
||||
/// The consumer simply receives responses and writes them to the given output file if they meet
|
||||
/// the given reporting criteria
|
||||
async fn spawn_file_reporter(mut report_channel: UnboundedReceiver<Response>) {
|
||||
log::trace!("enter: spawn_file_reporter({:?}", report_channel);
|
||||
|
||||
log::info!("Writing scan results to {}", CONFIGURATION.output);
|
||||
|
||||
match fs::OpenOptions::new() // tokio fs
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&CONFIGURATION.output)
|
||||
.await
|
||||
{
|
||||
Ok(outfile) => {
|
||||
log::debug!("{:?} opened in append mode", outfile);
|
||||
|
||||
let mut writer = io::BufWriter::new(outfile); // tokio BufWriter
|
||||
|
||||
while let Some(resp) = report_channel.recv().await {
|
||||
log::debug!("received {} on reporting channel", resp.url());
|
||||
|
||||
if CONFIGURATION.statuscodes.contains(&resp.status().as_u16()) {
|
||||
let report = if CONFIGURATION.quiet {
|
||||
format!("{}\n", resp.url())
|
||||
} else {
|
||||
// example output
|
||||
// 200 3280 https://localhost.com/FAQ
|
||||
format!(
|
||||
"{} {:>10} {}\n",
|
||||
resp.status().as_str(),
|
||||
resp.content_length().unwrap_or(0),
|
||||
resp.url()
|
||||
)
|
||||
};
|
||||
|
||||
match writer.write(report.as_bytes()).await {
|
||||
Ok(written) => {
|
||||
log::trace!("wrote {} bytes to {}", written, CONFIGURATION.output);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("could not write report to disk: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match writer.flush().await {
|
||||
// i'm flushing inside the while loop so in the event of a ctrl+c or w/e
|
||||
// results seen so far are saved instead of left lying around in the buffer
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("error writing to file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("report complete: {}", resp.url());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("error opening file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: spawn_file_reporter");
|
||||
lazy_static! {
|
||||
/// Set of urls that have been sent to [scan_url](fn.scan_url.html), used for deduplication
|
||||
static ref SCANNED_URLS: RwLock<HashSet<String>> = RwLock::new(HashSet::new());
|
||||
}
|
||||
|
||||
/// Spawn a single consumer task (sc side of mpsc)
|
||||
/// Adds the given url to `SCANNED_URLS`
|
||||
///
|
||||
/// The consumer simply receives responses and prints them if they meet the given
|
||||
/// reporting criteria
|
||||
async fn spawn_terminal_reporter(mut report_channel: UnboundedReceiver<Response>) {
|
||||
log::trace!("enter: spawn_terminal_reporter({:?})", report_channel);
|
||||
/// If `SCANNED_URLS` did not already contain the url, return true; otherwise return false
|
||||
fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock<HashSet<String>>) -> bool {
|
||||
log::trace!(
|
||||
"enter: add_url_to_list_of_scanned_urls({}, {:?})",
|
||||
resp,
|
||||
scanned_urls
|
||||
);
|
||||
|
||||
while let Some(resp) = report_channel.recv().await {
|
||||
log::debug!("received {} on reporting channel", resp.url());
|
||||
|
||||
if CONFIGURATION.statuscodes.contains(&resp.status().as_u16()) {
|
||||
if CONFIGURATION.quiet {
|
||||
ferox_print(&format!("{}", resp.url()), &PROGRESS_PRINTER);
|
||||
match scanned_urls.write() {
|
||||
// check new url against what's already been scanned
|
||||
Ok(mut urls) => {
|
||||
let normalized_url = if resp.ends_with('/') {
|
||||
// append a / to the list of 'seen' urls, this is to prevent the case where
|
||||
// 3xx and 2xx duplicate eachother
|
||||
resp.to_string()
|
||||
} else {
|
||||
let status = status_colorizer(&resp.status().as_str());
|
||||
ferox_print(
|
||||
&format!(
|
||||
// example output
|
||||
// 200 3280 https://localhost.com/FAQ
|
||||
"{} {:>10} {}",
|
||||
status,
|
||||
resp.content_length().unwrap_or(0),
|
||||
resp.url()
|
||||
),
|
||||
&PROGRESS_PRINTER,
|
||||
);
|
||||
}
|
||||
format!("{}/", resp)
|
||||
};
|
||||
|
||||
// If the set did not contain resp, true is returned.
|
||||
// If the set did contain resp, false is returned.
|
||||
let response = urls.insert(normalized_url);
|
||||
|
||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> {}", response);
|
||||
response
|
||||
}
|
||||
Err(e) => {
|
||||
// poisoned lock
|
||||
log::error!("Set of scanned urls poisoned: {}", e);
|
||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> false");
|
||||
false
|
||||
}
|
||||
log::debug!("report complete: {}", resp.url());
|
||||
}
|
||||
log::trace!("exit: spawn_terminal_reporter");
|
||||
}
|
||||
|
||||
/// Spawn a single consumer task (sc side of mpsc)
|
||||
@@ -126,22 +66,44 @@ fn spawn_recursion_handler(
|
||||
mut recursion_channel: UnboundedReceiver<String>,
|
||||
wordlist: Arc<HashSet<String>>,
|
||||
base_depth: usize,
|
||||
tx_term: UnboundedSender<Response>,
|
||||
tx_file: UnboundedSender<String>,
|
||||
) -> BoxFuture<'static, Vec<JoinHandle<()>>> {
|
||||
log::trace!(
|
||||
"enter: spawn_recursion_handler({:?}, wordlist[{} words...], {})",
|
||||
"enter: spawn_recursion_handler({:?}, wordlist[{} words...], {}, {:?}, {:?})",
|
||||
recursion_channel,
|
||||
wordlist.len(),
|
||||
base_depth
|
||||
base_depth,
|
||||
tx_term,
|
||||
tx_file
|
||||
);
|
||||
|
||||
let boxed_future = async move {
|
||||
let mut scans = vec![];
|
||||
while let Some(resp) = recursion_channel.recv().await {
|
||||
let unknown = add_url_to_list_of_scanned_urls(&resp, &SCANNED_URLS);
|
||||
|
||||
if !unknown {
|
||||
// not unknown, i.e. we've seen the url before and don't need to scan again
|
||||
continue;
|
||||
}
|
||||
|
||||
log::info!("received {} on recursion channel", resp);
|
||||
let clonedresp = resp.clone();
|
||||
let clonedlist = wordlist.clone();
|
||||
|
||||
let term_clone = tx_term.clone();
|
||||
let file_clone = tx_file.clone();
|
||||
let resp_clone = resp.clone();
|
||||
let list_clone = wordlist.clone();
|
||||
|
||||
scans.push(tokio::spawn(async move {
|
||||
scan_url(clonedresp.to_owned().as_str(), clonedlist, base_depth).await
|
||||
scan_url(
|
||||
resp_clone.to_owned().as_str(),
|
||||
list_clone,
|
||||
base_depth,
|
||||
term_clone,
|
||||
file_clone,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
}
|
||||
scans
|
||||
@@ -250,10 +212,15 @@ fn response_is_directory(response: &Response) -> bool {
|
||||
///
|
||||
/// Essentially looks at the Url path and determines how many directories are present in the
|
||||
/// given Url
|
||||
fn reached_max_depth(url: &Url, base_depth: usize) -> bool {
|
||||
log::trace!("enter: reached_max_depth({}, {})", url, base_depth);
|
||||
fn reached_max_depth(url: &Url, base_depth: usize, max_depth: usize) -> bool {
|
||||
log::trace!(
|
||||
"enter: reached_max_depth({}, {}, {})",
|
||||
url,
|
||||
base_depth,
|
||||
max_depth
|
||||
);
|
||||
|
||||
if CONFIGURATION.depth == 0 {
|
||||
if max_depth == 0 {
|
||||
// early return, as 0 means recurse forever; no additional processing needed
|
||||
log::trace!("exit: reached_max_depth -> false");
|
||||
return false;
|
||||
@@ -261,7 +228,7 @@ fn reached_max_depth(url: &Url, base_depth: usize) -> bool {
|
||||
|
||||
let depth = get_current_depth(url.as_str());
|
||||
|
||||
if depth - base_depth >= CONFIGURATION.depth {
|
||||
if depth - base_depth >= max_depth {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -284,7 +251,9 @@ async fn try_recursion(
|
||||
transmitter
|
||||
);
|
||||
|
||||
if !reached_max_depth(response.url(), base_depth) && response_is_directory(&response) {
|
||||
if !reached_max_depth(response.url(), base_depth, CONFIGURATION.depth)
|
||||
&& response_is_directory(&response)
|
||||
{
|
||||
if CONFIGURATION.redirects {
|
||||
// response is 2xx can simply send it because we're following redirects
|
||||
log::info!("Added new directory to recursive scan: {}", response.url());
|
||||
@@ -295,9 +264,8 @@ async fn try_recursion(
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"could not send {} across {:?}: {}",
|
||||
"Could not send {} to recursion handler: {}",
|
||||
response.url(),
|
||||
transmitter,
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -311,9 +279,8 @@ async fn try_recursion(
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"could not send {}/ across {:?}: {}",
|
||||
"Could not send {}/ to recursion handler: {}",
|
||||
response.url(),
|
||||
transmitter,
|
||||
e
|
||||
);
|
||||
}
|
||||
@@ -407,21 +374,25 @@ async fn make_requests(
|
||||
/// Scan a given url using a given wordlist
|
||||
///
|
||||
/// This is the primary entrypoint for the scanner
|
||||
pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_depth: usize) {
|
||||
pub async fn scan_url(
|
||||
target_url: &str,
|
||||
wordlist: Arc<HashSet<String>>,
|
||||
base_depth: usize,
|
||||
tx_term: UnboundedSender<Response>,
|
||||
tx_file: UnboundedSender<String>,
|
||||
) {
|
||||
log::trace!(
|
||||
"enter: scan_url({:?}, wordlist[{} words...], {})",
|
||||
"enter: scan_url({:?}, wordlist[{} words...], {}, {:?}, {:?})",
|
||||
target_url,
|
||||
wordlist.len(),
|
||||
base_depth
|
||||
base_depth,
|
||||
tx_term,
|
||||
tx_file
|
||||
);
|
||||
|
||||
log::info!("Starting scan against: {}", target_url);
|
||||
|
||||
let (tx_rpt, rx_rpt): (UnboundedSender<Response>, UnboundedReceiver<Response>) =
|
||||
mpsc::unbounded_channel();
|
||||
|
||||
let (tx_dir, rx_dir): (UnboundedSender<String>, UnboundedReceiver<String>) =
|
||||
mpsc::unbounded_channel();
|
||||
let (tx_dir, rx_dir): FeroxChannel<String> = mpsc::unbounded_channel();
|
||||
|
||||
let num_reqs_expected: u64 = if CONFIGURATION.extensions.is_empty() {
|
||||
wordlist.len().try_into().unwrap()
|
||||
@@ -437,44 +408,43 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
||||
// join can only be called once, otherwise it causes the thread to panic
|
||||
tokio::task::spawn_blocking(move || PROGRESS_BAR.join().unwrap());
|
||||
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// this protection around join also allows us to add the first scanned url to SCANNED_URLS
|
||||
// from within the scan_url function instead of the recursion handler
|
||||
add_url_to_list_of_scanned_urls(&target_url, &SCANNED_URLS);
|
||||
}
|
||||
|
||||
// Arc clones to be passed around to the various scans
|
||||
let wildcard_bar = progress_bar.clone();
|
||||
|
||||
let reporter = if !CONFIGURATION.output.is_empty() {
|
||||
// output file defined
|
||||
tokio::spawn(async move { spawn_file_reporter(rx_rpt).await })
|
||||
} else {
|
||||
tokio::spawn(async move { spawn_terminal_reporter(rx_rpt).await })
|
||||
};
|
||||
|
||||
// lifetime satisfiers, as it's an Arc, clones are cheap anyway
|
||||
let looping_words = wordlist.clone();
|
||||
let heuristics_file_clone = tx_file.clone();
|
||||
let recurser_term_clone = tx_term.clone();
|
||||
let recurser_file_clone = tx_file.clone();
|
||||
let recurser_words = wordlist.clone();
|
||||
let looping_words = wordlist.clone();
|
||||
|
||||
let recurser =
|
||||
tokio::spawn(
|
||||
async move { spawn_recursion_handler(rx_dir, recurser_words, base_depth).await },
|
||||
);
|
||||
let recurser = tokio::spawn(async move {
|
||||
spawn_recursion_handler(
|
||||
rx_dir,
|
||||
recurser_words,
|
||||
base_depth,
|
||||
recurser_term_clone,
|
||||
recurser_file_clone,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
let filter = match heuristics::wildcard_test(&target_url, wildcard_bar).await {
|
||||
Some(f) => {
|
||||
if CONFIGURATION.dontfilter {
|
||||
// don't auto filter, i.e. use the defaults
|
||||
Arc::new(WildcardFilter::default())
|
||||
} else {
|
||||
Arc::new(f)
|
||||
}
|
||||
}
|
||||
None => Arc::new(WildcardFilter::default()),
|
||||
};
|
||||
let filter =
|
||||
match heuristics::wildcard_test(&target_url, wildcard_bar, heuristics_file_clone).await {
|
||||
Some(f) => Arc::new(f),
|
||||
None => Arc::new(WildcardFilter::default()),
|
||||
};
|
||||
|
||||
// producer tasks (mp of mpsc); responsible for making requests
|
||||
let producers = stream::iter(looping_words.deref().to_owned())
|
||||
.map(|word| {
|
||||
let wc_filter = filter.clone();
|
||||
let txd = tx_dir.clone();
|
||||
let txr = tx_rpt.clone();
|
||||
let txr = tx_term.clone();
|
||||
let pb = progress_bar.clone(); // progress bar is an Arc around internal state
|
||||
let tgt = target_url.to_string(); // done to satisfy 'static lifetime below
|
||||
(
|
||||
@@ -511,18 +481,6 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
||||
futures::future::join_all(recurser.await.unwrap()).await;
|
||||
log::trace!("done awaiting recursive scan receiver/scans");
|
||||
|
||||
// same thing here, drop report tx so the rx can finish up
|
||||
log::trace!("dropped report handler's transmitter");
|
||||
drop(tx_rpt);
|
||||
|
||||
log::trace!("awaiting report receiver");
|
||||
match reporter.await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::error!("error awaiting report receiver: {}", e);
|
||||
}
|
||||
}
|
||||
log::trace!("done awaiting report receiver");
|
||||
log::trace!("exit: scan_url");
|
||||
}
|
||||
|
||||
@@ -583,4 +541,79 @@ mod tests {
|
||||
assert_eq!(urls, expected[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// call reached_max_depth with max depth of zero, which is infinite recursion, expect false
|
||||
fn reached_max_depth_returns_early_on_zero() {
|
||||
let url = Url::parse("http://localhost").unwrap();
|
||||
let result = reached_max_depth(&url, 0, 0);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// call reached_max_depth with url depth equal to max depth, expect true
|
||||
fn reached_max_depth_current_depth_equals_max() {
|
||||
let url = Url::parse("http://localhost/one/two").unwrap();
|
||||
let result = reached_max_depth(&url, 0, 2);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// call reached_max_depth with url dpeth less than max depth, expect false
|
||||
fn reached_max_depth_current_depth_less_than_max() {
|
||||
let url = Url::parse("http://localhost").unwrap();
|
||||
let result = reached_max_depth(&url, 0, 2);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// call reached_max_depth with url of 2, base depth of 2, and max depth of 2, expect false
|
||||
fn reached_max_depth_base_depth_equals_max_depth() {
|
||||
let url = Url::parse("http://localhost/one/two").unwrap();
|
||||
let result = reached_max_depth(&url, 2, 2);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// call reached_max_depth with url depth greater than max depth, expect true
|
||||
fn reached_max_depth_current_greater_than_max() {
|
||||
let url = Url::parse("http://localhost/one/two/three").unwrap();
|
||||
let result = reached_max_depth(&url, 0, 2);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add an unknown url to the hashset, expect true
|
||||
fn add_url_to_list_of_scanned_urls_with_unknown_url() {
|
||||
let urls = RwLock::new(HashSet::<String>::new());
|
||||
let url = "http://unknown_url";
|
||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a known url to the hashset, with a trailing slash, expect false
|
||||
fn add_url_to_list_of_scanned_urls_with_known_url() {
|
||||
let urls = RwLock::new(HashSet::<String>::new());
|
||||
let url = "http://unknown_url/";
|
||||
|
||||
assert_eq!(urls.write().unwrap().insert(url.to_string()), true);
|
||||
|
||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a known url to the hashset, without a trailing slash, expect false
|
||||
fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
|
||||
let urls = RwLock::new(HashSet::<String>::new());
|
||||
let url = "http://unknown_url";
|
||||
|
||||
assert_eq!(
|
||||
urls.write()
|
||||
.unwrap()
|
||||
.insert("http://unknown_url/".to_string()),
|
||||
true
|
||||
);
|
||||
|
||||
assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ fn banner_prints_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||
String::from("http://localhost"),
|
||||
String::from("http://schmocalhost"),
|
||||
];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls)?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
|
||||
27
tests/test_config.rs
Normal file
27
tests/test_config.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
mod utils;
|
||||
use assert_cmd::prelude::*;
|
||||
use predicates::prelude::*;
|
||||
use std::process::Command;
|
||||
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
|
||||
#[test]
|
||||
/// send a single valid request, expect a 200 response
|
||||
fn read_in_config_file_for_settings() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["threads = 37".to_string()], "ferox-config.toml")?;
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.current_dir(&tmp_dir)
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("│ 37"));
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
/// test passes one bad target via -u to the scanner, expected result is that the
|
||||
/// scanner dies
|
||||
fn test_single_target_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
@@ -37,7 +37,7 @@ fn test_two_targets_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let not_real =
|
||||
String::from("http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk");
|
||||
let urls = vec![not_real.clone(), not_real];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls)?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
@@ -67,7 +67,7 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
|
||||
let not_real =
|
||||
String::from("http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk");
|
||||
let urls = vec![not_real, srv.url("/"), String::from("LICENSE")];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls)?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
@@ -100,7 +100,7 @@ fn test_one_good_and_one_bad_target_scan_succeeds() -> Result<(), Box<dyn std::e
|
||||
/// test finds a static wildcard and reports as much to stdout
|
||||
fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
@@ -132,10 +132,11 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a dynamic wildcard and reports as much to stdout
|
||||
/// test finds a dynamic wildcard and reports as much to stdout and a file
|
||||
fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
@@ -158,10 +159,26 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(contents.contains("WLD"), true);
|
||||
assert_eq!(contents.contains("Got"), true);
|
||||
assert_eq!(contents.contains("200"), true);
|
||||
assert_eq!(contents.contains("auto-filtering"), true);
|
||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
||||
assert_eq!(contents.contains("Wildcard response is dynamic"), true);
|
||||
assert_eq!(
|
||||
contents.contains("(14 + url length) responses; toggle this behavior by using"),
|
||||
true
|
||||
);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
@@ -179,3 +196,242 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error
|
||||
assert_eq!(mock2.times_called(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// uses dontfilter, so the normal wildcard test should never happen
|
||||
fn heuristics_static_wildcard_request_with_dontfilter() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--dontfilter")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(mock.times_called(), 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports as much to stdout
|
||||
fn heuristics_wildcard_test_with_two_static_wildcards() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock2 = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("(url length: 96)"))
|
||||
.and(predicate::str::contains(
|
||||
"Wildcard response is static; auto-filtering 46",
|
||||
)),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock2.times_called(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports nothing to stdout
|
||||
fn heuristics_wildcard_test_with_two_static_wildcards_with_quiet_enabled(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock2 = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("-q")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert().success().stdout(predicate::str::is_empty());
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock2.times_called(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports as much to stdout and a file
|
||||
fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock2 = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
|
||||
.return_status(200)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(contents.contains("WLD"), true);
|
||||
assert_eq!(contents.contains("Got"), true);
|
||||
assert_eq!(contents.contains("200"), true);
|
||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||
assert_eq!(contents.contains("(url length: 96)"), true);
|
||||
assert_eq!(
|
||||
contents.contains("Wildcard response is static; auto-filtering 46"),
|
||||
true
|
||||
);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("(url length: 96)"))
|
||||
.and(predicate::str::contains(
|
||||
"Wildcard response is static; auto-filtering 46",
|
||||
)),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock2.times_called(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
|
||||
/// in the output file
|
||||
fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap())
|
||||
.return_status(301)
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock2 = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap())
|
||||
.return_status(301)
|
||||
.return_header("Location", &srv.url("/some-redirect"))
|
||||
.return_body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert_eq!(contents.contains("WLD"), true);
|
||||
assert_eq!(contents.contains("301"), true);
|
||||
assert_eq!(contents.contains("/some-redirect"), true);
|
||||
assert_eq!(contents.contains("redirects to => "), true);
|
||||
assert_eq!(contents.contains(&srv.url("/")), true);
|
||||
assert_eq!(contents.contains("(url length: 32)"), true);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("redirects to => ")
|
||||
.and(predicate::str::contains("/some-redirect"))
|
||||
.and(predicate::str::contains("301"))
|
||||
.and(predicate::str::contains(srv.url("/")))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("WLD")),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock2.times_called(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ fn main_use_root_owned_file_as_wordlist() -> Result<(), Box<dyn std::error::Erro
|
||||
/// send the function an empty file
|
||||
fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&[])?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&[], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
@@ -70,7 +70,7 @@ fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[test]
|
||||
/// send nothing over stdin, expect heuristics to be upset during connectivity test
|
||||
fn main_use_empty_stdin_targets() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (tmp_dir, file) = setup_tmp_directory(&[])?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&[], "wordlist")?;
|
||||
|
||||
// get_targets is called before scan, so the empty wordlist shouldn't trigger
|
||||
// the 'Did not find any words' error
|
||||
|
||||
@@ -8,9 +8,9 @@ use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
|
||||
#[test]
|
||||
/// send a single valid request, expect a 200 response
|
||||
fn test_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn scanner_single_request_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()])?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
@@ -49,7 +49,7 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"dev".to_string(),
|
||||
"file.js".to_string(),
|
||||
];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls)?;
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
|
||||
|
||||
let js_mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
@@ -107,3 +107,306 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a valid request, follow 200s into new directories, expect 200 responses
|
||||
fn scanner_recursive_request_scan_using_only_success_responses(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let urls = [
|
||||
"js/".to_string(),
|
||||
"prod/".to_string(),
|
||||
"dev/".to_string(),
|
||||
"file.js".to_string(),
|
||||
];
|
||||
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
|
||||
|
||||
let js_mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/js/")
|
||||
.return_status(200)
|
||||
.return_header("Location", &srv.url("/js/"))
|
||||
.create_on(&srv);
|
||||
|
||||
let js_prod_mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/js/prod/")
|
||||
.return_status(200)
|
||||
.return_header("Location", &srv.url("/js/prod/"))
|
||||
.create_on(&srv);
|
||||
|
||||
let js_dev_mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/js/dev/")
|
||||
.return_status(200)
|
||||
.return_header("Location", &srv.url("/js/dev/"))
|
||||
.create_on(&srv);
|
||||
|
||||
let js_dev_file_mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/js/dev/file.js")
|
||||
.return_status(200)
|
||||
.return_body("this is a test and is more bytes than other ones")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.arg("-t")
|
||||
.arg("1")
|
||||
.arg("--redirects")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::is_match("200.*js")
|
||||
.unwrap()
|
||||
.and(predicate::str::is_match("200.*js/prod").unwrap())
|
||||
.and(predicate::str::is_match("200.*js/dev").unwrap())
|
||||
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap()),
|
||||
);
|
||||
|
||||
assert_eq!(js_mock.times_called(), 1);
|
||||
assert_eq!(js_prod_mock.times_called(), 1);
|
||||
assert_eq!(js_dev_mock.times_called(), 1);
|
||||
assert_eq!(js_dev_file_mock.times_called(), 1);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a single valid request, get a response, and write it to disk
|
||||
fn scanner_single_request_scan_with_file_output() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let outfile = tmp_dir.path().join("output");
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.arg("-o")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile)?;
|
||||
|
||||
assert!(contents.contains("/LICENSE"));
|
||||
assert!(contents.contains("200"));
|
||||
assert!(contents.contains("14"));
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a single valid request with -q, get a response, and write only the url to disk
|
||||
fn scanner_single_request_scan_with_file_output_and_tack_q(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let outfile = tmp_dir.path().join("output");
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.arg("-q")
|
||||
.arg("-o")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile)?;
|
||||
|
||||
let url = srv.url("/LICENSE");
|
||||
assert!(contents.contains(&url));
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send an invalid output file, expect nothing to be written to disk
|
||||
fn scanner_single_request_scan_with_invalid_file_output() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let outfile = tmp_dir.path(); // outfile is a directory
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.arg("-q")
|
||||
.arg("-o")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile);
|
||||
assert!(contents.is_err());
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a single valid request using -q, expect only the url on stdout
|
||||
fn scanner_single_request_quiet_scan() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-x")
|
||||
.arg("js,html")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains(srv.url("/LICENSE"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.not()
|
||||
.and(predicate::str::contains("14"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send single valid request, get back a 301 without a Location header, expect false
|
||||
fn scanner_single_request_returns_301_without_location_header(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(301)
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-T")
|
||||
.arg("5")
|
||||
.arg("-a")
|
||||
.arg("some-user-agent-string")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains(srv.url("/LICENSE"))
|
||||
.and(predicate::str::contains("301"))
|
||||
.and(predicate::str::contains("14"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a single valid request, filter the size of the response, expect one out of 2 urls
|
||||
fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["LICENSE".to_string(), "ignored".to_string()], "wordlist")?;
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(200)
|
||||
.return_body("this is a not a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let filtered_mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/ignored")
|
||||
.return_status(200)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-n")
|
||||
.arg("-S")
|
||||
.arg("14")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("20"))
|
||||
.and(predicate::str::contains("ignored"))
|
||||
.not()
|
||||
.and(predicate::str::contains("14"))
|
||||
.not(),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(filtered_mock.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// integration test helper: creates a temp directory, and writes `words` to
|
||||
/// a file named `wordlist` in the temp directory
|
||||
/// a file named `filename` in the temp directory
|
||||
pub fn setup_tmp_directory(
|
||||
words: &[String],
|
||||
filename: &str,
|
||||
) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let file = tmp_dir.path().join("wordlist");
|
||||
let file = tmp_dir.path().join(&filename);
|
||||
write(&file, words.join("\n"))?;
|
||||
Ok((tmp_dir, file))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user