mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-24 14:01:12 -03:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
393e775285 | ||
|
|
cf6c02307c | ||
|
|
88b9bc3a01 | ||
|
|
d1f90efb09 | ||
|
|
df4fad07a9 | ||
|
|
56d533117e | ||
|
|
9549e27f19 | ||
|
|
1677b51c2d | ||
|
|
d4f9442d38 | ||
|
|
8191fa1a5e | ||
|
|
4811b37aa4 | ||
|
|
941cad5844 | ||
|
|
d59af94f62 | ||
|
|
cf403c4d4a | ||
|
|
57a2b1cbab | ||
|
|
ef195bd653 | ||
|
|
9b1a24bca3 | ||
|
|
c6aefbfa97 | ||
|
|
42bad85208 | ||
|
|
f5709739fa | ||
|
|
248f56ed7a | ||
|
|
3de6ed9696 | ||
|
|
4bad39f4b9 | ||
|
|
9b303d8b5a | ||
|
|
7e0b003216 | ||
|
|
dc36a7bf4d | ||
|
|
d33632c421 | ||
|
|
7dc6a867a5 | ||
|
|
b937a0191e | ||
|
|
d57a83956c | ||
|
|
71efd78f03 | ||
|
|
139006d0a7 | ||
|
|
b5abb8b6e8 | ||
|
|
a076a333df | ||
|
|
461ed0a9ff | ||
|
|
4381569a0f | ||
|
|
a52bd10340 | ||
|
|
56a1144865 | ||
|
|
23ab009c08 | ||
|
|
fa4e3d5d88 | ||
|
|
ad7a1ffe44 | ||
|
|
0e4f8893f8 | ||
|
|
8e0b801ec5 | ||
|
|
97889f917d | ||
|
|
cedb3ccc8d | ||
|
|
d7cfd8ff60 | ||
|
|
223e75923d | ||
|
|
dd9f2f72c0 | ||
|
|
8ffea2500d | ||
|
|
5ed890e3fd | ||
|
|
8fe458263d | ||
|
|
6de36585a9 | ||
|
|
30538c366c | ||
|
|
89a0ac8aa4 | ||
|
|
c9a93f2843 | ||
|
|
bfdb4abdce | ||
|
|
eb17eeecd3 | ||
|
|
c2819ef2e7 | ||
|
|
030b588448 | ||
|
|
4ee143968e | ||
|
|
834d681bb9 | ||
|
|
fc35bb6764 | ||
|
|
8e2b08ce90 | ||
|
|
24a44ff253 | ||
|
|
0345e03e6a | ||
|
|
873539ac92 | ||
|
|
9c85f90faf | ||
|
|
1643643e77 | ||
|
|
a7e4cc914b | ||
|
|
6daa2a230a | ||
|
|
5486e3c95f | ||
|
|
204aa5e226 |
5
.cargo/config
Normal file
5
.cargo/config
Normal file
@@ -0,0 +1,5 @@
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [epi052]
|
||||
ko_fi: epi052
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -7,7 +7,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
|
||||
- [ ] Your PR description references the associated issue (i.e. fixes #123456)
|
||||
- [ ] Code is in its own branch
|
||||
- [ ] Branch name is related to the PR contents
|
||||
- [ ] PR targets master
|
||||
- [ ] PR targets main
|
||||
|
||||
## Static analysis checks
|
||||
- [ ] All rust files are formatted using `cargo fmt`
|
||||
|
||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
strategy:
|
||||
matrix:
|
||||
type: [ubuntu-x64, ubuntu-x86]
|
||||
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
|
||||
include:
|
||||
- type: ubuntu-x64
|
||||
os: ubuntu-latest
|
||||
@@ -22,12 +22,24 @@ jobs:
|
||||
name: x86-linux-feroxbuster
|
||||
path: target/i686-unknown-linux-musl/release/feroxbuster
|
||||
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
|
||||
- type: armv7
|
||||
os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
name: armv7-feroxbuster
|
||||
path: target/armv7-unknown-linux-gnueabihf/release/feroxbuster
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
- type: aarch64
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
name: aarch64-feroxbuster
|
||||
path: target/aarch64-unknown-linux-gnu/release/feroxbuster
|
||||
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install System Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config
|
||||
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
@@ -43,7 +55,7 @@ jobs:
|
||||
args: --release --target=${{ matrix.target }}
|
||||
- name: Strip symbols from binary
|
||||
run: |
|
||||
strip -s ${{ matrix.path }}
|
||||
strip -s ${{ matrix.path }} || arm-linux-gnueabihf-strip -s ${{ matrix.path }} || aarch64-linux-gnu-strip -s ${{ matrix.path }}
|
||||
- name: Build tar.gz for homebrew installs
|
||||
if: matrix.type == 'ubuntu-x64'
|
||||
run: |
|
||||
|
||||
572
Cargo.lock
generated
572
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "2.1.0"
|
||||
version = "2.2.4"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
@@ -21,9 +21,9 @@ regex = "1"
|
||||
lazy_static = "1.4"
|
||||
|
||||
[dependencies]
|
||||
futures = { version = "0.3"}
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio-util = {version = "0.6.3", features = ["codec"]}
|
||||
futures = { version = "0.3.14"}
|
||||
tokio = { version = "1.5.0", features = ["full"] }
|
||||
tokio-util = {version = "0.6.6", features = ["codec"]}
|
||||
log = "0.4"
|
||||
env_logger = "0.8.3"
|
||||
reqwest = { version = "0.11", features = ["socks"] }
|
||||
@@ -31,7 +31,7 @@ clap = "2.33"
|
||||
lazy_static = "1.4"
|
||||
toml = "0.5"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
serde_json = "1.0.64"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
indicatif = "0.15"
|
||||
console = "0.14"
|
||||
@@ -39,17 +39,17 @@ openssl = { version = "0.10", features = ["vendored"] }
|
||||
dirs = "3.0"
|
||||
regex = "1"
|
||||
crossterm = "0.19"
|
||||
rlimit = "0.5"
|
||||
ctrlc = "3.1"
|
||||
rlimit = "0.5.4"
|
||||
ctrlc = "3.1.9"
|
||||
fuzzyhash = "0.2.1"
|
||||
anyhow = "1.0"
|
||||
leaky-bucket = "0.10.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1"
|
||||
httpmock = "0.5.2"
|
||||
httpmock = "0.5.8"
|
||||
assert_cmd = "1.0.3"
|
||||
predicates = "1.0.7"
|
||||
predicates = "1.0.8"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
@@ -63,4 +63,7 @@ conf-files = ["/etc/feroxbuster/ferox-config.toml"]
|
||||
assets = [
|
||||
["target/release/feroxbuster", "/usr/bin/", "755"],
|
||||
["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"],
|
||||
["shell_completions/feroxbuster.bash", "/usr/share/bash-completion/completions/feroxbuster.bash", "644"],
|
||||
["shell_completions/feroxbuster.fish", "/usr/share/fish/completions/feroxbuster.fish", "644"],
|
||||
["shell_completions/_feroxbuster", "/usr/share/zsh/vendor-completions/_feroxbuster", "644"],
|
||||
]
|
||||
|
||||
47
Makefile
47
Makefile
@@ -6,12 +6,16 @@ datarootdir = $(prefix)/share
|
||||
datadir = $(datarootdir)
|
||||
example_config = ferox-config.toml.example
|
||||
config_file = ferox-config.toml
|
||||
completion_dir = shell_completions
|
||||
completion_prefix = $(completion_dir)/$(BIN)
|
||||
|
||||
BIN=feroxbuster
|
||||
SHR_SOURCES = $(shell find src -type f -wholename '*src/*.rs') Cargo.toml Cargo.lock
|
||||
|
||||
RELEASE = debug
|
||||
DEBUG ?= 0
|
||||
ifeq (0,$(DEBUG))
|
||||
|
||||
ifeq (0, $(DEBUG))
|
||||
ARGS = --release
|
||||
RELEASE = release
|
||||
endif
|
||||
@@ -23,54 +27,47 @@ endif
|
||||
|
||||
TARGET = target/$(RELEASE)
|
||||
|
||||
.PHONY: all clean distclean install uninstall update
|
||||
|
||||
BIN=feroxbuster
|
||||
DESKTOP=$(APPID).desktop
|
||||
.PHONY: all clean install uninstall
|
||||
|
||||
all: cli
|
||||
|
||||
cli: $(TARGET)/$(BIN) $(TARGET)/$(BIN).1.gz $(SHR_SOURCES)
|
||||
install: all install-cli
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
distclean: clean
|
||||
rm -rf .cargo vendor Cargo.lock vendor.tar
|
||||
|
||||
vendor: vendor.tar
|
||||
|
||||
vendor.tar:
|
||||
mkdir -p .cargo
|
||||
cargo vendor | head -n -1 > .cargo/config
|
||||
echo 'directory = "vendor"' >> .cargo/config
|
||||
cargo vendor
|
||||
tar pcf vendor.tar vendor
|
||||
rm -rf vendor
|
||||
|
||||
install-cli: cli
|
||||
install -Dm 0755 "$(TARGET)/$(BIN)" "$(DESTDIR)$(bindir)/$(BIN)"
|
||||
install -Dm 0644 "$(completion_prefix).bash" "$(DESTDIR)/usr/share/bash-completion/completions/$(BIN).bash"
|
||||
install -Dm 0644 "$(completion_prefix).fish" "$(DESTDIR)/usr/share/fish/completions/$(BIN).fish"
|
||||
install -Dm 0644 "$(completion_dir)/_$(BIN)" "$(DESTDIR)/usr/share/zsh/vendor-completions/_$(BIN)"
|
||||
install -sDm 0755 "$(TARGET)/$(BIN)" "$(DESTDIR)$(bindir)/$(BIN)"
|
||||
install -Dm 0644 "$(TARGET)/$(BIN).1.gz" "$(DESTDIR)$(datadir)/man/man1/$(BIN).1.gz"
|
||||
install -Dm 0644 "$(example_config)" "/etc/$(BIN)/$(config_File)"
|
||||
install -Dm 0644 "$(example_config)" "$(DESTDIR)/etc/$(BIN)/$(config_file)"
|
||||
|
||||
install: all install-cli
|
||||
|
||||
uninstall-cli:
|
||||
uninstall:
|
||||
rm -f "$(DESTDIR)$(bindir)/$(BIN)"
|
||||
rm -f "$(DESTDIR)$(datadir)/man/man1/$(BIN).1.gz"
|
||||
rm -rf "/etc/$(BIN)/"
|
||||
|
||||
uninstall: uninstall-cli
|
||||
|
||||
update:
|
||||
cargo update
|
||||
rm -rf "$(DESTDIR)/etc/$(BIN)/"
|
||||
rm -f "$(DESTDIR)/usr/share/bash-completion/completions/$(BIN).bash"
|
||||
rm -f "$(DESTDIR)/usr/share/zsh/vendor-completions/_$(BIN)"
|
||||
rm -f "$(DESTDIR)/usr/share/fish/completions/$(BIN).fish"
|
||||
|
||||
extract:
|
||||
ifeq ($(VENDORED),1)
|
||||
ifeq (1, $(VENDORED))
|
||||
tar pxf vendor.tar
|
||||
endif
|
||||
|
||||
$(TARGET)/$(BIN): extract
|
||||
cargo build --manifest-path Cargo.toml $(ARGS)
|
||||
mkdir -p .cargo
|
||||
cp debian/cargo.config .cargo/config.toml
|
||||
cargo build $(ARGS)
|
||||
|
||||
$(TARGET)/$(BIN).1.gz: $(TARGET)/$(BIN)
|
||||
help2man --no-info $< | gzip -c > $@.partial
|
||||
|
||||
60
README.md
60
README.md
@@ -70,6 +70,7 @@ Enumeration.
|
||||
- [Snap Install](#snap-install)
|
||||
- [Homebrew on MacOS and Linux](#homebrew-on-macos-and-linux)
|
||||
- [Cargo Install](#cargo-install)
|
||||
- [Kali Install](#kali-install)
|
||||
- [apt Install](#apt-install)
|
||||
- [AUR Install](#aur-install)
|
||||
- [Docker Install](#docker-install)
|
||||
@@ -105,6 +106,7 @@ Enumeration.
|
||||
- [Limit Number of Requests per Second (Rate Limiting) (new in `v2.0.0`)](#limit-number-of-requests-per-second-rate-limiting-new-in-v200)
|
||||
- [Silence all Output or Be Kinda Quiet (new in `v2.0.0`)](#silence-all-output-or-be-kinda-quiet-new-in-v200)
|
||||
- [Auto-tune or Auto-bail from Scans (new in `v2.1.0`)](#auto-tune-or-auto-bail-from-scans-new-in-v210)
|
||||
- [Run Scans in Parallel (new in `v2.2.0`)](#run-scans-in-parallel-new-in-v220)
|
||||
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
|
||||
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
||||
- [No file descriptors available](#no-file-descriptors-available)
|
||||
@@ -117,8 +119,9 @@ Enumeration.
|
||||
|
||||
### Download a Release
|
||||
|
||||
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.
|
||||
Releases for `armv7`, `aarch64`, and an `x86_64 .deb` can be found in the [Releases](https://github.com/epi052/feroxbuster/releases) section.
|
||||
|
||||
All other OS/architecture combinations can be installed dynamically using one of the methods shown below.
|
||||
|
||||
#### Linux (32 and 64-bit) & MacOS
|
||||
|
||||
@@ -193,6 +196,16 @@ brew install feroxbuster
|
||||
cargo install feroxbuster
|
||||
```
|
||||
|
||||
### Kali Install
|
||||
|
||||
🥳 `feroxbuster` was recently added to the official Kali Linux repos 🥳
|
||||
|
||||
If you're using kali, this is the preferred install method. Installing from the repos adds a [**ferox-config.toml**](#ferox-config.toml) in `/etc/feroxbuster/`, adds command completion for bash, fish, and zsh, includes a man page entry, and installs `feroxbuster` itself.
|
||||
|
||||
```
|
||||
sudo apt update && sudo apt install -y feroxbuster
|
||||
```
|
||||
|
||||
### apt Install
|
||||
|
||||
Download `feroxbuster_amd64.deb` from the [Releases](https://github.com/epi052/feroxbuster/releases) section. After
|
||||
@@ -212,6 +225,14 @@ Install `feroxbuster-git` on Arch Linux with your AUR helper of choice:
|
||||
yay -S feroxbuster-git
|
||||
```
|
||||
|
||||
### BlackArch install
|
||||
|
||||
Install `feroxbuster` on BlackArch Linux:
|
||||
|
||||
```
|
||||
pacman -S feroxbuster
|
||||
```
|
||||
|
||||
### Docker Install
|
||||
|
||||
> The following steps assume you have docker installed / setup
|
||||
@@ -370,6 +391,7 @@ A pre-made configuration file with examples of all available settings can be fou
|
||||
# status_codes = [200, 500]
|
||||
# filter_status = [301]
|
||||
# threads = 1
|
||||
# parallel = 2
|
||||
# timeout = 5
|
||||
# auto_tune = true
|
||||
# auto_bail = true
|
||||
@@ -463,6 +485,9 @@ OPTIONS:
|
||||
-W, --filter-words <WORDS>... Filter out messages of a particular word count (ex: -W 312 -W 91,82)
|
||||
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
|
||||
-o, --output <FILE> Output file to write results to (use w/ --json for JSON entries)
|
||||
--parallel <PARALLEL_SCANS>
|
||||
Run parallel feroxbuster instances (one child process per url passed via stdin)
|
||||
|
||||
-p, --proxy <PROXY>
|
||||
Proxy to use for requests (ex: http(s)://host:port, socks5(h)://host:port)
|
||||
|
||||
@@ -811,10 +836,11 @@ Below is an example of the Scan Cancel Menu™.
|
||||
Using the menu is pretty simple:
|
||||
- Press `ENTER` to view the menu
|
||||
- Choose a scan to cancel by entering its scan index (`1`)
|
||||
- more than one scan can be selected by using a comma-separated list (`1,2,3` ... etc)
|
||||
- more than one scan can be selected by using a comma-separated list of indexes and/or ranges (`1-4,8,9-13` ... etc)
|
||||
- Confirm selections, after which all non-cancelled scans will resume
|
||||
- To skip confirmation, simply add a `-f` somewhere in your input (`3-5 -f`)
|
||||
|
||||
Here is a short demonstration of cancelling two in-progress scans found via recursion.
|
||||
Here is a short demonstration of force cancelling a range of scans followed by a single scan with interactive prompt.
|
||||
|
||||

|
||||
|
||||
@@ -898,6 +924,29 @@ The AutoBail policy aborts individual directory scans when one of the criteria a
|
||||
|
||||

|
||||
|
||||
### Run Scans in Parallel (new in `v2.2.0`)
|
||||
|
||||
Version 2.2.0 introduces the `--parallel` option. If you're one of those people who use `feroxbuster` to scan 100s of hosts at a time, this is the option for you! `--parallel` spawns a child process per target passed in over stdin (recursive directories are still async within each child).
|
||||
|
||||
The number of parallel scans is limited to whatever you pass to `--parallel`. When one child finishes its scan, the next child will be spawned.
|
||||
|
||||
Unfortunately, using `--parallel` limits terminal output such that only discovered URLs are shown. No amount of `-v`'s will help you here. I imagine this isn't too big of a deal, as folks that need `--parallel` probably aren't sitting there watching the output... 🙃
|
||||
|
||||
Example Command:
|
||||
```
|
||||
cat large-target-list | ./feroxbuster --stdin --parallel 10 --extract-links --auto-bail
|
||||
```
|
||||
|
||||
Resuling Process List (illustrative):
|
||||
```
|
||||
feroxbuster --stdin --parallel 10
|
||||
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-one
|
||||
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-two
|
||||
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-three
|
||||
\_ ...
|
||||
\_ feroxbuster --silent --extract-links --auto-bail -u https://target-ten
|
||||
```
|
||||
|
||||
## 🧐 Comparison w/ Similar Tools
|
||||
|
||||
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
||||
@@ -921,7 +970,7 @@ few of the use-cases in which feroxbuster may be a better fit:
|
||||
| fast | ✔ | ✔ | ✔ |
|
||||
| allows recursion | ✔ | | ✔ |
|
||||
| can specify query parameters | ✔ | | ✔ |
|
||||
| SOCKS proxy support | ✔ | | |
|
||||
| SOCKS proxy support | ✔ | | ✔ |
|
||||
| multiple target scan (via stdin or multiple -u) | ✔ | | ✔ |
|
||||
| configuration file for default value override | ✔ | | ✔ |
|
||||
| can accept urls via STDIN as part of a pipeline | ✔ | | ✔ |
|
||||
@@ -947,6 +996,7 @@ few of the use-cases in which feroxbuster may be a better fit:
|
||||
| hide progress bars or be silent (or some variation) (`v2.0.0`) | ✔ | ✔ | ✔ |
|
||||
| automatically tune scans based on errors/403s/429s (`v2.1.0`) | ✔ | | |
|
||||
| automatically stop scans based on errors/403s/429s (`v2.1.0`) | ✔ | | ✔ |
|
||||
| run scans in parallel (1 process per target) (`v2.2.0`) | ✔ | | |
|
||||
| **huge** number of other options | | | ✔ |
|
||||
|
||||
Of note, there's another written-in-rust content discovery tool, [rustbuster](https://github.com/phra/rustbuster). I
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# replay_proxy = "http://127.0.0.1:8081"
|
||||
# replay_codes = [200, 302]
|
||||
# verbosity = 1
|
||||
# parallel = 8
|
||||
# scan_limit = 6
|
||||
# rate_limit = 250
|
||||
# quiet = true
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 670 KiB |
@@ -3,59 +3,63 @@
|
||||
BASE_URL=https://github.com/epi052/feroxbuster/releases/latest/download
|
||||
|
||||
MAC_ZIP=x86_64-macos-feroxbuster.zip
|
||||
MAC_URL="${BASE_URL}/${MAC_ZIP}"
|
||||
MAC_URL="$BASE_URL/$MAC_ZIP"
|
||||
|
||||
LIN32_ZIP=x86-linux-feroxbuster.zip
|
||||
LIN32_URL="${BASE_URL}/${LIN32_ZIP}"
|
||||
LIN32_URL="$BASE_URL/$LIN32_ZIP"
|
||||
|
||||
LIN64_ZIP=x86_64-linux-feroxbuster.zip
|
||||
LIN64_URL="${BASE_URL}/${LIN64_ZIP}"
|
||||
LIN64_URL="$BASE_URL/$LIN64_ZIP"
|
||||
|
||||
EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf
|
||||
|
||||
echo "[+] Installing feroxbuster!"
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "[=] Found MacOS, downloading from ${MAC_URL}"
|
||||
|
||||
curl -sLO "${MAC_URL}"
|
||||
unzip -o "${MAC_ZIP}" > /dev/null
|
||||
rm "${MAC_ZIP}"
|
||||
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
|
||||
if [[ $(getconf LONG_BIT) == 32 ]]; then
|
||||
echo "[=] Found 32-bit Linux, downloading from ${LIN32_URL}"
|
||||
|
||||
curl -sLO "${LIN32_URL}"
|
||||
unzip -o "${LIN32_ZIP}" > /dev/null
|
||||
rm "${LIN32_ZIP}"
|
||||
else
|
||||
echo "[=] Found 64-bit Linux, downloading from ${LIN64_URL}"
|
||||
|
||||
curl -sLO "${LIN64_URL}"
|
||||
unzip -o "${LIN64_ZIP}" > /dev/null
|
||||
rm "${LIN64_ZIP}"
|
||||
fi
|
||||
|
||||
if [[ -e ~/.fonts/NotoColorEmoji.ttf ]]; then
|
||||
echo "[=] Found Noto Emoji Font, skipping install"
|
||||
else
|
||||
echo "[=] Installing Noto Emoji Font"
|
||||
mkdir -p ~/.fonts
|
||||
pushd ~/.fonts 2>&1 >/dev/null
|
||||
|
||||
curl -sLO "${EMOJI_URL}"
|
||||
|
||||
fc-cache -f -v >/dev/null
|
||||
|
||||
popd 2>&1 >/dev/null
|
||||
echo "[+] Noto Emoji Font installed"
|
||||
fi
|
||||
which unzip &>/dev/null
|
||||
if [ "$?" = "0" ]; then
|
||||
echo "[+] unzip found"
|
||||
else
|
||||
echo "[ ] unzip not found, exiting. "
|
||||
exit -1
|
||||
fi
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "[=] Found MacOS, downloading from $MAC_URL"
|
||||
|
||||
curl -sLO "$MAC_URL"
|
||||
unzip -o "$MAC_ZIP" >/dev/null
|
||||
rm "$MAC_ZIP"
|
||||
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
|
||||
if [[ $(getconf LONG_BIT) == 32 ]]; then
|
||||
echo "[=] Found 32-bit Linux, downloading from $LIN32_URL"
|
||||
|
||||
curl -sLO "$LIN32_URL"
|
||||
unzip -o "$LIN32_ZIP" >/dev/null
|
||||
rm "$LIN32_ZIP"
|
||||
else
|
||||
echo "[=] Found 64-bit Linux, downloading from $LIN64_URL"
|
||||
|
||||
curl -sLO "$LIN64_URL"
|
||||
unzip -o "$LIN64_ZIP" >/dev/null
|
||||
rm "$LIN64_ZIP"
|
||||
fi
|
||||
|
||||
if [[ -e ~/.fonts/NotoColorEmoji.ttf ]]; then
|
||||
echo "[=] Found Noto Emoji Font, skipping install"
|
||||
else
|
||||
echo "[=] Installing Noto Emoji Font"
|
||||
mkdir -p ~/.fonts
|
||||
pushd ~/.fonts 2>&1 >/dev/null
|
||||
|
||||
curl -sLO "$EMOJI_URL"
|
||||
|
||||
fc-cache -f -v >/dev/null
|
||||
|
||||
popd 2>&1 >/dev/null
|
||||
echo "[+] Noto Emoji Font installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x ./feroxbuster
|
||||
|
||||
echo "[+] Installed feroxbuster version $(./feroxbuster -V)"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ _feroxbuster() {
|
||||
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]' \
|
||||
'-L+[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
|
||||
'--scan-limit=[Limit total number of concurrent scans (default: 0, i.e. no limit)]' \
|
||||
'--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]' \
|
||||
'(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default: 0, i.e. no limit)]' \
|
||||
'--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]' \
|
||||
'(--silent)*-v[Increase verbosity level (use -vv or more for greater effect. \[CAUTION\] 4 -v'\''s is probably too much)]' \
|
||||
|
||||
@@ -63,6 +63,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--filter-similar-to', 'filter-similar-to', [CompletionResultType]::ParameterName, 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)')
|
||||
[CompletionResult]::new('-L', 'L', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
|
||||
[CompletionResult]::new('--scan-limit', 'scan-limit', [CompletionResultType]::ParameterName, 'Limit total number of concurrent scans (default: 0, i.e. no limit)')
|
||||
[CompletionResult]::new('--parallel', 'parallel', [CompletionResultType]::ParameterName, 'Run parallel feroxbuster instances (one child process per url passed via stdin)')
|
||||
[CompletionResult]::new('--rate-limit', 'rate-limit', [CompletionResultType]::ParameterName, 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)')
|
||||
[CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)')
|
||||
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v''s is probably too much)')
|
||||
|
||||
@@ -20,7 +20,7 @@ _feroxbuster() {
|
||||
|
||||
case "${cmd}" in
|
||||
feroxbuster)
|
||||
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --rate-limit --time-limit "
|
||||
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
@@ -199,6 +199,10 @@ _feroxbuster() {
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--parallel)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--rate-limit)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
|
||||
@@ -21,6 +21,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -s N -l filter-lines -d 'Filt
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s C -l filter-status -d 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l filter-similar-to -d 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s L -l scan-limit -d 'Limit total number of concurrent scans (default: 0, i.e. no limit)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l parallel -d 'Run parallel feroxbuster instances (one child process per url passed via stdin)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l rate-limit -d 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -l time-limit -d 'Limit total run time of all scans (ex: --time-limit 10m)'
|
||||
complete -c feroxbuster -n "__fish_use_subcommand" -s v -l verbosity -d 'Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v\'s is probably too much)'
|
||||
|
||||
@@ -125,6 +125,9 @@ pub struct Banner {
|
||||
/// represents Configuration.rate_limit
|
||||
rate_limit: BannerEntry,
|
||||
|
||||
/// represents Configuration.parallel
|
||||
parallel: BannerEntry,
|
||||
|
||||
/// represents Configuration.auto_tune
|
||||
auto_tune: BannerEntry,
|
||||
|
||||
@@ -168,7 +171,7 @@ impl Banner {
|
||||
code_filters.push(status_colorizer(&code.to_string()))
|
||||
}
|
||||
let filter_status = BannerEntry::new(
|
||||
"🗑",
|
||||
"💢",
|
||||
"Status Code Filters",
|
||||
&format!("[{}]", code_filters.join(", ")),
|
||||
);
|
||||
@@ -281,6 +284,7 @@ impl Banner {
|
||||
BannerEntry::new("🤪", "Filter Wildcards", &(!config.dont_filter).to_string());
|
||||
let add_slash = BannerEntry::new("🪓", "Add Slash", &config.add_slash.to_string());
|
||||
let time_limit = BannerEntry::new("🕖", "Time Limit", &config.time_limit);
|
||||
let parallel = BannerEntry::new("🛤", "Parallel Scans", &config.parallel.to_string());
|
||||
let rate_limit =
|
||||
BannerEntry::new("🚧", "Requests per Second", &config.rate_limit.to_string());
|
||||
|
||||
@@ -304,6 +308,7 @@ impl Banner {
|
||||
filter_line_count,
|
||||
filter_regex,
|
||||
extract_links,
|
||||
parallel,
|
||||
json,
|
||||
queries,
|
||||
output,
|
||||
@@ -518,6 +523,10 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
writeln!(&mut writer, "{}", self.scan_limit)?;
|
||||
}
|
||||
|
||||
if config.parallel > 0 {
|
||||
writeln!(&mut writer, "{}", self.parallel)?;
|
||||
}
|
||||
|
||||
if config.rate_limit > 0 {
|
||||
writeln!(&mut writer, "{}", self.rate_limit)?;
|
||||
}
|
||||
|
||||
@@ -198,6 +198,10 @@ pub struct Configuration {
|
||||
#[serde(default)]
|
||||
pub scan_limit: usize,
|
||||
|
||||
/// Number of parallel scans permitted; a limit of 0 means no limit is imposed
|
||||
#[serde(default)]
|
||||
pub parallel: usize,
|
||||
|
||||
/// Number of requests per second permitted (per directory); a limit of 0 means no limit is imposed
|
||||
#[serde(default)]
|
||||
pub rate_limit: usize,
|
||||
@@ -280,6 +284,7 @@ impl Default for Configuration {
|
||||
json: false,
|
||||
verbosity: 0,
|
||||
scan_limit: 0,
|
||||
parallel: 0,
|
||||
rate_limit: 0,
|
||||
add_slash: false,
|
||||
insecure: false,
|
||||
@@ -350,7 +355,8 @@ impl Configuration {
|
||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||
/// - **depth**: `4` (maximum recursion depth)
|
||||
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
||||
/// - **rate_limit**: `0` (no limit on concurrent scans imposed)
|
||||
/// - **parallel**: `0` (no limit on parallel scans imposed)
|
||||
/// - **rate_limit**: `0` (no limit on requests per second imposed)
|
||||
/// - **time_limit**: `None` (no limit on length of scan imposed)
|
||||
/// - **replay_proxy**: `None` (no limit on concurrent scans imposed)
|
||||
/// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
|
||||
@@ -486,6 +492,7 @@ impl Configuration {
|
||||
update_config_if_present!(&mut config.threads, args, "threads", usize);
|
||||
update_config_if_present!(&mut config.depth, args, "depth", usize);
|
||||
update_config_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
|
||||
update_config_if_present!(&mut config.parallel, args, "parallel", usize);
|
||||
update_config_if_present!(&mut config.rate_limit, args, "rate_limit", usize);
|
||||
update_config_if_present!(&mut config.wordlist, args, "wordlist", String);
|
||||
update_config_if_present!(&mut config.output, args, "output", String);
|
||||
@@ -793,6 +800,7 @@ impl Configuration {
|
||||
);
|
||||
update_if_not_default!(&mut conf.dont_filter, new.dont_filter, false);
|
||||
update_if_not_default!(&mut conf.scan_limit, new.scan_limit, 0);
|
||||
update_if_not_default!(&mut conf.parallel, new.parallel, 0);
|
||||
update_if_not_default!(&mut conf.rate_limit, new.rate_limit, 0);
|
||||
update_if_not_default!(&mut conf.replay_proxy, new.replay_proxy, "");
|
||||
update_if_not_default!(&mut conf.debug_log, new.debug_log, "");
|
||||
|
||||
@@ -20,6 +20,7 @@ fn setup_config_test() -> Configuration {
|
||||
auto_bail = true
|
||||
verbosity = 1
|
||||
scan_limit = 6
|
||||
parallel = 14
|
||||
rate_limit = 250
|
||||
time_limit = "10m"
|
||||
output = "/some/otherpath"
|
||||
@@ -146,6 +147,13 @@ fn config_reads_scan_limit() {
|
||||
assert_eq!(config.scan_limit, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_parallel() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.parallel, 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_rate_limit() {
|
||||
@@ -356,9 +364,10 @@ fn config_reads_headers() {
|
||||
/// parse the test config and see that the values parsed are correct
|
||||
fn config_reads_queries() {
|
||||
let config = setup_config_test();
|
||||
let mut queries = vec![];
|
||||
queries.push(("name".to_string(), "value".to_string()));
|
||||
queries.push(("rick".to_string(), "astley".to_string()));
|
||||
let queries = vec![
|
||||
("name".to_string(), "value".to_string()),
|
||||
("rick".to_string(), "astley".to_string()),
|
||||
];
|
||||
assert_eq!(config.queries, queries);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
@@ -53,7 +52,7 @@ pub enum Command {
|
||||
TryRecursion(Box<FeroxResponse>),
|
||||
|
||||
/// Send a pointer to the wordlist to the recursion handler
|
||||
UpdateWordlist(Arc<HashSet<String>>),
|
||||
UpdateWordlist(Arc<Vec<String>>),
|
||||
|
||||
/// Instruct the ScanHandler to join on all known scans, use sender to notify main when done
|
||||
JoinTasks(Sender<bool>),
|
||||
|
||||
@@ -139,8 +139,8 @@ impl TermOutHandler {
|
||||
Self {
|
||||
receiver,
|
||||
tx_file,
|
||||
config,
|
||||
file_task,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
@@ -54,7 +53,7 @@ pub struct ScanHandler {
|
||||
receiver: CommandReceiver,
|
||||
|
||||
/// wordlist (re)used for each scan
|
||||
wordlist: std::sync::Mutex<Option<Arc<HashSet<String>>>>,
|
||||
wordlist: std::sync::Mutex<Option<Arc<Vec<String>>>>,
|
||||
|
||||
/// group of scans that need to be joined
|
||||
tasks: Vec<Arc<FeroxScan>>,
|
||||
@@ -105,7 +104,7 @@ impl ScanHandler {
|
||||
}
|
||||
|
||||
/// Set the wordlist
|
||||
fn wordlist(&self, wordlist: Arc<HashSet<String>>) {
|
||||
fn wordlist(&self, wordlist: Arc<Vec<String>>) {
|
||||
if let Ok(mut guard) = self.wordlist.lock() {
|
||||
if guard.is_none() {
|
||||
let _ = std::mem::replace(&mut *guard, Some(wordlist));
|
||||
@@ -175,7 +174,7 @@ impl ScanHandler {
|
||||
}
|
||||
|
||||
/// Helper to easily get the (locked) underlying wordlist
|
||||
pub fn get_wordlist(&self) -> Result<Arc<HashSet<String>>> {
|
||||
pub fn get_wordlist(&self) -> Result<Arc<Vec<String>>> {
|
||||
if let Ok(guard) = self.wordlist.lock().as_ref() {
|
||||
if let Some(list) = guard.as_ref() {
|
||||
return Ok(list.clone());
|
||||
|
||||
94
src/main.rs
94
src/main.rs
@@ -1,13 +1,18 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
env::args,
|
||||
fs::File,
|
||||
io::{stderr, BufRead, BufReader},
|
||||
ops::Index,
|
||||
process::Command,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use futures::StreamExt;
|
||||
use tokio::{io, sync::oneshot};
|
||||
use tokio::{
|
||||
io,
|
||||
sync::{oneshot, Semaphore},
|
||||
};
|
||||
use tokio_util::codec::{FramedRead, LinesCodec};
|
||||
|
||||
use feroxbuster::{
|
||||
@@ -26,16 +31,23 @@ use feroxbuster::{
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
|
||||
lazy_static! {
|
||||
/// Limits the number of parallel scans active at any given time when using --parallel
|
||||
static ref PARALLEL_LIMITER: Semaphore = Semaphore::new(0);
|
||||
}
|
||||
|
||||
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
|
||||
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<HashSet<String>>> {
|
||||
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
|
||||
log::trace!("enter: get_unique_words_from_wordlist({})", path);
|
||||
|
||||
let file = File::open(&path).with_context(|| format!("Could not open {}", path))?;
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut words = HashSet::new();
|
||||
let mut words = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let result = match line {
|
||||
@@ -47,7 +59,7 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<HashSet<String>>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
words.insert(result);
|
||||
words.push(result);
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
@@ -65,11 +77,7 @@ async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
|
||||
// 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
|
||||
|
||||
let words = {
|
||||
let words_handles = handles.clone();
|
||||
tokio::spawn(async move { get_unique_words_from_wordlist(&words_handles.config.wordlist) })
|
||||
.await??
|
||||
};
|
||||
let words = get_unique_words_from_wordlist(&handles.config.wordlist)?;
|
||||
|
||||
if words.len() == 0 {
|
||||
bail!("Did not find any words in {}", handles.config.wordlist);
|
||||
@@ -226,6 +234,72 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
// --parallel branch
|
||||
if config.parallel > 0 {
|
||||
log::trace!("enter: parallel branch");
|
||||
|
||||
PARALLEL_LIMITER.add_permits(config.parallel);
|
||||
|
||||
let invocation = args();
|
||||
|
||||
let para_regex =
|
||||
Regex::new("--stdin|-q|--quiet|--silent|--verbosity|-v|-vv|-vvv|-vvvv").unwrap();
|
||||
|
||||
// remove stdin since only the original process will process targets
|
||||
// remove quiet and silent so we can force silent later to normalize output
|
||||
let mut original = invocation
|
||||
.filter(|s| !para_regex.is_match(s))
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
original.push("--silent".to_string()); // only output modifier allowed
|
||||
|
||||
// we need remove --parallel from command line so we don't hit this branch over and over
|
||||
// but we must remove --parallel N manually; the filter above never sees --parallel and the
|
||||
// value passed to it at the same time, so can't filter them out in one pass
|
||||
|
||||
// unwrap is fine, as it has to be in the args for us to be in this code branch
|
||||
let parallel_index = original.iter().position(|s| *s == "--parallel").unwrap();
|
||||
|
||||
// remove --parallel
|
||||
original.remove(parallel_index);
|
||||
|
||||
// remove N passed to --parallel (it's the same index again since everything shifts
|
||||
// from removing --parallel)
|
||||
original.remove(parallel_index);
|
||||
|
||||
// unvalidated targets fresh from stdin, just spawn children and let them do all checks
|
||||
for target in targets {
|
||||
// add the current target to the provided command
|
||||
let mut cloned = original.clone();
|
||||
cloned.push("-u".to_string());
|
||||
cloned.push(target);
|
||||
|
||||
let bin = cloned.index(0).to_owned(); // user's path to feroxbuster
|
||||
let args = cloned.index(1..).to_vec(); // and args
|
||||
|
||||
let permit = PARALLEL_LIMITER.acquire().await?;
|
||||
|
||||
log::debug!("parallel exec: {} {}", bin, args.join(" "));
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = Command::new(bin)
|
||||
.args(&args)
|
||||
.spawn()
|
||||
.expect("failed to spawn a child process")
|
||||
.wait()
|
||||
.expect("child process errored during execution");
|
||||
|
||||
drop(permit);
|
||||
result
|
||||
});
|
||||
}
|
||||
|
||||
clean_up(handles, tasks).await?;
|
||||
|
||||
log::trace!("exit: parallel branch && wrapped main");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if matches!(config.output_level, OutputLevel::Default) {
|
||||
// only print banner if output level is default (no banner on --quiet|--silent)
|
||||
let std_stderr = stderr(); // std::io::stderr
|
||||
|
||||
@@ -348,6 +348,14 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.takes_value(true)
|
||||
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("parallel")
|
||||
.long("parallel")
|
||||
.value_name("PARALLEL_SCANS")
|
||||
.takes_value(true)
|
||||
.requires("stdin")
|
||||
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("rate_limit")
|
||||
.long("rate-limit")
|
||||
|
||||
@@ -31,9 +31,10 @@ impl Menu {
|
||||
let separator = "─".to_string();
|
||||
|
||||
let instructions = format!(
|
||||
"Enter a {} list of indexes to {} (ex: 2,3)",
|
||||
"Enter a {} list of indexes/ranges to {} ({}: 1-4,8,9-13)",
|
||||
style("comma-separated").yellow(),
|
||||
style("cancel").red(),
|
||||
style("ex").cyan(),
|
||||
);
|
||||
|
||||
let name = format!(
|
||||
@@ -43,14 +44,22 @@ impl Menu {
|
||||
"💀"
|
||||
);
|
||||
|
||||
let force_msg = format!(
|
||||
"Add {} to {} confirmation ({}: 3-5 -f)",
|
||||
style("-f").yellow(),
|
||||
style("skip").yellow(),
|
||||
style("ex").cyan(),
|
||||
);
|
||||
|
||||
let longest = measure_text_width(&instructions).max(measure_text_width(&name));
|
||||
|
||||
let border = separator.repeat(longest);
|
||||
|
||||
let padded_name = pad_str(&name, longest, Alignment::Center, None);
|
||||
let padded_force = pad_str(&force_msg, longest, Alignment::Center, None);
|
||||
|
||||
let header = format!("{}\n{}\n{}", border, padded_name, border);
|
||||
let footer = format!("{}\n{}\n{}", border, instructions, border);
|
||||
let footer = format!("{}\n{}\n{}\n{}", border, instructions, padded_force, border);
|
||||
|
||||
Self {
|
||||
separator,
|
||||
@@ -93,23 +102,71 @@ impl Menu {
|
||||
self.term.write_line(msg).unwrap_or_default();
|
||||
}
|
||||
|
||||
/// split a string into vec of usizes
|
||||
pub(super) fn split_to_nums(&self, line: &str) -> Vec<usize> {
|
||||
line.split(',')
|
||||
.map(|s| {
|
||||
s.trim().to_string().parse::<usize>().unwrap_or_else(|e| {
|
||||
self.println(&format!("Found non-numeric input: {}", e));
|
||||
0
|
||||
})
|
||||
/// Helper for parsing a usize from a str
|
||||
fn str_to_usize(&self, value: &str) -> usize {
|
||||
if value.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
value
|
||||
.trim()
|
||||
.to_string()
|
||||
.parse::<usize>()
|
||||
.unwrap_or_else(|e| {
|
||||
self.println(&format!("Found non-numeric input: {}: {:?}", e, value));
|
||||
0
|
||||
})
|
||||
.filter(|m| *m != 0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// split a comma delimited string into vec of usizes
|
||||
pub(super) fn split_to_nums(&self, line: &str) -> Vec<usize> {
|
||||
let mut nums = Vec::new();
|
||||
let values = line.split(',');
|
||||
|
||||
for mut value in values {
|
||||
value = value.trim();
|
||||
|
||||
if value.contains('-') {
|
||||
// range of two values, needs further processing
|
||||
|
||||
let range: Vec<usize> = value
|
||||
.split('-')
|
||||
.map(|s| self.str_to_usize(s))
|
||||
.filter(|m| *m != 0)
|
||||
.collect();
|
||||
|
||||
if range.len() != 2 {
|
||||
// expecting [1, 4] or similar, if a 0 was used, we'd be left with a vec of size 1
|
||||
self.println(&format!("Found invalid range of scans: {}", value));
|
||||
continue;
|
||||
}
|
||||
|
||||
(range[0]..=range[1]).for_each(|n| {
|
||||
// iterate from lower to upper bound and add all interim values, skipping
|
||||
// any already known
|
||||
if !nums.contains(&n) {
|
||||
nums.push(n)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let value = self.str_to_usize(value);
|
||||
|
||||
if value != 0 && !nums.contains(&value) {
|
||||
// the zeroth scan is always skipped, skip already known values
|
||||
nums.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nums
|
||||
}
|
||||
|
||||
/// get comma-separated list of scan indexes from the user
|
||||
pub(super) fn get_scans_from_user(&self) -> Option<Vec<usize>> {
|
||||
pub(super) fn get_scans_from_user(&self) -> Option<(Vec<usize>, bool)> {
|
||||
if let Ok(line) = self.term.read_line() {
|
||||
Some(self.split_to_nums(&line))
|
||||
let force = line.contains("-f");
|
||||
let line = line.replace("-f", "");
|
||||
Some((self.split_to_nums(&line), force))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ impl FeroxScans {
|
||||
}
|
||||
|
||||
/// Given a list of indexes, cancel their associated FeroxScans
|
||||
async fn cancel_scans(&self, indexes: Vec<usize>) -> usize {
|
||||
async fn cancel_scans(&self, indexes: Vec<usize>, force: bool) -> usize {
|
||||
let menu_pause_duration = Duration::from_millis(SLEEP_DURATION);
|
||||
|
||||
let mut num_cancelled = 0_usize;
|
||||
@@ -273,7 +273,11 @@ impl FeroxScans {
|
||||
Err(..) => continue,
|
||||
};
|
||||
|
||||
let input = self.menu.confirm_cancellation(&selected.url);
|
||||
let input = if force {
|
||||
'y'
|
||||
} else {
|
||||
self.menu.confirm_cancellation(&selected.url)
|
||||
};
|
||||
|
||||
if input == 'y' || input == '\n' {
|
||||
self.menu.println(&format!("Stopping {}...", selected.url));
|
||||
@@ -305,8 +309,8 @@ impl FeroxScans {
|
||||
|
||||
let mut num_cancelled = 0_usize;
|
||||
|
||||
if let Some(input) = self.menu.get_scans_from_user() {
|
||||
num_cancelled += self.cancel_scans(input).await;
|
||||
if let Some((input, force)) = self.menu.get_scans_from_user() {
|
||||
num_cancelled += self.cancel_scans(input, force).await;
|
||||
};
|
||||
|
||||
self.menu.clear_screen();
|
||||
|
||||
@@ -47,7 +47,7 @@ impl FeroxSerialize for FeroxState {
|
||||
|
||||
/// Simple call to produce a JSON string using the given FeroxState
|
||||
fn as_json(&self) -> Result<String> {
|
||||
Ok(serde_json::to_string(&self)
|
||||
.with_context(|| fmt_err("Could not convert scan's running state to JSON"))?)
|
||||
serde_json::to_string(&self)
|
||||
.with_context(|| fmt_err("Could not convert scan's running state to JSON"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ fn feroxstates_feroxserialize_implementation() {
|
||||
|
||||
let json_state = ferox_state.as_json().unwrap();
|
||||
let expected = format!(
|
||||
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
|
||||
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
|
||||
saved_id, VERSION
|
||||
);
|
||||
println!("{}\n{}", expected, json_state);
|
||||
@@ -521,9 +521,13 @@ fn menu_print_header_and_footer() {
|
||||
fn split_to_nums_is_correct() {
|
||||
let menu = Menu::new();
|
||||
|
||||
let nums = menu.split_to_nums("1, 3, 4");
|
||||
let nums = menu.split_to_nums("1, 3, 4, 7 - 12, 10-10, 10-11, 9-12, 12-6, -1, 4-");
|
||||
|
||||
assert_eq!(nums, vec![1, 3, 4]);
|
||||
assert_eq!(nums, vec![1, 3, 4, 7, 8, 9, 10, 11, 12]);
|
||||
assert_eq!(menu.split_to_nums("9-12"), vec![9, 10, 11, 12]);
|
||||
assert!(menu.split_to_nums("-12").is_empty());
|
||||
assert!(menu.split_to_nums("12-").is_empty());
|
||||
assert!(menu.split_to_nums("\n").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -41,7 +41,7 @@ pub async fn start_max_time_thread(handles: Arc<Handles>) {
|
||||
log::trace!("exit: start_max_time_thread");
|
||||
|
||||
#[cfg(test)]
|
||||
panic!(handles);
|
||||
panic!("{:?}", handles);
|
||||
#[cfg(not(test))]
|
||||
let _ = TermInputHandler::sigint_handler(handles.clone());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashSet, ops::Deref, sync::atomic::Ordering, sync::Arc, time::Instant};
|
||||
use std::{ops::Deref, sync::atomic::Ordering, sync::Arc, time::Instant};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use futures::{stream, StreamExt};
|
||||
@@ -40,7 +40,7 @@ pub struct FeroxScanner {
|
||||
order: ScanOrder,
|
||||
|
||||
/// wordlist that's already been read from disk
|
||||
wordlist: Arc<HashSet<String>>,
|
||||
wordlist: Arc<Vec<String>>,
|
||||
|
||||
/// limiter that restricts the number of active FeroxScanners
|
||||
scan_limiter: Arc<Semaphore>,
|
||||
@@ -52,7 +52,7 @@ impl FeroxScanner {
|
||||
pub fn new(
|
||||
target_url: &str,
|
||||
order: ScanOrder,
|
||||
wordlist: Arc<HashSet<String>>,
|
||||
wordlist: Arc<Vec<String>>,
|
||||
scan_limiter: Arc<Semaphore>,
|
||||
handles: Arc<Handles>,
|
||||
) -> Self {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::TryFrom,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
sync::{
|
||||
@@ -9,7 +11,8 @@ use std::{
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
traits::FeroxSerialize,
|
||||
@@ -19,9 +22,8 @@ use crate::{
|
||||
use super::{error::StatError, field::StatField};
|
||||
|
||||
/// Data collection of statistics related to a scan
|
||||
#[derive(Default, Deserialize, Debug, Serialize)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Stats {
|
||||
#[serde(rename = "type")]
|
||||
/// Name of this type of struct, used for serialization, i.e. `{"type":"statistics"}`
|
||||
kind: String,
|
||||
|
||||
@@ -125,11 +127,9 @@ pub struct Stats {
|
||||
total_runtime: Mutex<Vec<f64>>,
|
||||
|
||||
/// tracker for the number of extensions the user specified
|
||||
#[serde(skip)]
|
||||
num_extensions: usize,
|
||||
|
||||
/// tracker for whether to use json during serialization or not
|
||||
#[serde(skip)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
@@ -147,6 +147,301 @@ impl FeroxSerialize for Stats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize implementation for Stats
|
||||
impl Serialize for Stats {
|
||||
/// Function that handles serialization of Stats
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("Stats", 32)?;
|
||||
|
||||
state.serialize_field("type", &self.kind)?;
|
||||
state.serialize_field("timeouts", &atomic_load!(self.timeouts))?;
|
||||
state.serialize_field("requests", &atomic_load!(self.requests))?;
|
||||
state.serialize_field("expected_per_scan", &atomic_load!(self.expected_per_scan))?;
|
||||
state.serialize_field("total_expected", &atomic_load!(self.total_expected))?;
|
||||
state.serialize_field("errors", &atomic_load!(self.errors))?;
|
||||
state.serialize_field("successes", &atomic_load!(self.successes))?;
|
||||
state.serialize_field("redirects", &atomic_load!(self.redirects))?;
|
||||
state.serialize_field("client_errors", &atomic_load!(self.client_errors))?;
|
||||
state.serialize_field("server_errors", &atomic_load!(self.server_errors))?;
|
||||
state.serialize_field("total_scans", &atomic_load!(self.total_scans))?;
|
||||
state.serialize_field("initial_targets", &atomic_load!(self.initial_targets))?;
|
||||
state.serialize_field("links_extracted", &atomic_load!(self.links_extracted))?;
|
||||
state.serialize_field("status_200s", &atomic_load!(self.status_200s))?;
|
||||
state.serialize_field("status_301s", &atomic_load!(self.status_301s))?;
|
||||
state.serialize_field("status_302s", &atomic_load!(self.status_302s))?;
|
||||
state.serialize_field("status_401s", &atomic_load!(self.status_401s))?;
|
||||
state.serialize_field("status_403s", &atomic_load!(self.status_403s))?;
|
||||
state.serialize_field("status_429s", &atomic_load!(self.status_429s))?;
|
||||
state.serialize_field("status_500s", &atomic_load!(self.status_500s))?;
|
||||
state.serialize_field("status_503s", &atomic_load!(self.status_503s))?;
|
||||
state.serialize_field("status_504s", &atomic_load!(self.status_504s))?;
|
||||
state.serialize_field("status_508s", &atomic_load!(self.status_508s))?;
|
||||
state.serialize_field("wildcards_filtered", &atomic_load!(self.wildcards_filtered))?;
|
||||
state.serialize_field("responses_filtered", &atomic_load!(self.responses_filtered))?;
|
||||
state.serialize_field(
|
||||
"resources_discovered",
|
||||
&atomic_load!(self.resources_discovered),
|
||||
)?;
|
||||
state.serialize_field("url_format_errors", &atomic_load!(self.url_format_errors))?;
|
||||
state.serialize_field("redirection_errors", &atomic_load!(self.redirection_errors))?;
|
||||
state.serialize_field("connection_errors", &atomic_load!(self.connection_errors))?;
|
||||
state.serialize_field("request_errors", &atomic_load!(self.request_errors))?;
|
||||
state.serialize_field("directory_scan_times", &self.directory_scan_times)?;
|
||||
state.serialize_field("total_runtime", &self.total_runtime)?;
|
||||
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize implementation for Stats
|
||||
impl<'a> Deserialize<'a> for Stats {
|
||||
/// Deserialize a Stats object from a serde_json::Value
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'a>,
|
||||
{
|
||||
let stats = Self::new(0, false);
|
||||
|
||||
let map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
|
||||
|
||||
for (key, value) in &map {
|
||||
match key.as_str() {
|
||||
"timeouts" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.timeouts, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"requests" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.requests, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"expected_per_scan" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.expected_per_scan, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"total_expected" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.total_expected, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"successes" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.successes, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"redirects" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.redirects, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"client_errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.client_errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"server_errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.server_errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"total_scans" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.total_scans, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"initial_targets" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.initial_targets, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"links_extracted" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.links_extracted, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_200s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_200s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_301s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_301s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_302s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_302s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_401s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_401s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_403s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_403s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_429s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_429s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_500s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_500s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_503s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_503s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_504s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_504s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"status_508s" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.status_508s, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"wildcards_filtered" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.wildcards_filtered, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"responses_filtered" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.responses_filtered, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"resources_discovered" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.resources_discovered, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"url_format_errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.url_format_errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"redirection_errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.redirection_errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"connection_errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.connection_errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"request_errors" => {
|
||||
if let Some(num) = value.as_u64() {
|
||||
if let Ok(parsed) = usize::try_from(num) {
|
||||
atomic_increment!(stats.request_errors, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
"directory_scan_times" => {
|
||||
if let Some(arr) = value.as_array() {
|
||||
for val in arr {
|
||||
if let Some(parsed) = val.as_f64() {
|
||||
if let Ok(mut guard) = stats.directory_scan_times.lock() {
|
||||
guard.push(parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"total_runtime" => {
|
||||
if let Some(arr) = value.as_array() {
|
||||
for val in arr {
|
||||
if let Some(parsed) = val.as_f64() {
|
||||
if let Ok(mut guard) = stats.total_runtime.lock() {
|
||||
guard.push(parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
}
|
||||
|
||||
/// implementation of statistics data collection struct
|
||||
impl Stats {
|
||||
/// Small wrapper for default to set `kind` to "statistics" and `total_runtime` to have at least
|
||||
|
||||
@@ -948,3 +948,27 @@ fn banner_doesnt_print_when_quiet() {
|
||||
.and(predicate::str::contains("User-Agent").not()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see nothing as --parallel forces --silent to be true
|
||||
fn banner_prints_parallel() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--stdin")
|
||||
.arg("--parallel")
|
||||
.arg("4316")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.not()
|
||||
.and(predicate::str::contains("Target Url").not())
|
||||
.and(predicate::str::contains("Parallel Scans").not())
|
||||
.and(predicate::str::contains("Threads").not())
|
||||
.and(predicate::str::contains("Wordlist").not())
|
||||
.and(predicate::str::contains("Status Codes").not())
|
||||
.and(predicate::str::contains("Timeout (secs)").not())
|
||||
.and(predicate::str::contains("User-Agent").not()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
mod utils;
|
||||
use assert_cmd::Command;
|
||||
use httpmock::Method::GET;
|
||||
use httpmock::MockServer;
|
||||
use httpmock::{MockServer, Regex};
|
||||
use predicates::prelude::*;
|
||||
use std::fs::read_to_string;
|
||||
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
|
||||
#[test]
|
||||
@@ -89,3 +90,66 @@ fn main_use_empty_stdin_targets() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send three targets over stdin, expect parallel to spawn children and each child config to show
|
||||
/// up in the output file
|
||||
fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let t1 = MockServer::start();
|
||||
let t2 = MockServer::start();
|
||||
let t3 = MockServer::start();
|
||||
|
||||
let words = [
|
||||
String::from("LICENSE"),
|
||||
String::from("stuff"),
|
||||
String::from("things"),
|
||||
String::from("mostuff"),
|
||||
String::from("mothings"),
|
||||
];
|
||||
let (word_tmp_dir, wordlist) = setup_tmp_directory(&words, "wordlist")?;
|
||||
let (output_dir, outfile) = setup_tmp_directory(&[], "output-file")?;
|
||||
let (tgt_tmp_dir, targets) =
|
||||
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--stdin")
|
||||
.arg("--parallel")
|
||||
.arg("2")
|
||||
.arg("-vvvv")
|
||||
.arg("--debug-log")
|
||||
.arg(outfile.as_os_str())
|
||||
.arg("--wordlist")
|
||||
.arg(wordlist.as_os_str())
|
||||
.pipe_stdin(targets)
|
||||
.unwrap()
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("Could not connect to any target provided")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.not(), // no target url found
|
||||
);
|
||||
|
||||
let contents = read_to_string(outfile).unwrap();
|
||||
println!("contents: {}", contents);
|
||||
|
||||
assert!(contents.contains("parallel branch && wrapped main")); // exits parallel branch
|
||||
|
||||
// DBG 0.007 feroxbuster parallel exec: target/debug/feroxbuster
|
||||
// --debug-log /tmp/.tmpAjRts6/output-file --wordlist /tmp/.tmpS4CKKq/wordlist
|
||||
// --silent -u http://127.0.0.1:41979/
|
||||
let r1 = Regex::new(&format!("parallel exec:.*-u {}", t1.url("/"))).unwrap();
|
||||
let r2 = Regex::new(&format!("parallel exec:.*-u {}", t2.url("/"))).unwrap();
|
||||
let r3 = Regex::new(&format!("parallel exec:.*-u {}", t3.url("/"))).unwrap();
|
||||
|
||||
assert!(r1.is_match(&contents)); // all 3 were spawned
|
||||
assert!(r2.is_match(&contents));
|
||||
assert!(r3.is_match(&contents));
|
||||
|
||||
teardown_tmp_directory(word_tmp_dir);
|
||||
teardown_tmp_directory(tgt_tmp_dir);
|
||||
teardown_tmp_directory(output_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user