Compare commits

...

73 Commits

Author SHA1 Message Date
epi
ccb10c1c68 Update README.md 2022-04-15 05:53:58 -05:00
epi
20ab0aade3 Update README.md 2022-04-15 05:52:14 -05:00
epi
02ad0b1d85 Update README.md 2022-04-15 05:49:58 -05:00
epi
09aad922c1 Update README.md 2022-04-15 05:48:49 -05:00
epi
697f947bfa Update README.md 2022-04-15 05:47:42 -05:00
epi
d300d68737 Update README.md 2022-04-15 05:46:39 -05:00
epi
c2c6854db4 Merge pull request #541 from epi052/all-contributors/add-Flangyver
docs: add Flangyver as a contributor for ideas
2022-04-15 05:45:26 -05:00
allcontributors[bot]
63be575d89 docs: update .all-contributorsrc [skip ci] 2022-04-15 10:45:17 +00:00
allcontributors[bot]
0d25fda11e docs: update README.md [skip ci] 2022-04-15 10:45:16 +00:00
epi
b0341c2432 Merge pull request #540 from epi052/all-contributors/add-0xdf223
docs: add 0xdf223 as a contributor for bug, ideas
2022-04-15 05:42:54 -05:00
allcontributors[bot]
62352db152 docs: update .all-contributorsrc [skip ci] 2022-04-15 10:42:41 +00:00
allcontributors[bot]
3090edc49c docs: update README.md [skip ci] 2022-04-15 10:42:40 +00:00
epi
85d686d1aa Merge pull request #539 from epi052/all-contributors/add-ThisLimn0
docs: add ThisLimn0 as a contributor for bug
2022-04-15 05:41:41 -05:00
allcontributors[bot]
17138f4ef7 docs: update .all-contributorsrc [skip ci] 2022-04-15 10:41:28 +00:00
allcontributors[bot]
1d30b7db31 docs: update README.md [skip ci] 2022-04-15 10:41:27 +00:00
epi
4c0d3c91a0 Merge pull request #536 from epi052/535-status-code-filter-overhaul
535 status code filter overhaul
2022-04-15 05:39:55 -05:00
epi
96fc6b232a update 2022-04-14 21:08:33 -05:00
epi
9b306aad34 update 2022-04-14 16:45:14 -05:00
epi
10eee184d0 update 2022-04-14 16:42:25 -05:00
epi
986161f05f update 2022-04-14 16:39:50 -05:00
epi
4a19dbfd7d update 2022-04-14 16:14:23 -05:00
epi
b8ceeaff0f update 2022-04-14 16:07:37 -05:00
epi
d04e58036e update 2022-04-14 13:10:56 -05:00
epi
d1a74207f4 one more again 2022-04-14 08:13:51 -05:00
epi
03a36f0b60 update 2022-04-14 07:54:43 -05:00
epi
f2d9269643 update 2022-04-14 07:44:35 -05:00
epi
bba7cba02e update 2022-04-14 06:46:26 -05:00
epi
fffd1e5c82 update 2022-04-14 06:44:51 -05:00
epi
3c6da0f782 update 2022-04-14 06:40:35 -05:00
epi
5ecd937c0e reverted heuristics tests 2022-04-14 06:25:55 -05:00
epi
9f6221daf6 removed more toolchain actions 2022-04-14 06:18:00 -05:00
epi
af49fd8e62 attempting to remove toolchain action 2022-04-14 06:15:14 -05:00
epi
d1daefd8ba removed allows from ci clippy 2022-04-14 06:12:42 -05:00
epi
3e8255d5b7 reverted ci back to normal 2022-04-14 06:11:38 -05:00
epi
5af18e83d8 ci tweak 2022-04-14 05:48:43 -05:00
epi
d1d0757d56 test troubleshoot 2022-04-14 05:28:20 -05:00
epi
f5f9344a81 test troubleshoot 2022-04-14 05:09:50 -05:00
epi
fd52e39188 reverted build to main only 2022-04-13 20:44:57 -05:00
epi
22377dc9a3 trying again with new ci configs 2022-04-13 20:43:34 -05:00
epi
1cf7dff734 trying again with new ci configs 2022-04-13 20:34:16 -05:00
epi
7c9eb900b7 reverted test action 2022-04-13 20:14:57 -05:00
epi
8480b3cc2c reverted coverage 2022-04-13 20:01:18 -05:00
epi
9d29142046 lint 2022-04-13 19:45:30 -05:00
epi
38c194b222 tweaked coverage action 2022-04-13 19:36:05 -05:00
epi
72dc14bf3d fixed nlp tests 2022-04-13 19:20:24 -05:00
epi
9a7c690c17 changed to SecLists 2022-04-13 17:55:33 -05:00
epi
de4514e381 test build windows 2022-04-13 17:27:15 -05:00
epi
2be8aaf2bf changed -w default when on windows host 2022-04-13 17:19:09 -05:00
epi
4db3a0b056 updated help a bit 2022-04-13 16:40:51 -05:00
epi
a9ef7f180f force-recursion and no-recursion are mutually exclusive 2022-04-13 16:34:52 -05:00
epi
ac7cb5d6b6 lint / tests 2022-04-13 06:25:35 -05:00
epi
f8e18abb48 added filter checking logic to collect-backups requests 2022-04-12 21:09:51 -05:00
epi
1d805aca5a tweaked pre-processing and selection criteria for nlp 2022-04-12 20:55:37 -05:00
epi
fa09266804 handles passed through to termhandler 2022-04-12 20:53:24 -05:00
epi
15592c3dfd added AddHandles command 2022-04-12 20:52:57 -05:00
epi
53c171aeb5 extract-links should also abide by forced recursion into found assets only 2022-04-12 07:15:40 -05:00
epi
5167d24c4b another deb builder fix attempt 2022-04-12 06:49:42 -05:00
epi
81ff62c53d force recursion should only happen on found assets 2022-04-12 06:45:37 -05:00
epi
433f68458e updated test runner to nextest 2022-04-12 06:11:09 -05:00
epi
d32720a90a fixed deb build, maybe 2022-04-12 06:04:35 -05:00
epi
0ceef975e6 updated ci coverage job 2022-04-12 05:52:03 -05:00
epi
6d7d6c4e7b added tests for --force-recursion 2022-04-11 20:14:41 -05:00
epi
5f21953bc1 added --force-recursion option 2022-04-11 20:13:10 -05:00
epi
6906ac0ee8 added force-recursion to example config 2022-04-11 20:12:58 -05:00
epi
cafe766d9e bumped version to 2.7.0 2022-04-11 20:12:38 -05:00
epi
98d0d177df updated parse extensions logic and banner to reflect status code changes 2022-04-11 17:37:36 -05:00
epi
23e10833d0 cicd test 2022-04-11 17:13:01 -05:00
epi
7f51d0f7bf initial stab at updated statuscode behavior 2022-04-11 17:10:53 -05:00
epi
9515a5da99 fixed bug related to methods/responses not being displayed 2022-04-09 11:08:46 -05:00
epi
b417ab41a5 fixed bug related to methods/responses not being displayed 2022-04-09 11:08:16 -05:00
epi
b946565c2f updated shell completions for new version 2022-04-09 06:46:14 -05:00
epi
714b054360 bumped version to 2.6.3 2022-04-09 06:24:48 -05:00
epi
30544eaf7d fixed bug in replay proxy related to #501 2022-04-09 06:18:50 -05:00
32 changed files with 544 additions and 283 deletions

View File

@@ -393,6 +393,34 @@
"contributions": [ "contributions": [
"ideas" "ideas"
] ]
},
{
"login": "ThisLimn0",
"name": "Limn0",
"avatar_url": "https://avatars.githubusercontent.com/u/67125885?v=4",
"profile": "https://github.com/ThisLimn0",
"contributions": [
"bug"
]
},
{
"login": "0xdf223",
"name": "0xdf",
"avatar_url": "https://avatars.githubusercontent.com/u/76954092?v=4",
"profile": "https://github.com/0xdf223",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "Flangyver",
"name": "Flangyver",
"avatar_url": "https://avatars.githubusercontent.com/u/59575870?v=4",
"profile": "https://github.com/Flangyver",
"contributions": [
"ideas"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -73,18 +73,22 @@ jobs:
name: ${{ matrix.name }}.tar.gz name: ${{ matrix.name }}.tar.gz
path: ${{ matrix.name }}.tar.gz path: ${{ matrix.name }}.tar.gz
build-deb: # build-deb:
needs: [build-nix] # needs: [build-nix]
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- uses: actions/checkout@master # - uses: actions/checkout@master
- name: Deb Build # - name: Install cargo-deb
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0 # run: cargo install -f cargo-deb
- name: Upload Deb Artifact # - name: Install musl toolchain
uses: actions/upload-artifact@v2 # run: rustup target add x86_64-unknown-linux-musl
with: # - name: Deb Build
name: feroxbuster_amd64.deb # run: cargo deb --target=x86_64-unknown-linux-musl
path: ./target/x86_64-unknown-linux-musl/debian/* # - name: Upload Deb Artifact
# uses: actions/upload-artifact@v2
# with:
# name: feroxbuster_amd64.deb
# path: ./target/x86_64-unknown-linux-musl/debian/*
build-macos: build-macos:
env: env:

View File

@@ -8,11 +8,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: check command: check
@@ -22,26 +17,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - name: Install latest nextest release
uses: taiki-e/install-action@nextest
- name: Test with latest nextest release
uses: actions-rs/cargo@v1
with: with:
profile: minimal command: nextest
toolchain: stable args: run --all-features --all-targets --retries 10
override: true
- uses: actions-rs/cargo@v1
with:
command: test
fmt: fmt:
name: Rust fmt name: Rust fmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: fmt command: fmt
@@ -52,13 +40,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add clippy
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: clippy command: clippy
args: --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic args: --all-targets --all-features -- -D warnings

View File

@@ -3,42 +3,22 @@ on: [push]
name: Code Coverage Pipeline name: Code Coverage Pipeline
jobs: jobs:
upload-coverage: coverage:
name: LLVM Coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1 - name: Install llvm-tools-preview
with: run: rustup toolchain install stable --component llvm-tools-preview
toolchain: nightly - name: Install cargo-llvm-cov
override: true uses: taiki-e/install-action@cargo-llvm-cov
- uses: actions-rs/cargo@v1 - name: Install cargo-nextest
with: uses: taiki-e/install-action@nextest
command: clean - name: Generate code coverage
- uses: actions-rs/cargo@v1 run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info -- --retries 10
with: - name: Upload coverage to Codecov
command: test uses: codecov/codecov-action@v1
args: --all-features --no-fail-fast
env:
CARGO_INCREMENTAL: '0'
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
- uses: actions/upload-artifact@v2
with:
name: lcov.info
path: lcov.info
- name: Convert lcov to xml
run: |
curl -O https://raw.githubusercontent.com/epi052/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py
chmod +x lcov_cobertura.py
./lcov_cobertura.py ./lcov.info
- uses: codecov/codecov-action@v1
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml files: lcov.info
name: codecov-umbrella
fail_ci_if_error: true fail_ci_if_error: true
- uses: actions/upload-artifact@v2
with:
name: coverage.xml
path: ./coverage.xml

2
Cargo.lock generated
View File

@@ -671,7 +671,7 @@ dependencies = [
[[package]] [[package]]
name = "feroxbuster" name = "feroxbuster"
version = "2.6.2" version = "2.7.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "feroxbuster" name = "feroxbuster"
version = "2.6.2" version = "2.7.0"
authors = ["Ben 'epi' Risher (@epi052)"] authors = ["Ben 'epi' Risher (@epi052)"]
license = "MIT" license = "MIT"
edition = "2021" edition = "2021"
@@ -8,7 +8,13 @@ homepage = "https://github.com/epi052/feroxbuster"
repository = "https://github.com/epi052/feroxbuster" repository = "https://github.com/epi052/feroxbuster"
description = "A fast, simple, recursive content discovery tool." description = "A fast, simple, recursive content discovery tool."
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"] keywords = [
"pentest",
"enumeration",
"url-bruteforce",
"content-discovery",
"web",
]
exclude = [".github/*", "img/*", "check-coverage.sh"] exclude = [".github/*", "img/*", "check-coverage.sh"]
build = "build.rs" build = "build.rs"
@@ -49,7 +55,7 @@ rlimit = "0.8.3"
ctrlc = "3.2.1" ctrlc = "3.2.1"
fuzzyhash = "0.2.1" fuzzyhash = "0.2.1"
anyhow = "1.0.56" anyhow = "1.0.56"
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3.0"
@@ -67,9 +73,29 @@ section = "utility"
license-file = ["LICENSE", "4"] license-file = ["LICENSE", "4"]
conf-files = ["/etc/feroxbuster/ferox-config.toml"] conf-files = ["/etc/feroxbuster/ferox-config.toml"]
assets = [ assets = [
["target/release/feroxbuster", "/usr/bin/", "755"], [
["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"], "target/release/feroxbuster",
["shell_completions/feroxbuster.bash", "/usr/share/bash-completion/completions/feroxbuster.bash", "644"], "/usr/bin/",
["shell_completions/feroxbuster.fish", "/usr/share/fish/completions/feroxbuster.fish", "644"], "755",
["shell_completions/_feroxbuster", "/usr/share/zsh/vendor-completions/_feroxbuster", "644"], ],
[
"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",
],
] ]

View File

@@ -201,7 +201,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
</tr> </tr>
<tr> <tr>
<td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td> <td align="center"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tib3rius</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ATib3rius" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf" title="Bug reports">🐛</a></td> <td align="center"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td> <td align="center"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>secure-77</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asecure-77" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td> <td align="center"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt=""/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
@@ -235,6 +235,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td> <td align="center"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Agtjamesa" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr> </tr>
<tr>
<td align="center"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Limn0</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AThisLimn0" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0xdf</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3A0xdf223" title="Bug reports">🐛</a> <a href="#ideas-0xdf223" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
</table> </table>
<!-- markdownlint-restore --> <!-- markdownlint-restore -->
@@ -242,4 +247,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -45,6 +45,7 @@
# dont_filter = true # dont_filter = true
# extract_links = true # extract_links = true
# depth = 1 # depth = 1
# force_recursion = true
# filter_size = [5174] # filter_size = [5174]
# filter_regex = ["^ignore me$"] # filter_regex = ["^ignore me$"]
# filter_similar = ["https://somesite.com/soft404"] # filter_similar = ["https://somesite.com/soft404"]

View File

@@ -24,8 +24,8 @@ _feroxbuster() {
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \ '--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.6.2)]:USER_AGENT: ' \ '-a+[Sets the User-Agent (default: feroxbuster/2.7.0)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.2)]:USER_AGENT: ' \ '--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.0)]:USER_AGENT: ' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \ '*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
@@ -46,8 +46,8 @@ _feroxbuster() {
'*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \ '*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \
'*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \ '*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
'*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \ '*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
'*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \ '(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \ '(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \ '*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \
'*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \ '*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
'*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \ '*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
@@ -88,6 +88,7 @@ _feroxbuster() {
'--insecure[Disables TLS certificate validation in the client]' \ '--insecure[Disables TLS certificate validation in the client]' \
'-n[Do not scan recursively]' \ '-n[Do not scan recursively]' \
'--no-recursion[Do not scan recursively]' \ '--no-recursion[Do not scan recursively]' \
'(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \ '-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \ '--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \
'(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \ '(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \

View File

@@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests') [CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.2)') [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.0)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.2)') [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.0)')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
@@ -94,6 +94,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client') [CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively') [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively') [CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('--force-recursion', 'force-recursion', [CompletionResultType]::ParameterName, 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings') [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
[CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings') [CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings')
[CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered') [CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered')

View File

@@ -19,7 +19,7 @@ _feroxbuster() {
case "${cmd}" in case "${cmd}" in
feroxbuster) feroxbuster)
opts="-h -V -u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o --help --version --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state" opts="-h -V -u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o --help --version --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --force-recursion --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests' cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.6.2)' cand -a 'Sets the User-Agent (default: feroxbuster/2.7.0)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.2)' cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.0)'
cand -x 'File extension(s) to search for (ex: -x php -x pdf js)' cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)' cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)'
cand -m 'Which HTTP request method(s) should be sent (default: GET)' cand -m 'Which HTTP request method(s) should be sent (default: GET)'
@@ -91,6 +91,7 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --insecure 'Disables TLS certificate validation in the client' cand --insecure 'Disables TLS certificate validation in the client'
cand -n 'Do not scan recursively' cand -n 'Do not scan recursively'
cand --no-recursion 'Do not scan recursively' cand --no-recursion 'Do not scan recursively'
cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)'
cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings' cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings' cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings'
cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered' cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered'

View File

@@ -163,6 +163,9 @@ pub struct Banner {
/// represents Configuration.collect_words /// represents Configuration.collect_words
collect_words: BannerEntry, collect_words: BannerEntry,
/// represents Configuration.collect_words
force_recursion: BannerEntry,
} }
/// implementation of Banner /// implementation of Banner
@@ -300,6 +303,8 @@ impl Banner {
&config.scan_limit.to_string(), &config.scan_limit.to_string(),
); );
let force_recursion =
BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string());
let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy); let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy);
let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string()); let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string());
let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string()); let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string());
@@ -409,6 +414,7 @@ impl Banner {
no_recursion, no_recursion,
rate_limit, rate_limit,
scan_limit, scan_limit,
force_recursion,
time_limit, time_limit,
url_denylist, url_denylist,
collect_extensions, collect_extensions,
@@ -511,11 +517,12 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.threads)?; writeln!(&mut writer, "{}", self.threads)?;
writeln!(&mut writer, "{}", self.wordlist)?; writeln!(&mut writer, "{}", self.wordlist)?;
writeln!(&mut writer, "{}", self.status_codes)?;
if !config.filter_status.is_empty() { if config.filter_status.is_empty() {
// exception here for an optional print in the middle of always printed values is due // -C and -s are mutually exclusive, and -s meaning changes when -C is used
// to me wanting the allows and denys to be printed one after the other // so only print one or the other
writeln!(&mut writer, "{}", self.status_codes)?;
} else {
writeln!(&mut writer, "{}", self.filter_status)?; writeln!(&mut writer, "{}", self.filter_status)?;
} }
@@ -642,6 +649,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.no_recursion)?; writeln!(&mut writer, "{}", self.no_recursion)?;
if config.force_recursion {
writeln!(&mut writer, "{}", self.force_recursion)?;
}
if config.scan_limit > 0 { if config.scan_limit > 0 {
writeln!(&mut writer, "{}", self.scan_limit)?; writeln!(&mut writer, "{}", self.scan_limit)?;
} }

View File

@@ -281,6 +281,10 @@ pub struct Configuration {
/// Automatically discover important words from within responses and add them to the wordlist /// Automatically discover important words from within responses and add them to the wordlist
#[serde(default)] #[serde(default)]
pub collect_words: bool, pub collect_words: bool,
/// override recursion logic to always attempt recursion, still respects --depth
#[serde(default)]
pub force_recursion: bool,
} }
impl Default for Configuration { impl Default for Configuration {
@@ -329,6 +333,7 @@ impl Default for Configuration {
collect_backups: false, collect_backups: false,
collect_words: false, collect_words: false,
save_state: true, save_state: true,
force_recursion: false,
proxy: String::new(), proxy: String::new(),
config: String::new(), config: String::new(),
output: String::new(), output: String::new(),
@@ -405,6 +410,7 @@ impl Configuration {
/// - **json**: `false` /// - **json**: `false`
/// - **dont_filter**: `false` (auto filter wildcard responses) /// - **dont_filter**: `false` (auto filter wildcard responses)
/// - **depth**: `4` (maximum recursion depth) /// - **depth**: `4` (maximum recursion depth)
/// - **force_recursion**: `false` (still respects recursion depth)
/// - **scan_limit**: `0` (no limit on concurrent scans imposed) /// - **scan_limit**: `0` (no limit on concurrent scans imposed)
/// - **parallel**: `0` (no limit on parallel scans imposed) /// - **parallel**: `0` (no limit on parallel scans imposed)
/// - **rate_limit**: `0` (no limit on requests per second imposed) /// - **rate_limit**: `0` (no limit on requests per second imposed)
@@ -774,6 +780,10 @@ impl Configuration {
config.json = true; config.json = true;
} }
if args.is_present("force_recursion") {
config.force_recursion = true;
}
//// ////
// organizational breakpoint; all options below alter the Client configuration // organizational breakpoint; all options below alter the Client configuration
//// ////
@@ -942,6 +952,7 @@ impl Configuration {
update_if_not_default!(&mut conf.output, new.output, ""); update_if_not_default!(&mut conf.output, new.output, "");
update_if_not_default!(&mut conf.redirects, new.redirects, false); update_if_not_default!(&mut conf.redirects, new.redirects, false);
update_if_not_default!(&mut conf.insecure, new.insecure, false); update_if_not_default!(&mut conf.insecure, new.insecure, false);
update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false);
update_if_not_default!(&mut conf.extract_links, new.extract_links, false); update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new()); update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new()); update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new());

View File

@@ -49,6 +49,7 @@ fn setup_config_test() -> Configuration {
json = true json = true
save_state = false save_state = false
depth = 1 depth = 1
force_recursion = true
filter_size = [4120] filter_size = [4120]
filter_regex = ["^ignore me$"] filter_regex = ["^ignore me$"]
filter_similar = ["https://somesite.com/soft404"] filter_similar = ["https://somesite.com/soft404"]
@@ -95,6 +96,7 @@ fn default_configuration() {
assert!(config.save_state); assert!(config.save_state);
assert!(!config.stdin); assert!(!config.stdin);
assert!(!config.add_slash); assert!(!config.add_slash);
assert!(!config.force_recursion);
assert!(!config.redirects); assert!(!config.redirects);
assert!(!config.extract_links); assert!(!config.extract_links);
assert!(!config.insecure); assert!(!config.insecure);
@@ -208,6 +210,13 @@ fn config_reads_silent() {
assert!(config.silent); assert!(config.silent);
} }
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_force_recursion() {
let config = setup_config_test();
assert!(config.force_recursion);
}
#[test] #[test]
/// parse the test config and see that the value parsed is correct /// parse the test config and see that the value parsed is correct
fn config_reads_quiet() { fn config_reads_quiet() {

View File

@@ -5,6 +5,7 @@ use tokio::sync::oneshot::Sender;
use crate::response::FeroxResponse; use crate::response::FeroxResponse;
use crate::{ use crate::{
event_handlers::Handles,
message::FeroxMessage, message::FeroxMessage,
statistics::{StatError, StatField}, statistics::{StatError, StatField},
traits::FeroxFilter, traits::FeroxFilter,
@@ -78,4 +79,8 @@ pub enum Command {
/// Break out of the (infinite) mpsc receive loop /// Break out of the (infinite) mpsc receive loop
Exit, Exit,
/// Give a handler access to an Arc<Handles> instance after the handler has
/// already been initialized
AddHandles(Arc<Handles>),
} }

View File

@@ -5,14 +5,13 @@ use anyhow::{Context, Result};
use futures::future::{BoxFuture, FutureExt}; use futures::future::{BoxFuture, FutureExt};
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use crate::statistics::StatField::TotalExpected;
use crate::{ use crate::{
config::Configuration, config::Configuration,
progress::PROGRESS_PRINTER, progress::PROGRESS_PRINTER,
response::FeroxResponse, response::FeroxResponse,
scanner::RESPONSES, scanner::RESPONSES,
send_command, skip_fail, send_command, skip_fail,
statistics::StatField::ResourcesDiscovered, statistics::StatField::{ResourcesDiscovered, TotalExpected},
traits::FeroxSerialize, traits::FeroxSerialize,
utils::{ferox_print, fmt_err, make_request, open_file, write_to}, utils::{ferox_print, fmt_err, make_request, open_file, write_to},
CommandReceiver, CommandSender, Joiner, CommandReceiver, CommandSender, Joiner,
@@ -144,6 +143,9 @@ pub struct TermOutHandler {
/// pointer to "global" configuration struct /// pointer to "global" configuration struct
config: Arc<Configuration>, config: Arc<Configuration>,
/// handles instance
handles: Option<Arc<Handles>>,
} }
/// implementation of TermOutHandler /// implementation of TermOutHandler
@@ -161,6 +163,7 @@ impl TermOutHandler {
tx_file, tx_file,
file_task, file_task,
config, config,
handles: None,
} }
} }
@@ -212,6 +215,9 @@ impl TermOutHandler {
Command::Sync(sender) => { Command::Sync(sender) => {
sender.send(true).unwrap_or_default(); sender.send(true).unwrap_or_default();
} }
Command::AddHandles(handles) => {
self.handles = Some(handles);
}
Command::Exit => { Command::Exit => {
if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() { if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() {
self.file_task.as_mut().unwrap().await??; // wait for death self.file_task.as_mut().unwrap().await??; // wait for death
@@ -236,9 +242,26 @@ impl TermOutHandler {
log::trace!("enter: process_response({:?}, {:?})", resp, call_type); log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
async move { async move {
let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16()); let should_filter = self
.handles
.as_ref()
.unwrap()
.filters
.data
.should_filter_response(&resp, self.handles.as_ref().unwrap().stats.tx.clone());
let contains_sentry = if !self.config.filter_status.is_empty() {
// -C was used, meaning -s was not and we should ignore the defaults
// https://github.com/epi052/feroxbuster/issues/535
// -C indicates that we should filter that status code, but allow all others
!self.config.filter_status.contains(&resp.status().as_u16())
} else {
// -C wasn't used, so, we defer to checking the -s values
self.config.status_codes.contains(&resp.status().as_u16())
};
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
let should_process_response = contains_sentry && unknown_sentry; let should_process_response = contains_sentry && unknown_sentry && !should_filter;
if should_process_response { if should_process_response {
// print to stdout // print to stdout
@@ -284,7 +307,7 @@ impl TermOutHandler {
&& matches!(call_type, ProcessResponseCall::Recursive) && matches!(call_type, ProcessResponseCall::Recursive)
{ {
// --collect-backups was used; the response is one we care about, and the function // --collect-backups was used; the response is one we care about, and the function
// call came from the loop in `.start` (i.e. recursive was specified // call came from the loop in `.start` (i.e. recursive was specified)
let backup_urls = self.generate_backup_urls(&resp).await; let backup_urls = self.generate_backup_urls(&resp).await;
// need to manually adjust stats // need to manually adjust stats
@@ -398,6 +421,7 @@ impl TermOutHandler {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::event_handlers::Command;
#[test] #[test]
/// try to hit struct field coverage of FileOutHandler /// try to hit struct field coverage of FileOutHandler
@@ -417,12 +441,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>(); let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>(); let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap()); let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler { let toh = TermOutHandler {
config, config,
file_task: None, file_task: None,
receiver: rx, receiver: rx,
tx_file, tx_file,
handles: Some(handles),
}; };
println!("{:?}", toh); println!("{:?}", toh);
@@ -435,12 +461,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>(); let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>(); let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap()); let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler { let toh = TermOutHandler {
config, config,
file_task: None, file_task: None,
receiver: rx, receiver: rx,
tx_file, tx_file,
handles: Some(handles),
}; };
let expected: Vec<_> = vec![ let expected: Vec<_> = vec![
@@ -478,12 +506,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>(); let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>(); let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap()); let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler { let toh = TermOutHandler {
config, config,
file_task: None, file_task: None,
receiver: rx, receiver: rx,
tx_file, tx_file,
handles: Some(handles),
}; };
let expected: Vec<_> = vec![ let expected: Vec<_> = vec![
@@ -521,12 +551,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>(); let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>(); let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap()); let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler { let toh = TermOutHandler {
config, config,
file_task: None, file_task: None,
receiver: rx, receiver: rx,
tx_file, tx_file,
handles: Some(handles),
}; };
let expected: Vec<_> = vec![ let expected: Vec<_> = vec![

View File

@@ -368,8 +368,8 @@ impl ScanHandler {
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> { async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
log::trace!("enter: try_recursion({:?})", response,); log::trace!("enter: try_recursion({:?})", response,);
if !response.is_directory() { if !self.handles.config.force_recursion && !response.is_directory() {
// not a directory, quick exit // not a directory and --force-recursion wasn't used, quick exit
return Ok(()); return Ok(());
} }

View File

@@ -2,7 +2,6 @@ use super::*;
use crate::{ use crate::{
client, client,
event_handlers::{ event_handlers::{
Command,
Command::{AddError, AddToUsizeField}, Command::{AddError, AddToUsizeField},
Handles, Handles,
}, },
@@ -12,14 +11,13 @@ use crate::{
StatField::{LinksExtracted, TotalExpected}, StatField::{LinksExtracted, TotalExpected},
}, },
url::FeroxUrl, url::FeroxUrl,
utils::{logged_request, make_request, should_deny_url}, utils::{logged_request, make_request, send_try_recursion_command, should_deny_url},
ExtractionResult, DEFAULT_METHOD, ExtractionResult, DEFAULT_METHOD,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url}; use reqwest::{Client, StatusCode, Url};
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use std::collections::HashSet; use std::collections::HashSet;
use tokio::sync::oneshot;
/// Whether an active scan is recursive or not /// Whether an active scan is recursive or not
#[derive(Debug)] #[derive(Debug)]
@@ -186,11 +184,21 @@ impl<'a> Extractor<'a> {
resp.set_url(&format!("{}/", resp.url())); resp.set_url(&format!("{}/", resp.url()));
} }
self.handles if self.handles.config.filter_status.is_empty() {
.send_scan_command(Command::TryRecursion(Box::new(resp)))?; // -C wasn't used, so -s is the only 'filter' left to account for
let (tx, rx) = oneshot::channel::<bool>(); if self
self.handles.send_scan_command(Command::Sync(tx))?; .handles
rx.await?; .config
.status_codes
.contains(&resp.status().as_u16())
{
send_try_recursion_command(self.handles.clone(), resp).await?;
}
} else {
// -C was used, that means the filters above would have removed
// those responses, and anything else should be let through
send_try_recursion_command(self.handles.clone(), resp).await?;
}
} }
} }
log::trace!("exit: request_links"); log::trace!("exit: request_links");

View File

@@ -65,10 +65,17 @@ pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set /// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file. /// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
/// ///
/// defaults to kali's default install location: /// defaults to kali's default install location on linux:
/// - `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt` /// - `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt`
///
/// and to the current directory on windows
/// - `.\seclists\Discovery\Web-Content\raft-medium-directories.txt`
#[cfg(not(target_os = "windows"))]
pub const DEFAULT_WORDLIST: &str = pub const DEFAULT_WORDLIST: &str =
"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt"; "/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
#[cfg(target_os = "windows")]
pub const DEFAULT_WORDLIST: &str =
".\\SecLists\\Discovery\\Web-Content\\raft-medium-directories.txt";
/// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan /// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
pub(crate) const SLEEP_DURATION: u64 = 500; pub(crate) const SLEEP_DURATION: u64 = 500;

View File

@@ -22,7 +22,9 @@ use feroxbuster::{
banner::{Banner, UPDATE_URL}, banner::{Banner, UPDATE_URL},
config::{Configuration, OutputLevel}, config::{Configuration, OutputLevel},
event_handlers::{ event_handlers::{
Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist}, Command::{
AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist,
},
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler, FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
TermOutHandler, SCAN_COMPLETE, TermOutHandler, SCAN_COMPLETE,
}, },
@@ -220,6 +222,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone()); let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone());
handles.set_scan_handle(scan_handle); // must be done after Handles initialization handles.set_scan_handle(scan_handle); // must be done after Handles initialization
handles.output.send(AddHandles(handles.clone()))?;
filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler

View File

@@ -23,7 +23,7 @@ impl Document {
document.number_of_terms += processed.len(); document.number_of_terms += processed.len();
for normalized in processed { for normalized in processed {
if normalized.len() > 2 { if normalized.len() >= 2 {
document.add_term(&normalized) document.add_term(&normalized)
} }
} }

View File

@@ -38,19 +38,16 @@ fn normalize_case<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
} }
} }
/// remove ascii and some utf-8 punctuation characters from the given string /// replace ascii and some utf-8 punctuation characters with ' ' (space) in the given string
fn remove_punctuation(text: &str) -> String { fn remove_punctuation(text: &str) -> String {
// non-separator type chars can be replaced with an empty string, while separators are replaced
// with a space. This attempts to keep things like
// 'aboutblogfaqcontactpresstermslexicondisclosure' from happening
text.replace( text.replace(
[ [
'!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?', '!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?',
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '', '', '', '', '@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '', '', '', '', '/',
'', '—', '.',
], ],
"", " ",
) )
.replace(['/', '', '—', '.'], " ")
} }
/// remove stop words from the given string /// remove stop words from the given string
@@ -86,7 +83,10 @@ mod tests {
fn test_remove_punctuation() { fn test_remove_punctuation() {
let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n"; let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n";
// the `" \n"` is because of the things like / getting replaced with a space // the `" \n"` is because of the things like / getting replaced with a space
assert_eq!(remove_punctuation(tester), " \n"); assert_eq!(
remove_punctuation(tester),
" \n "
);
} }
#[test] #[test]
@@ -115,7 +115,7 @@ mod tests {
/// ensure preprocess /// ensure preprocess
fn test_preprocess_results() { fn test_preprocess_results() {
let tester = "WHY are Y'all YELLing?"; let tester = "WHY are Y'all YELLing?";
assert_eq!(&preprocess(tester), &["yall", "yelling"]); assert_eq!(&preprocess(tester), &["y", "all", "yelling"]);
} }
#[test] #[test]

View File

@@ -333,6 +333,7 @@ pub fn initialize() -> Command<'static> {
.multiple_values(true) .multiple_values(true)
.multiple_occurrences(true) .multiple_occurrences(true)
.use_value_delimiter(true) .use_value_delimiter(true)
.conflicts_with("status_codes")
.help_heading("Response filters") .help_heading("Response filters")
.help( .help(
"Filter out status codes (deny list) (ex: -C 200 -C 401)", "Filter out status codes (deny list) (ex: -C 200 -C 401)",
@@ -426,6 +427,12 @@ pub fn initialize() -> Command<'static> {
.takes_value(true) .takes_value(true)
.help_heading("Scan settings") .help_heading("Scan settings")
.help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"), .help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
).arg(
Arg::new("force_recursion")
.long("force-recursion")
.conflicts_with("no_recursion")
.help_heading("Scan settings")
.help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
).arg( ).arg(
Arg::new("extract_links") Arg::new("extract_links")
.short('e') .short('e')
@@ -668,7 +675,7 @@ EXAMPLES:
cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
Proxy traffic through Burp Proxy traffic through Burp
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080 ./feroxbuster -u http://127.1 --burp
Proxy traffic through a SOCKS proxy Proxy traffic through a SOCKS proxy
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050 ./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050

View File

@@ -279,7 +279,9 @@ impl FeroxResponse {
if handles if handles
.config .config
.status_codes .status_codes
.contains(&self.status().as_u16()) .contains(&self.status().as_u16()) // in -s list
// or -C was used, and -s should be all responses that aren't filtered
|| !handles.config.filter_status.is_empty()
{ {
// only add extensions to those responses that pass our checks; filtered out // only add extensions to those responses that pass our checks; filtered out
// status codes are handled by should_filter, but we need to still check against // status codes are handled by should_filter, but we need to still check against

View File

@@ -45,7 +45,7 @@ impl FeroxResponses {
pub fn contains(&self, other: &FeroxResponse) -> bool { pub fn contains(&self, other: &FeroxResponse) -> bool {
if let Ok(responses) = self.responses.read() { if let Ok(responses) = self.responses.read() {
for response in responses.iter() { for response in responses.iter() {
if response.url() == other.url() { if response.url() == other.url() && response.method() == other.method() {
return true; return true;
} }
} }

View File

@@ -452,6 +452,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""quiet":false"#, r#""quiet":false"#,
r#""auto_bail":false"#, r#""auto_bail":false"#,
r#""auto_tune":false"#, r#""auto_tune":false"#,
r#""force_recursion":false"#,
r#""json":false"#, r#""json":false"#,
r#""output":"""#, r#""output":"""#,
r#""debug_log":"""#, r#""debug_log":"""#,

View File

@@ -8,7 +8,7 @@ use anyhow::Result;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use leaky_bucket::LeakyBucket; use leaky_bucket::LeakyBucket;
use tokio::{ use tokio::{
sync::{oneshot, RwLock}, sync::RwLock,
time::{sleep, Duration}, time::{sleep, Duration},
}; };
@@ -16,7 +16,7 @@ use crate::{
atomic_load, atomic_store, atomic_load, atomic_store,
config::RequesterPolicy, config::RequesterPolicy,
event_handlers::{ event_handlers::{
Command::{self, AddError, SubtractFromUsizeField}, Command::{AddError, SubtractFromUsizeField},
Handles, Handles,
}, },
extractor::{ExtractionTarget, ExtractorBuilder}, extractor::{ExtractionTarget, ExtractorBuilder},
@@ -25,7 +25,7 @@ use crate::{
scan_manager::{FeroxScan, ScanStatus}, scan_manager::{FeroxScan, ScanStatus},
statistics::{StatError::Other, StatField::TotalExpected}, statistics::{StatError::Other, StatField::TotalExpected},
url::FeroxUrl, url::FeroxUrl,
utils::{logged_request, should_deny_url}, utils::{logged_request, send_try_recursion_command, should_deny_url},
HIGH_ERROR_RATIO, HIGH_ERROR_RATIO,
}; };
@@ -379,14 +379,14 @@ impl Requester {
.await; .await;
// do recursion if appropriate // do recursion if appropriate
if !self.handles.config.no_recursion { if !self.handles.config.no_recursion && !self.handles.config.force_recursion {
self.handles // to support --force-recursion, we want to limit recursive calls to only
.send_scan_command(Command::TryRecursion(Box::new( // 'found' assets. That means we need to either gate or delay the call.
ferox_response.clone(), //
)))?; // this branch will retain the 'old' behavior by checking that
let (tx, rx) = oneshot::channel::<bool>(); // --force-recursion isn't turned on
self.handles.send_scan_command(Command::Sync(tx))?; send_try_recursion_command(self.handles.clone(), ferox_response.clone())
rx.await?; .await?;
} }
// purposefully doing recursion before filtering. the thought process is that // purposefully doing recursion before filtering. the thought process is that
@@ -400,6 +400,33 @@ impl Requester {
continue; continue;
} }
if !self.handles.config.no_recursion && self.handles.config.force_recursion {
// in this branch, we're saying that both recursion AND force recursion
// are turned on. It comes after should_filter_response, so those cases
// are handled. Now we need to account for -s/-C options.
if self.handles.config.filter_status.is_empty() {
// -C wasn't used, so -s is the only 'filter' left to account for
if self
.handles
.config
.status_codes
.contains(&ferox_response.status().as_u16())
{
send_try_recursion_command(
self.handles.clone(),
ferox_response.clone(),
)
.await?;
}
} else {
// -C was used, that means the filters above would have removed
// those responses, and anything else should be let through
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
.await?;
}
}
if self.handles.config.collect_extensions { if self.handles.config.collect_extensions {
ferox_response.parse_extension(self.handles.clone())?; ferox_response.parse_extension(self.handles.clone())?;
} }
@@ -469,6 +496,7 @@ mod tests {
use crate::{ use crate::{
config::Configuration, config::Configuration,
config::OutputLevel, config::OutputLevel,
event_handlers::Command::AddStatus,
event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler}, event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler},
filters, filters,
scan_manager::{ScanOrder, ScanType}, scan_manager::{ScanOrder, ScanType},
@@ -509,10 +537,7 @@ mod tests {
/// helper to stay DRY /// helper to stay DRY
async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) { async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) {
for _ in 0..num_errors { for _ in 0..num_errors {
handles handles.stats.send(AddError(StatError::Other)).unwrap();
.stats
.send(Command::AddError(StatError::Other))
.unwrap();
scan.add_error(); scan.add_error();
} }
@@ -549,7 +574,7 @@ mod tests {
code: StatusCode, code: StatusCode,
) { ) {
for _ in 0..num_codes { for _ in 0..num_codes {
handles.stats.send(Command::AddStatus(code)).unwrap(); handles.stats.send(AddStatus(code)).unwrap();
if code == StatusCode::FORBIDDEN { if code == StatusCode::FORBIDDEN {
scan.add_403(); scan.add_403();
} else { } else {

View File

@@ -12,16 +12,17 @@ use std::{
time::Duration, time::Duration,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::{mpsc::UnboundedSender, oneshot};
use crate::config::Configuration;
use crate::{ use crate::{
config::Configuration,
config::OutputLevel, config::OutputLevel,
event_handlers::{ event_handlers::{
Command::{self, AddError, AddStatus}, Command::{self, AddError, AddStatus},
Handles, Handles,
}, },
progress::PROGRESS_PRINTER, progress::PROGRESS_PRINTER,
response::FeroxResponse,
send_command, send_command,
statistics::StatError::{Connection, Other, Redirection, Request, Timeout}, statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
traits::FeroxSerialize, traits::FeroxSerialize,
@@ -67,6 +68,20 @@ pub fn fmt_err(msg: &str) -> String {
format!("{}: {}", status_colorizer("ERROR"), msg) format!("{}: {}", status_colorizer("ERROR"), msg)
} }
/// given a FeroxResponse, send a TryRecursion command
///
/// moved to utils to allow for calls from extractor and scanner
pub(crate) async fn send_try_recursion_command(
handles: Arc<Handles>,
response: FeroxResponse,
) -> Result<()> {
handles.send_scan_command(Command::TryRecursion(Box::new(response.clone())))?;
let (tx, rx) = oneshot::channel::<bool>();
handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
Ok(())
}
/// Takes in a string and colors it using console::style /// Takes in a string and colors it using console::style
/// ///
/// mainly putting this here in case i want to change the color later, making any changes easy /// mainly putting this here in case i want to change the color later, making any changes easy
@@ -146,7 +161,7 @@ pub async fn make_request(
let mut request = client.request(Method::from_bytes(method.as_bytes())?, url.to_owned()); let mut request = client.request(Method::from_bytes(method.as_bytes())?, url.to_owned());
if (!config.proxy.is_empty() || config.replay_proxy.is_empty()) if (!config.proxy.is_empty() || !config.replay_proxy.is_empty())
&& data.is_none() && data.is_none()
&& ["post", "put", "patch"].contains(&method.to_ascii_lowercase().as_str()) && ["post", "put", "patch"].contains(&method.to_ascii_lowercase().as_str())
{ {

View File

@@ -784,7 +784,6 @@ fn banner_prints_filter_status() {
.and(predicate::str::contains("http://localhost")) .and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("Threads")) .and(predicate::str::contains("Threads"))
.and(predicate::str::contains("Wordlist")) .and(predicate::str::contains("Wordlist"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("Timeout (secs)")) .and(predicate::str::contains("Timeout (secs)"))
.and(predicate::str::contains("User-Agent")) .and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("Status Code Filters")) .and(predicate::str::contains("Status Code Filters"))
@@ -1394,3 +1393,30 @@ fn banner_prints_all_composite_settings_burp_replay() {
.and(predicate::str::contains("─┴─")), .and(predicate::str::contains("─┴─")),
); );
} }
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_force_recursion() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--force-recursion")
.arg("--wordlist")
.arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
.assert()
.success()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Target Url"))
.and(predicate::str::contains("http://localhost"))
.and(predicate::str::contains("Threads"))
.and(predicate::str::contains("Wordlist"))
.and(predicate::str::contains("Status Codes"))
.and(predicate::str::contains("Timeout (secs)"))
.and(predicate::str::contains("User-Agent"))
.and(predicate::str::contains("Force Recursion"))
.and(predicate::str::contains("─┴─")),
);
}

View File

@@ -269,51 +269,53 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
Ok(()) Ok(())
} }
#[test] // #[test]
/// test finds a static wildcard and reports as much to stdout // /// test finds a static wildcard and reports as much to stdout
fn heuristics_wildcard_test_with_two_static_wildcards() { // fn heuristics_wildcard_test_with_two_static_wildcards() {
let srv = MockServer::start(); // let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap(); // let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| { // let mock = srv.mock(|when, then| {
when.method(GET) // when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap()); // .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200) // then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}); // });
let mock2 = srv.mock(|when, then| { // let mock2 = srv.mock(|when, then| {
when.method(GET) // when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap()); // .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200) // then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}); // });
let cmd = Command::cargo_bin("feroxbuster") // let cmd = Command::cargo_bin("feroxbuster")
.unwrap() // .unwrap()
.arg("--url") // .arg("--url")
.arg(srv.url("/")) // .arg(srv.url("/"))
.arg("--wordlist") // .arg("--wordlist")
.arg(file.as_os_str()) // .arg(file.as_os_str())
.arg("--add-slash") // .arg("--add-slash")
.unwrap(); // .arg("--threads")
// .arg("1")
// .unwrap();
teardown_tmp_directory(tmp_dir); // teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout( // cmd.assert().success().stdout(
predicate::str::contains("WLD") // predicate::str::contains("WLD")
.and(predicate::str::contains("Got")) // .and(predicate::str::contains("Got"))
.and(predicate::str::contains("200")) // .and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)")) // .and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)")) // .and(predicate::str::contains("(url length: 96)"))
.and(predicate::str::contains( // .and(predicate::str::contains(
"Wildcard response is static; auto-filtering 46", // "Wildcard response is static; auto-filtering 46",
)), // )),
); // );
assert_eq!(mock.hits(), 1); // assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1); // assert_eq!(mock2.hits(), 1);
} // }
#[test] #[test]
/// test finds a static wildcard and reports nothing to stdout /// test finds a static wildcard and reports nothing to stdout
@@ -344,6 +346,8 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
.arg(file.as_os_str()) .arg(file.as_os_str())
.arg("--add-slash") .arg("--add-slash")
.arg("--silent") .arg("--silent")
.arg("--threads")
.arg("1")
.unwrap(); .unwrap();
teardown_tmp_directory(tmp_dir); teardown_tmp_directory(tmp_dir);
@@ -355,119 +359,126 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
Ok(()) Ok(())
} }
#[test] // #[test]
/// test finds a static wildcard and reports as much to stdout and a file // /// 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() { // fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
let srv = MockServer::start(); // let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap(); // let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile"); // let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| { // let mock = srv.mock(|when, then| {
when.method(GET) // when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap()); // .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200) // then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}); // });
let mock2 = srv.mock(|when, then| { // let mock2 = srv.mock(|when, then| {
when.method(GET) // when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap()); // .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200) // then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}); // });
let cmd = Command::cargo_bin("feroxbuster") // let cmd = Command::cargo_bin("feroxbuster")
.unwrap() // .unwrap()
.arg("--url") // .arg("--url")
.arg(srv.url("/")) // .arg(srv.url("/"))
.arg("--wordlist") // .arg("--wordlist")
.arg(file.as_os_str()) // .arg(file.as_os_str())
.arg("--add-slash") // .arg("--add-slash")
.arg("--output") // .arg("--output")
.arg(outfile.as_os_str()) // .arg(outfile.as_os_str())
.unwrap(); // .arg("--threads")
// .arg("1")
// .unwrap();
let contents = std::fs::read_to_string(outfile).unwrap(); // let contents = std::fs::read_to_string(outfile).unwrap();
teardown_tmp_directory(tmp_dir); // teardown_tmp_directory(tmp_dir);
assert!(contents.contains("WLD")); // assert!(contents.contains("WLD"));
assert!(contents.contains("Got")); // assert!(contents.contains("Got"));
assert!(contents.contains("200")); // assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)")); // assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)")); // assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout( // cmd.assert().success().stdout(
predicate::str::contains("WLD") // predicate::str::contains("WLD")
.and(predicate::str::contains("Got")) // .and(predicate::str::contains("Got"))
.and(predicate::str::contains("200")) // .and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)")) // .and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)")) // .and(predicate::str::contains("(url length: 96)"))
.and(predicate::str::contains( // .and(predicate::str::contains(
"Wildcard response is static; auto-filtering 46", // "Wildcard response is static; auto-filtering 46",
)), // )),
); // );
assert_eq!(mock.hits(), 1); // assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1); // assert_eq!(mock2.hits(), 1);
} // }
#[test] // #[test]
/// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as // /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
/// in the output file // /// in the output file
fn heuristics_wildcard_test_with_redirect_as_response_code( // fn heuristics_wildcard_test_with_redirect_as_response_code(
) -> Result<(), Box<dyn std::error::Error>> { // ) -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start(); // let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| { // let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
when.method(GET) // let outfile = tmp_dir.path().join("outfile");
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(301)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = srv.mock(|when, then| { // let mock = srv.mock(|when, then| {
when.method(GET) // when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap()); // .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(301) // then.status(301)
.header("Location", &srv.url("/some-redirect")) // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); // });
});
let cmd = Command::cargo_bin("feroxbuster") // let mock2 = srv.mock(|when, then| {
.unwrap() // when.method(GET)
.arg("--url") // .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
.arg(srv.url("/")) // then.status(301)
.arg("--wordlist") // .header("Location", &srv.url("/some-redirect"))
.arg(file.as_os_str()) // .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
.arg("--add-slash") // });
.arg("--output")
.arg(outfile.as_os_str())
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap(); // let cmd = Command::cargo_bin("feroxbuster")
// .unwrap()
// .arg("--url")
// .arg(srv.url("/"))
// .arg("--wordlist")
// .arg(file.as_os_str())
// .arg("--add-slash")
// .arg("--output")
// .arg(outfile.as_os_str())
// .arg("--threads")
// .arg("1")
// .unwrap();
teardown_tmp_directory(tmp_dir); // let contents = std::fs::read_to_string(outfile).unwrap();
assert!(contents.contains("WLD")); // teardown_tmp_directory(tmp_dir);
assert!(contents.contains("301"));
assert!(contents.contains("/some-redirect"));
assert!(contents.contains(" => "));
assert!(contents.contains(&srv.url("/")));
assert!(contents.contains("(url length: 32)"));
cmd.assert().success().stdout( // assert!(contents.contains("WLD"));
predicate::str::contains(" => ") // assert!(contents.contains("301"));
.and(predicate::str::contains("/some-redirect")) // assert!(contents.contains("/some-redirect"));
.and(predicate::str::contains("301")) // assert!(contents.contains(" => "));
.and(predicate::str::contains(srv.url("/"))) // assert!(contents.contains(&srv.url("/")));
.and(predicate::str::contains("(url length: 32)")) // assert!(contents.contains("(url length: 32)"));
.and(predicate::str::contains("WLD")),
);
assert_eq!(mock.hits(), 1); // cmd.assert().success().stdout(
assert_eq!(mock2.hits(), 1); // predicate::str::contains(" => ")
Ok(()) // .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.hits(), 1);
// assert_eq!(mock2.hits(), 1);
// Ok(())
// }
// todo figure out why ci hates these tests

View File

@@ -852,3 +852,62 @@ fn collect_words_makes_appropriate_requests() {
teardown_tmp_directory(tmp_dir); teardown_tmp_directory(tmp_dir);
} }
#[test]
/// send a request to an endpoint that has abnormal redirect logic, ala fast-api
fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dyn std::error::Error>>
{
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let mock1 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(301)
.body("this is a test")
.header("Location", &srv.url("/LICENSE"));
});
let mock2 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE/LICENSE");
then.status(404);
});
let mock3 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE/LICENSE/LICENSE");
then.status(404);
});
let mock4 = srv.mock(|when, then| {
when.method(GET).path("/LICENSE/LICENSE/LICENSE/LICENSE");
then.status(404);
});
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("--force-recursion")
.arg("-o")
.arg(outfile.as_os_str())
.unwrap();
let contents = std::fs::read_to_string(outfile)?;
println!("{}", contents);
assert!(contents.contains("/LICENSE"));
assert!(contents.contains("301"));
assert!(contents.contains("14"));
assert_eq!(mock1.hits(), 2);
assert_eq!(mock2.hits(), 1);
assert_eq!(mock3.hits(), 0);
assert_eq!(mock4.hits(), 0);
teardown_tmp_directory(tmp_dir);
Ok(())
}