Compare commits

...

242 Commits

Author SHA1 Message Date
epi
e0f9a30ba9 allow CD workflow for testing 2023-03-09 06:06:42 -06:00
epi
867da048b4 allow CD workflow for testing 2023-03-09 06:06:03 -06:00
epi
31e66c1fa0 allow CD workflow for testing 2023-03-09 06:04:13 -06:00
Aan
703da383a7 Fix for fmt, clippy and nextest 2023-03-09 11:28:11 +07:00
Aan
aa83e40c4f Update README.md 2023-03-09 10:55:09 +07:00
Aan
a77c436e04 New feature checklist 2023-03-09 10:49:25 +07:00
Aan
c3455d123e Implement auto update feature 2023-03-09 10:06:17 +07:00
epi
2d381e7e05 added logo for chocolatey packaging 2023-03-08 06:20:37 -06:00
epi
7d26f368f5 Merge pull request #808 from epi052/fix-wildcard-directory-redirect-v2
Fix wildcard directory redirect v2
2023-03-08 06:14:27 -06:00
epi
36970896ca Merge pull request #810 from epi052/all-contributors/add-aancw
docs: add aancw as a contributor for code, and infra
2023-03-08 05:54:56 -06:00
epi
39a75f0608 Merge pull request #804 from aancw/scanmanager-banner
Showing banner again after finish scan management menu
2023-03-08 05:54:26 -06:00
allcontributors[bot]
ab8537beeb docs: update .all-contributorsrc [skip ci] 2023-03-08 11:54:12 +00:00
allcontributors[bot]
9e907d37d5 docs: update README.md [skip ci] 2023-03-08 11:54:11 +00:00
epi
19e0a7f48b Merge branch 'main' into fix-wildcard-directory-redirect-v2 2023-03-08 05:50:29 -06:00
epi
5e93da0a65 fixed #809; thorough/smart bypassed mutual exclusion 2023-03-08 05:29:30 -06:00
Aan
2704e33178 Update the code as requested in suggestion 2023-03-08 14:47:39 +07:00
epi
8392f6d26b fixed menu filter display; fixed wildcard filter comparison 2023-03-07 21:14:20 -06:00
epi
ca43a767d2 fixed failing test 2023-03-07 20:15:10 -06:00
epi
291ccedba3 clippy 2023-03-07 18:54:32 -06:00
epi
6d01bc8ec4 added a few tests taht were removed previously 2023-03-07 18:38:10 -06:00
epi
94aafccf8a bumped version 2023-03-07 06:30:53 -06:00
epi
8dd8871ae5 old tests pass 2023-03-07 06:27:24 -06:00
epi
ad0df8ccd3 updated deps 2023-03-07 06:00:00 -06:00
epi
31cdba64e4 fmt 2023-03-06 20:44:24 -06:00
epi
584fc940cd implemented fix for wildcard directories 2023-03-06 20:44:14 -06:00
Aan
43116f9aab Showing banner again after finish scan management menu 2023-03-06 19:49:50 +07:00
Aan
aec083ea58 Showing banner again after finish scan management menu 2023-03-06 19:47:33 +07:00
epi
52d08e504d Merge pull request #801 from epi052/all-contributors/add-Luoooio
docs: add Luoooio as a contributor for ideas
2023-02-28 15:55:12 -06:00
allcontributors[bot]
a254574ce7 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:55:03 +00:00
allcontributors[bot]
6cb7c8e342 docs: update README.md [skip ci] 2023-02-28 21:55:02 +00:00
epi
98670f367f Merge pull request #800 from epi052/all-contributors/add-xaeroborg
docs: add xaeroborg as a contributor for ideas
2023-02-28 15:53:42 -06:00
allcontributors[bot]
68913c9950 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:53:34 +00:00
allcontributors[bot]
5901c75187 docs: update README.md [skip ci] 2023-02-28 21:53:33 +00:00
epi
8499901bfe Merge pull request #799 from epi052/all-contributors/add-pich4ya
docs: add pich4ya as a contributor for ideas
2023-02-28 15:52:00 -06:00
allcontributors[bot]
69dcb38360 docs: update .all-contributorsrc [skip ci] 2023-02-28 21:51:46 +00:00
allcontributors[bot]
eb8b70668d docs: update README.md [skip ci] 2023-02-28 21:51:45 +00:00
epi
f0702794b0 Merge pull request #796 from epi052/751-resume-scan-with-offset
resume scan starts from offset in wordlist
2023-02-28 06:32:17 -06:00
epi
367dcdbd72 fixed hanging test 2023-02-28 06:06:53 -06:00
epi
4b7a25c13b fixed ordering of dir bars / overall bar; fixed overall offset when resuming 2023-02-27 19:25:17 -06:00
epi
339189ff13 resume scan starts from offset in wordlist 2023-02-27 07:26:59 -06:00
epi
ed701c13b0 Merge pull request #794 from epi052/784-content-based-auto-filtering
Content-based auto filtering
2023-02-26 19:50:41 -06:00
epi
e034734df4 Merge branch 'main' into 784-content-based-auto-filtering 2023-02-26 13:21:55 -06:00
epi
73e2558404 fmt 2023-02-26 13:05:37 -06:00
epi
eb7ad68c01 all tests passing 2023-02-26 12:48:02 -06:00
epi
c61688f984 fmt 2023-02-26 07:31:03 -06:00
epi
6c96589ca5 fixed some tests 2023-02-26 07:30:41 -06:00
epi
0437c2baac updated deps harder 2023-02-26 07:30:28 -06:00
epi
0d689942eb updated deps 2023-02-26 06:54:23 -06:00
epi
74a1a8d597 bumped version to 2.8.0 2023-02-26 06:50:31 -06:00
epi
729d88a724 clippy 2023-02-26 06:49:48 -06:00
epi
ad38b56473 finalized new detections; removed wildcard filter and supporting code 2023-02-26 06:39:03 -06:00
epi
655364d9bd removed wildcard test, integrated into 404 detection 2023-02-25 20:58:28 -06:00
epi
ac7f59cd3f updated default status codes to all; adjusted banner entry 2023-02-25 07:28:27 -06:00
epi
0d64d28fe6 removed cruft 2023-02-25 06:28:14 -06:00
epi
89c29600c7 removed cruft 2023-02-25 06:23:39 -06:00
epi
96375e7734 added minhash algo when resp too short for ssdeep 2023-02-25 06:20:30 -06:00
epi
3531b8c74b added gaoya dependency for minhash algo 2023-02-25 06:20:08 -06:00
epi
e8f4438a52 fixed bug in dynamic wildcards; reorded 404-like id strat 2023-02-24 20:09:29 -06:00
epi
02b25dc553 incremental save for testing 2023-02-23 17:21:48 -06:00
epi
551cf065f3 Merge pull request #793 from epi052/all-contributors/add-f3rn0s
docs: add f3rn0s as a contributor for bug
2023-02-16 20:30:41 -06:00
allcontributors[bot]
c81885cf5e docs: update .all-contributorsrc [skip ci] 2023-02-17 02:30:28 +00:00
allcontributors[bot]
6a3d250e3b docs: update README.md [skip ci] 2023-02-17 02:30:27 +00:00
epi
259fbcca74 Merge pull request #790 from epi052/all-contributors/add-joaociocca
docs: add joaociocca as a contributor for bug, and ideas
2023-02-15 20:50:31 -06:00
allcontributors[bot]
f3c9f8ed20 docs: update .all-contributorsrc [skip ci] 2023-02-16 02:49:52 +00:00
allcontributors[bot]
be400ce971 docs: update README.md [skip ci] 2023-02-16 02:49:51 +00:00
epi
b62c76bce3 updated deps 2023-02-15 20:44:07 -06:00
epi
990a471d71 Merge pull request #779 from epi052/fix-some-visuals
Fix some visuals; update deps
2023-02-15 20:39:58 -06:00
epi
ec47d6f934 updated deps 2023-02-15 20:31:54 -06:00
epi
da509bd208 clippy 2023-02-15 19:39:27 -06:00
epi
8568b340a9 clippy 2023-02-15 19:38:23 -06:00
epi
7c9d8f529d tweaked auto-tune behavior to more aggressively move upward 2023-02-15 19:36:16 -06:00
epi
6d47b4b68b fixed a case where the --dont-filter message wasnt shown 2023-02-15 07:04:48 -06:00
epi
be3290572e fallback to body.len when content-length header missing 2023-02-15 06:40:35 -06:00
epi
4f13fd7974 Merge branch 'fix-some-visuals' of github.com:epi052/feroxbuster into fix-some-visuals 2023-02-07 05:33:50 -06:00
epi
57b8117015 fixed stale file reference 2023-02-05 20:33:53 -06:00
epi
7b4900fa07 caught a comparison bug 2023-02-01 18:51:29 -06:00
epi
d1e47b0025 added messaging about state of auto-tune/bail 2023-01-30 16:46:29 -06:00
epi
98612e2256 changed auto-tune emoji to align across different terminals 2023-01-30 16:46:03 -06:00
epi
f08023b813 Update README.md 2023-01-13 05:53:18 -06:00
epi
98254e3cac Update README.md 2023-01-13 05:52:55 -06:00
epi
46cc64325f Update README.md 2023-01-13 05:51:02 -06:00
epi
fc034f0720 Update README.md 2023-01-13 05:49:51 -06:00
epi
ef4a597cb1 Update README.md 2023-01-13 05:48:56 -06:00
epi
bb6b12d168 Merge branch 'main' of github.com:epi052/feroxbuster 2023-01-13 05:32:53 -06:00
epi
21a9de2d39 fixed code coverage workflow 2023-01-13 05:32:48 -06:00
epi
7d8f3b0305 Merge pull request #764 from epi052/all-contributors/add-aidanhall34
docs: add aidanhall34 as a contributor for code, and infra
2023-01-13 05:13:39 -06:00
allcontributors[bot]
6b3fe48b4f docs: update .all-contributorsrc [skip ci] 2023-01-13 11:12:49 +00:00
allcontributors[bot]
7a79000d96 docs: update README.md [skip ci] 2023-01-13 11:12:48 +00:00
epi
d164034d3e Merge pull request #762 from aidanhall34/NA-dockerfile
Fixes #761 | Updated Dockerfile and CONTRIBUTING docs
2023-01-13 05:12:25 -06:00
aidan.hall34
e4dc7da756 Remove edge branch, update alpine 2023-01-12 23:34:03 +00:00
aidan.hall34
6090cefa4f Updated Dockerfile and CONTRIBUTING docs 2023-01-12 14:16:58 +00:00
epi
ec05644854 Merge pull request #753 from epi052/all-contributors/add-duokebei
docs: add duokebei as a contributor for ideas
2022-12-29 20:31:42 -06:00
allcontributors[bot]
567f927884 docs: update .all-contributorsrc [skip ci] 2022-12-30 02:31:17 +00:00
allcontributors[bot]
176a6a6426 docs: update README.md [skip ci] 2022-12-30 02:31:16 +00:00
epi
c99f6146e3 Update README.md 2022-12-29 20:29:56 -06:00
epi
fb34817509 Merge pull request #752 from epi052/all-contributors/add-hakdogpinas
docs: add hakdogpinas as a contributor for ideas
2022-12-29 20:29:26 -06:00
epi
0c8e5d51f0 Update .all-contributorsrc 2022-12-29 20:27:05 -06:00
allcontributors[bot]
ac24e507ac docs: update .all-contributorsrc [skip ci] 2022-12-30 02:24:54 +00:00
allcontributors[bot]
808c749f63 docs: update README.md [skip ci] 2022-12-30 02:24:53 +00:00
epi
b1f5ed507b Merge pull request #750 from epi052/742-748-state-file-bug-fixes
multiple bug fixes / small improvements
2022-12-29 19:57:23 -06:00
epi
79edc42b17 bumped version to 2.7.3 2022-12-29 15:56:56 -06:00
epi
1b223b0867 fixed #716; wordlist entries with leading slash are trimmed 2022-12-29 15:55:50 -06:00
epi
0c6d5193a9 fixed #743; redirects always show full url as Location 2022-12-29 15:43:57 -06:00
epi
c637355796 clippy 2022-12-29 15:32:22 -06:00
epi
a114cc8f85 fixed #748; cancelled scans persist across ctrl+c 2022-12-29 15:28:58 -06:00
epi
c8503faf02 updated dependencies 2022-12-29 07:03:03 -06:00
epi
cbbd642510 Merge pull request #749 from n0kovo/main
Fix incorrect username in Contributors
2022-12-29 06:59:34 -06:00
n0kovo
2c8e9bace9 Fix incorrect username in Contributors 2022-12-29 13:37:41 +01:00
epi
f4fe8c0544 Merge pull request #734 from epi052/all-contributors/add-kmanc
docs: add kmanc as a contributor for bug, and code
2022-12-14 06:09:50 -06:00
allcontributors[bot]
73109483fe docs: update .all-contributorsrc [skip ci] 2022-12-14 12:09:23 +00:00
allcontributors[bot]
aee33012b1 docs: update README.md [skip ci] 2022-12-14 12:09:22 +00:00
epi
eab95e0435 Merge pull request #733 from kmanc/bugfix-no-state-with-time-limit
FIX 732 ensure --no-state is respected even through --time-limit
2022-12-14 06:08:50 -06:00
koins
acb2f42f69 ensure --no-state is respected even through --time-limit 2022-12-13 22:37:27 -08:00
epi
ac20b213ec Merge branch 'main' of github.com:epi052/feroxbuster 2022-11-16 16:53:02 -06:00
epi
201873d7ac bumped version to 2.7.2 2022-11-16 16:52:35 -06:00
epi
9678b8f31c Merge pull request #708 from epi052/all-contributors/add-udoprog
docs: add udoprog as a contributor for code
2022-11-16 16:41:55 -06:00
allcontributors[bot]
20a826fc0f docs: update .all-contributorsrc [skip ci] 2022-11-16 22:41:13 +00:00
allcontributors[bot]
56b78a4e04 docs: update README.md [skip ci] 2022-11-16 22:41:12 +00:00
epi
4b6bf3645d bumped version to 2.7.2 2022-11-16 16:35:34 -06:00
epi
6fd201b717 clippy 2022-11-16 16:21:32 -06:00
epi
5f39d71fe8 updated deps; clippy 2022-11-16 16:20:09 -06:00
epi
c23850208b added link tag to html extraction 2022-11-16 16:01:01 -06:00
epi
d5605efb08 Merge pull request #706 from epi052/689-invalid-uri-during-extraction
fixed invalid uri exception during extraction
2022-11-16 07:44:48 -06:00
epi
5b8d3f5661 removed cruft 2022-11-16 07:42:09 -06:00
epi
ce7f3b79b8 fixed invalid uri exception during extraction 2022-11-16 07:09:02 -06:00
epi
c9c63bebd0 Merge pull request #672 from epi052/661-fix-double-dir-scan
661 fix double dir scan
2022-10-05 05:32:09 -05:00
epi
1f60e06247 turned off builds for all but main 2022-10-05 05:30:00 -05:00
epi
04e3ad69cc allowing a test build to happen 2022-10-04 07:09:40 -05:00
epi
fd5b1f6f25 refined the fix; updated tests and serialization 2022-10-04 07:07:24 -05:00
epi
a9dde3f7e1 normalized directory scan input + search in feroxscans 2022-10-04 05:45:34 -05:00
epi
7a9ee39941 Merge pull request #671 from epi052/update-clap-major
updated clap from 3.x to 4.x
2022-10-03 05:27:10 -05:00
epi
6befae1a93 updated clap from 3.x to 4.x 2022-10-03 05:16:50 -05:00
epi
e6b422b92a Merge pull request #670 from epi052/update-console
updated deps
2022-10-01 13:22:54 -05:00
epi
fb4bfa27fd updated deps 2022-10-01 13:21:41 -05:00
epi
2795ec4e72 fixed typo; closes #660 2022-09-27 06:21:51 -05:00
epi
e9d283bc59 Merge pull request #655 from epi052/update-deps
Update deps
2022-09-20 04:53:36 -05:00
epi
3a128df2fc clippy 2022-09-20 04:52:11 -05:00
epi
38f1b917c4 clippy 2022-09-20 04:49:58 -05:00
epi
afa7d6804c clippy 2022-09-18 05:51:13 -05:00
epi
28c3e25eeb updated deps 2022-09-18 05:50:39 -05:00
epi
55e22467ce clippy issues 2022-09-18 05:50:15 -05:00
epi
bbfaddaedd fixed #644; methods respected from config 2022-09-06 08:02:30 -05:00
epi
53e3420efd updated deps 2022-07-10 16:44:16 -05:00
epi
d390bbc12d Merge branch 'leakybucket-update' 2022-07-10 16:33:22 -05:00
epi
0c3b91855a Merge pull request #604 from udoprog/bump-leaky-bucket
Bump leaky-bucket to 0.12.1
2022-07-10 16:33:07 -05:00
epi
48f5362f5f appeased clippy 2022-07-10 16:30:52 -05:00
John-John Tedro
24514faf9e Bump leaky-bucket to 0.12.1 2022-07-10 18:20:48 +02:00
epi
a424057166 Merge pull request #581 from epi052/all-contributors/add-herrcykel
docs: add herrcykel as a contributor for code
2022-05-17 18:42:47 -05:00
epi
7d483b6edd Merge pull request #580 from herrcykel/patch-1
Remove superfluous if statement
2022-05-17 18:42:19 -05:00
allcontributors[bot]
1a717e878d docs: update .all-contributorsrc [skip ci] 2022-05-17 23:41:59 +00:00
allcontributors[bot]
e35a6dda9f docs: update README.md [skip ci] 2022-05-17 23:41:58 +00:00
O
f3b2193b2f Remove superfluous if statement 2022-05-17 23:31:29 +02:00
epi
07a7ac652e updated deps 2022-05-12 06:15:17 -05:00
epi
f51993cde0 Merge pull request #575 from epi052/all-contributors/add-postmodern
docs: add postmodern as a contributor for ideas
2022-05-12 06:03:16 -05:00
allcontributors[bot]
9093ffb92a docs: update .all-contributorsrc [skip ci] 2022-05-12 11:03:09 +00:00
allcontributors[bot]
d550448229 docs: update README.md [skip ci] 2022-05-12 11:03:08 +00:00
epi
492665154e Merge pull request #574 from epi052/all-contributors/add-DonatoReis
docs: add DonatoReis as a contributor for ideas
2022-05-12 06:02:21 -05:00
allcontributors[bot]
c14e617076 docs: update .all-contributorsrc [skip ci] 2022-05-12 11:02:03 +00:00
allcontributors[bot]
6bb748af17 docs: update README.md [skip ci] 2022-05-12 11:02:03 +00:00
epi
863ea089cc Merge pull request #573 from epi052/all-contributors/add-jhaddix
docs: add jhaddix as a contributor for bug
2022-05-12 06:01:24 -05:00
allcontributors[bot]
ad3091a7db docs: update .all-contributorsrc [skip ci] 2022-05-12 11:00:30 +00:00
allcontributors[bot]
b2bdea71dd docs: update README.md [skip ci] 2022-05-12 11:00:30 +00:00
epi
f478700b86 Merge pull request #564 from epi052/563-fix-leaky-bucket-unwrap-to-none
563 fix leaky bucket unwrap to none
2022-05-11 20:09:46 -05:00
epi
1f2aad5e52 updated deps 2022-05-11 17:29:59 -05:00
epi
3a6a61cc24 updated dependencies 2022-05-11 17:24:09 -05:00
epi
0311a846b3 added secondary wordlist check to main 2022-05-11 17:14:13 -05:00
epi
3066efa848 add https if missing url scheme; check /usr/local/share for wordlist 2022-05-10 06:45:10 -05:00
epi
a8fae65d63 allow extensions with prepended . 2022-05-10 06:44:21 -05:00
epi
970886a68b reverted actions change 2022-05-10 05:51:03 -05:00
epi
494eed81e8 fmt 2022-05-05 19:19:01 -05:00
epi
c8a577b1e7 removed unwrap from limit function 2022-05-05 19:18:29 -05:00
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
76 changed files with 3309 additions and 1783 deletions

View File

@@ -294,10 +294,10 @@
]
},
{
"login": "narkopolo",
"name": "narkopolo",
"login": "n0kovo",
"name": "n0kovo",
"avatar_url": "https://avatars.githubusercontent.com/u/16690056?v=4",
"profile": "https://github.com/narkopolo",
"profile": "https://github.com/n0kovo",
"contributions": [
"ideas"
]
@@ -391,8 +391,167 @@
"avatar_url": "https://avatars.githubusercontent.com/u/3488554?v=4",
"profile": "https://twitter.com/Jhaddix",
"contributions": [
"ideas",
"bug"
]
},
{
"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"
]
},
{
"login": "DonatoReis",
"name": "PeakyBlinder",
"avatar_url": "https://avatars.githubusercontent.com/u/93531354?v=4",
"profile": "https://github.com/DonatoReis",
"contributions": [
"ideas"
]
},
{
"login": "postmodern",
"name": "Postmodern",
"avatar_url": "https://avatars.githubusercontent.com/u/12671?v=4",
"profile": "https://postmodern.github.io/",
"contributions": [
"ideas"
]
},
{
"login": "herrcykel",
"name": "O",
"avatar_url": "https://avatars.githubusercontent.com/u/1936757?v=4",
"profile": "https://github.com/herrcykel",
"contributions": [
"code"
]
},
{
"login": "udoprog",
"name": "John-John Tedro",
"avatar_url": "https://avatars.githubusercontent.com/u/111092?v=4",
"profile": "http://udoprog.github.io/",
"contributions": [
"code"
]
},
{
"login": "kmanc",
"name": "kmanc",
"avatar_url": "https://avatars.githubusercontent.com/u/14863147?v=4",
"profile": "https://github.com/kmanc",
"contributions": [
"bug",
"code"
]
},
{
"login": "hakdogpinas",
"name": "hakdogpinas",
"avatar_url": "https://avatars.githubusercontent.com/u/71529469?v=4",
"profile": "https://github.com/hakdogpinas",
"contributions": [
"ideas"
]
},
{
"login": "duokebei",
"name": "多可悲",
"avatar_url": "https://avatars.githubusercontent.com/u/75022552?v=4",
"profile": "https://github.com/duokebei",
"contributions": [
"ideas"
]
},
{
"login": "aidanhall34",
"name": "Aidan Hall",
"avatar_url": "https://avatars.githubusercontent.com/u/58670593?v=4",
"profile": "https://blog.ah34.net/",
"contributions": [
"code",
"infra"
]
},
{
"login": "joaociocca",
"name": "João Ciocca",
"avatar_url": "https://avatars.githubusercontent.com/u/6473725?v=4",
"profile": "https://hachyderm.io/@JohnnyCiocca",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "f3rn0s",
"name": "f3rn0s",
"avatar_url": "https://avatars.githubusercontent.com/u/1351279?v=4",
"profile": "https://github.com/f3rn0s",
"contributions": [
"bug"
]
},
{
"login": "pich4ya",
"name": "LongCat",
"avatar_url": "https://avatars.githubusercontent.com/u/2099767?v=4",
"profile": "https://sth.sh",
"contributions": [
"ideas"
]
},
{
"login": "xaeroborg",
"name": "xaeroborg",
"avatar_url": "https://avatars.githubusercontent.com/u/33274680?v=4",
"profile": "https://github.com/xaeroborg",
"contributions": [
"ideas"
]
},
{
"login": "Luoooio",
"name": "Luoooio",
"avatar_url": "https://avatars.githubusercontent.com/u/26653157?v=4",
"profile": "https://github.com/Luoooio",
"contributions": [
"ideas"
]
},
{
"login": "aancw",
"name": "Aan",
"avatar_url": "https://avatars.githubusercontent.com/u/6284204?v=4",
"profile": "https://petruknisme.com",
"contributions": [
"code",
"infra"
]
}
],
"contributorsPerLine": 7,
@@ -400,5 +559,6 @@
"projectOwner": "epi052",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
"skipCi": true,
"commitConvention": "angular"
}

View File

@@ -73,24 +73,28 @@ jobs:
name: ${{ matrix.name }}.tar.gz
path: ${{ matrix.name }}.tar.gz
build-deb:
needs: [build-nix]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Deb Build
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0
- name: Upload Deb Artifact
uses: actions/upload-artifact@v2
with:
name: feroxbuster_amd64.deb
path: ./target/x86_64-unknown-linux-musl/debian/*
# build-deb:
# needs: [build-nix]
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@master
# - name: Install cargo-deb
# run: cargo install -f cargo-deb
# - name: Install musl toolchain
# run: rustup target add x86_64-unknown-linux-musl
# - name: Deb Build
# run: cargo deb --target=x86_64-unknown-linux-musl
# - name: Upload Deb Artifact
# uses: actions/upload-artifact@v2
# with:
# name: feroxbuster_amd64.deb
# path: ./target/x86_64-unknown-linux-musl/debian/*
build-macos:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
# if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@@ -122,7 +126,7 @@ jobs:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/main'
# if: github.ref == 'refs/heads/main'
strategy:
matrix:
type: [windows-x64, windows-x86]

View File

@@ -8,11 +8,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
@@ -22,26 +17,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
command: nextest
args: run --all-features --all-targets --retries 10
fmt:
name: Rust fmt
runs-on: ubuntu-latest
steps:
- 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
with:
command: fmt
@@ -52,13 +40,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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
with:
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
jobs:
upload-coverage:
coverage:
name: LLVM Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: nightly
override: true
- uses: actions-rs/cargo@v1
components: llvm-tools-preview
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@v2
with:
command: clean
- uses: actions-rs/cargo@v1
tool: cargo-nextest,cargo-llvm-cov
- name: Generate code coverage
run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
command: test
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:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
name: codecov-umbrella
files: lcov.info
fail_ci_if_error: true
- uses: actions/upload-artifact@v2
with:
name: coverage.xml
path: ./coverage.xml

View File

@@ -76,35 +76,35 @@ Now that you have a copy of your fork, there is work you will need to do to keep
Do this prior to every time you create a branch for a PR:
1. Make sure you are on the `master` branch
1. Make sure you are on the `main` branch
> ```sh
> $ git status
> On branch master
> Your branch is up-to-date with 'origin/master'.
> On branch main
> Your branch is up-to-date with 'origin/main'.
> ```
> If your aren't on `master`, resolve outstanding files and commits and checkout the `master` branch
> If your aren't on `main`, resolve outstanding files and commits and checkout the `main` branch
> ```sh
> $ git checkout master
> $ git checkout main
> ```
2. Do a pull with rebase against `upstream`
> ```sh
> $ git pull --rebase upstream master
> $ git pull --rebase upstream main
> ```
> This will pull down all of the changes to the official master branch, without making an additional commit in your local repo.
> This will pull down all of the changes to the official main branch, without making an additional commit in your local repo.
3. (_Optional_) Force push your updated master branch to your GitHub fork
3. (_Optional_) Force push your updated main branch to your GitHub fork
> ```sh
> $ git push origin master --force
> $ git push origin main --force
> ```
> This will overwrite the master branch of your fork.
> This will overwrite the main branch of your fork.
### Creating a branch
@@ -214,20 +214,20 @@ GitHub has a good guide on how to contribute to open source [here](https://opens
##### Editing via your local fork
1. Perform the maintenance step of rebasing `master`
2. Ensure you're on the `master` branch using `git status`:
1. Perform the maintenance step of rebasing `main`
2. Ensure you're on the `main` branch using `git status`:
```sh
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
On branch main
Your branch is up-to-date with 'origin/main'.
nothing to commit, working directory clean
```
1. If you're not on master or your working directory is not clean, resolve
any outstanding files/commits and checkout master `git checkout master`
2. Create a branch off of `master` with git: `git checkout -B
1. If you're not on main or your working directory is not clean, resolve
any outstanding files/commits and checkout main `git checkout main`
2. Create a branch off of `main` with git: `git checkout -B
branch/name-here`
3. Edit your file(s) locally with the editor of your choice
4. Check your `git status` to see unstaged files
@@ -239,8 +239,8 @@ nothing to commit, working directory clean
8. Push your commits to your GitHub Fork: `git push -u origin branch/name-here`
9. Once the edits have been committed, you will be prompted to create a pull
request on your fork's GitHub page
10. By default, all pull requests should be against the `master` branch
11. Submit a pull request from your branch to feroxbuster's `master` branch
10. By default, all pull requests should be against the `main` branch
11. Submit a pull request from your branch to feroxbuster's `main` branch
12. The title (also called the subject) of your PR should be descriptive of your
changes and succinctly indicate what is being fixed
- Examples: `Add test cases for Unicode support`; `Correct typo in overview documentation`

1541
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.6.3"
version = "2.10.0"
authors = ["Ben 'epi' Risher (@epi052)"]
license = "MIT"
edition = "2021"
@@ -8,7 +8,13 @@ homepage = "https://github.com/epi052/feroxbuster"
repository = "https://github.com/epi052/feroxbuster"
description = "A fast, simple, recursive content discovery tool."
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"]
build = "build.rs"
@@ -16,40 +22,41 @@ build = "build.rs"
maintenance = { status = "actively-developed" }
[build-dependencies]
clap = { version = "3.1.8", features = ["wrap_help", "cargo"] }
clap_complete = "3.1.1"
clap = { version = "4.1.8", features = ["wrap_help", "cargo"] }
clap_complete = "4.1.4"
regex = "1.5.5"
lazy_static = "1.4.0"
dirs = "4.0.0"
[dependencies]
scraper = "0.12.0"
futures = "0.3.21"
tokio = { version = "1.17.0", features = ["full"] }
tokio-util = { version = "0.7.1", features = ["codec"] }
log = "0.4.16"
env_logger = "0.9.0"
scraper = "0.15.0"
futures = "0.3.26"
tokio = { version = "1.26.0", features = ["full"] }
tokio-util = { version = "0.7.7", features = ["codec"] }
log = "0.4.17"
env_logger = "0.10.0"
reqwest = { version = "0.11.10", features = ["socks"] }
# uses feature unification to add 'serde' to reqwest::Url
url = { version = "2.2.2", features = ["serde"] }
serde_regex = "1.1.0"
clap = { version = "3.1.8", features = ["wrap_help", "cargo"] }
clap = { version = "4.1.8", features = ["wrap_help", "cargo"] }
lazy_static = "1.4.0"
toml = "0.5.8"
serde = { version = "1.0.136", features = ["derive", "rc"] }
serde_json = "1.0.79"
uuid = { version = "0.8.2", features = ["v4"] }
toml = "0.7.2"
serde = { version = "1.0.137", features = ["derive", "rc"] }
serde_json = "1.0.94"
uuid = { version = "1.3.0", features = ["v4"] }
indicatif = "0.15"
console = "0.15.0"
openssl = { version = "0.10.38", features = ["vendored"] }
console = "0.15.2"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "4.0.0"
regex = "1.5.5"
crossterm = "0.23.2"
rlimit = "0.8.3"
ctrlc = "3.2.1"
fuzzyhash = "0.2.1"
anyhow = "1.0.56"
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
crossterm = "0.26.0"
rlimit = "0.9.1"
ctrlc = "3.2.2"
anyhow = "1.0.69"
leaky-bucket = "0.12.1"
gaoya = "0.1.2"
self_update = {version = "0.36.0", features = ["archive-tar", "compression-flate2"]}
[dev-dependencies]
tempfile = "3.3.0"
@@ -67,9 +74,29 @@ section = "utility"
license-file = ["LICENSE", "4"]
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"],
[
"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",
],
]

View File

@@ -1,10 +1,7 @@
# Image: alpine:3.14.2
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as build
FROM alpine:3.17.1 as build
LABEL maintainer="wfnintr@null.net"
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories \
&& apk upgrade --update-cache --available && apk add --update openssl
RUN apk upgrade --update-cache --available && apk add --update openssl
# Download latest release
RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip -qO feroxbuster.zip \
@@ -12,9 +9,7 @@ RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-l
&& chmod +x /tmp/feroxbuster \
&& wget https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/raft-medium-directories.txt -O /tmp/raft-medium-directories.txt
# Image: alpine:3.14.2
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as release
from alpine:3.17.1 as release
COPY --from=build /tmp/raft-medium-directories.txt /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
COPY --from=build /tmp/feroxbuster /usr/local/bin/feroxbuster

View File

@@ -70,7 +70,8 @@ ifeq (1, $(VENDORED))
endif
$(TARGET)/$(BIN): extract
mkdir -p .cargo
mkdir -p .cargo debian
touch debian/cargo.config
cp debian/cargo.config .cargo/config.toml
cargo build $(ARGS)

View File

@@ -11,7 +11,7 @@ rm ferox-*.state
# dependency management
[tasks.upgrade-deps]
command = "cargo"
args = ["upgrade", "--exclude", "indicatif", "leaky-bucket"]
args = ["upgrade", "--exclude", "indicatif"]
[tasks.update]
command = "cargo"
@@ -23,3 +23,10 @@ clear = true
script = """
cargo clippy --all-targets --all-features -- -D warnings
"""
# tests
[tasks.test]
clear = true
script = """
cargo nextest run --all-features --all-targets --retries 10
"""

143
README.md
View File

@@ -8,7 +8,7 @@
<p align="center">
<a href="https://github.com/epi052/feroxbuster/actions?query=workflow%3A%22CI+Pipeline%22">
<img src="https://img.shields.io/github/workflow/status/epi052/feroxbuster/CI%20Pipeline/main?logo=github">
<img src="https://img.shields.io/github/actions/workflow/status/epi052/feroxbuster/.github/workflows/check.yml?branch=main&logo=github">
</a>
<a href="https://github.com/epi052/feroxbuster/releases">
@@ -167,6 +167,12 @@ cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js |
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
```
### Updating feroxbuster (new in v2.10.0)
```
./feroxbuster --update
```
## 🚀 Documentation has **moved** 🚀
For realsies, there used to be over 1300 lines in this README, but it's all been moved to the [new documentation site](https://epi052.github.io/feroxbuster-docs/docs/). Go check it out!
@@ -181,60 +187,85 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt=""/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt=""/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</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://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="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/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/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt=""/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt=""/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt=""/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/narkopolo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt=""/><br /><sub><b>narkopolo</b></sub></a><br /><a href="#ideas-narkopolo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt=""/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt=""/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt=""/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt=""/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</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>
</tr>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://io.fi"><img src="https://avatars.githubusercontent.com/u/5235109?v=4?s=100" width="100px;" alt="Joona Hoikkala"/><br /><sub><b>Joona Hoikkala</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=joohoi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jsav0"><img src="https://avatars.githubusercontent.com/u/20546041?v=4?s=100" width="100px;" alt="J Savage"/><br /><sub><b>J Savage</b></sub></a><br /><a href="#infra-jsav0" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=jsav0" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.tgotwig.dev"><img src="https://avatars.githubusercontent.com/u/30773779?v=4?s=100" width="100px;" alt="Thomas Gotwig"/><br /><sub><b>Thomas Gotwig</b></sub></a><br /><a href="#infra-TGotwig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=TGotwig" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/spikecodes"><img src="https://avatars.githubusercontent.com/u/19519553?v=4?s=100" width="100px;" alt="Spike"/><br /><sub><b>Spike</b></sub></a><br /><a href="#infra-spikecodes" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=spikecodes" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/evanrichter"><img src="https://avatars.githubusercontent.com/u/330292?v=4?s=100" width="100px;" alt="Evan Richter"/><br /><sub><b>Evan Richter</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=evanrichter" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mzpqnxow"><img src="https://avatars.githubusercontent.com/u/8016228?v=4?s=100" width="100px;" alt="AG"/><br /><sub><b>AG</b></sub></a><br /><a href="#ideas-mzpqnxow" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=mzpqnxow" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://n-thumann.de/"><img src="https://avatars.githubusercontent.com/u/46975855?v=4?s=100" width="100px;" alt="Nicolas Thumann"/><br /><sub><b>Nicolas Thumann</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Code">💻</a> <a href="https://github.com/epi052/feroxbuster/commits?author=n-thumann" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomtastic"><img src="https://avatars.githubusercontent.com/u/302127?v=4?s=100" width="100px;" alt="Tom Matthews"/><br /><sub><b>Tom Matthews</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=tomtastic" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bsysop"><img src="https://avatars.githubusercontent.com/u/9998303?v=4?s=100" width="100px;" alt="bsysop"/><br /><sub><b>bsysop</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bsysop" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://bpsizemore.me"><img src="https://avatars.githubusercontent.com/u/11645898?v=4?s=100" width="100px;" alt="Brian Sizemore"/><br /><sub><b>Brian Sizemore</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=bpsizemore" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://pwn.by/noraj"><img src="https://avatars.githubusercontent.com/u/16578570?v=4?s=100" width="100px;" alt="Alexandre ZANNI"/><br /><sub><b>Alexandre ZANNI</b></sub></a><br /><a href="#infra-noraj" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/epi052/feroxbuster/commits?author=noraj" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/craig"><img src="https://avatars.githubusercontent.com/u/99729?v=4?s=100" width="100px;" alt="Craig"/><br /><sub><b>Craig</b></sub></a><br /><a href="#infra-craig" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.reddit.com/u/EONRaider"><img src="https://avatars.githubusercontent.com/u/15611424?v=4?s=100" width="100px;" alt="EONRaider"/><br /><sub><b>EONRaider</b></sub></a><br /><a href="#infra-EONRaider" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wtwver"><img src="https://avatars.githubusercontent.com/u/53866088?v=4?s=100" width="100px;" alt="wtwver"/><br /><sub><b>wtwver</b></sub></a><br /><a href="#infra-wtwver" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tib3rius.com"><img src="https://avatars.githubusercontent.com/u/48113936?v=4?s=100" width="100px;" alt="Tib3rius"/><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" valign="top" width="14.28%"><a href="https://github.com/0xdf"><img src="https://avatars.githubusercontent.com/u/1489045?v=4?s=100" width="100px;" alt="0xdf"/><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" valign="top" width="14.28%"><a href="http://secure77.de"><img src="https://avatars.githubusercontent.com/u/31564517?v=4?s=100" width="100px;" alt="secure-77"/><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" valign="top" width="14.28%"><a href="https://github.com/sbrun"><img src="https://avatars.githubusercontent.com/u/7712154?v=4?s=100" width="100px;" alt="Sophie Brun"/><br /><sub><b>Sophie Brun</b></sub></a><br /><a href="#infra-sbrun" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/black-A"><img src="https://avatars.githubusercontent.com/u/30686803?v=4?s=100" width="100px;" alt="black-A"/><br /><sub><b>black-A</b></sub></a><br /><a href="#ideas-black-A" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dinosn"><img src="https://avatars.githubusercontent.com/u/3851678?v=4?s=100" width="100px;" alt="Nicolas Krassas"/><br /><sub><b>Nicolas Krassas</b></sub></a><br /><a href="#ideas-dinosn" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/N0ur5"><img src="https://avatars.githubusercontent.com/u/24260009?v=4?s=100" width="100px;" alt="N0ur5"/><br /><sub><b>N0ur5</b></sub></a><br /><a href="#ideas-N0ur5" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moscowchill"><img src="https://avatars.githubusercontent.com/u/72578879?v=4?s=100" width="100px;" alt="mchill"/><br /><sub><b>mchill</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Amoscowchill" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://BitThr3at.github.io"><img src="https://avatars.githubusercontent.com/u/45028933?v=4?s=100" width="100px;" alt="Naman"/><br /><sub><b>Naman</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ABitThr3at" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sicks3c"><img src="https://avatars.githubusercontent.com/u/32225186?v=4?s=100" width="100px;" alt="Ayoub Elaich"/><br /><sub><b>Ayoub Elaich</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Asicks3c" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HenryHoggard"><img src="https://avatars.githubusercontent.com/u/1208121?v=4?s=100" width="100px;" alt="Henry"/><br /><sub><b>Henry</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AHenryHoggard" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SleepiPanda"><img src="https://avatars.githubusercontent.com/u/6428561?v=4?s=100" width="100px;" alt="SleepiPanda"/><br /><sub><b>SleepiPanda</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ASleepiPanda" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uBadRequest"><img src="https://avatars.githubusercontent.com/u/47282747?v=4?s=100" width="100px;" alt="Bad Requests"/><br /><sub><b>Bad Requests</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AuBadRequest" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://home.dnaka91.rocks"><img src="https://avatars.githubusercontent.com/u/36804488?v=4?s=100" width="100px;" alt="Dominik Nakamura"/><br /><sub><b>Dominik Nakamura</b></sub></a><br /><a href="#infra-dnaka91" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hunter0x8"><img src="https://avatars.githubusercontent.com/u/46222314?v=4?s=100" width="100px;" alt="Muhammad Ahsan"/><br /><sub><b>Muhammad Ahsan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ahunter0x8" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cortantief"><img src="https://avatars.githubusercontent.com/u/34527333?v=4?s=100" width="100px;" alt="cortantief"/><br /><sub><b>cortantief</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Acortantief" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=cortantief" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dsaxton"><img src="https://avatars.githubusercontent.com/u/2658661?v=4?s=100" width="100px;" alt="Daniel Saxton"/><br /><sub><b>Daniel Saxton</b></sub></a><br /><a href="#ideas-dsaxton" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=dsaxton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/n0kovo"><img src="https://avatars.githubusercontent.com/u/16690056?v=4?s=100" width="100px;" alt="n0kovo"/><br /><sub><b>n0kovo</b></sub></a><br /><a href="#ideas-n0kovo" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ring0.lol"><img src="https://avatars.githubusercontent.com/u/1893909?v=4?s=100" width="100px;" alt="Justin Steven"/><br /><sub><b>Justin Steven</b></sub></a><br /><a href="#ideas-justinsteven" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/7047payloads"><img src="https://avatars.githubusercontent.com/u/95562424?v=4?s=100" width="100px;" alt="7047payloads"/><br /><sub><b>7047payloads</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=7047payloads" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/unkn0wnsyst3m"><img src="https://avatars.githubusercontent.com/u/21272239?v=4?s=100" width="100px;" alt="unkn0wnsyst3m"/><br /><sub><b>unkn0wnsyst3m</b></sub></a><br /><a href="#ideas-unkn0wnsyst3m" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://ironwort.me/"><img src="https://avatars.githubusercontent.com/u/15280042?v=4?s=100" width="100px;" alt="0x08"/><br /><sub><b>0x08</b></sub></a><br /><a href="#ideas-its0x08" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MD-Levitan"><img src="https://avatars.githubusercontent.com/u/12116508?v=4?s=100" width="100px;" alt="kusok"/><br /><sub><b>kusok</b></sub></a><br /><a href="#ideas-MD-Levitan" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=MD-Levitan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/godylockz"><img src="https://avatars.githubusercontent.com/u/81207744?v=4?s=100" width="100px;" alt="godylockz"/><br /><sub><b>godylockz</b></sub></a><br /><a href="#ideas-godylockz" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/commits?author=godylockz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://ryanmontgomery.me"><img src="https://avatars.githubusercontent.com/u/44453666?v=4?s=100" width="100px;" alt="Ryan Montgomery"/><br /><sub><b>Ryan Montgomery</b></sub></a><br /><a href="#ideas-0dayCTF" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IppSec"><img src="https://avatars.githubusercontent.com/u/24677271?v=4?s=100" width="100px;" alt="ippsec"/><br /><sub><b>ippsec</b></sub></a><br /><a href="#ideas-ippsec" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gtjamesa"><img src="https://avatars.githubusercontent.com/u/2078364?v=4?s=100" width="100px;" alt="James"/><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" valign="top" width="14.28%"><a href="https://twitter.com/Jhaddix"><img src="https://avatars.githubusercontent.com/u/3488554?v=4?s=100" width="100px;" alt="Jason Haddix"/><br /><sub><b>Jason Haddix</b></sub></a><br /><a href="#ideas-jhaddix" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajhaddix" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThisLimn0"><img src="https://avatars.githubusercontent.com/u/67125885?v=4?s=100" width="100px;" alt="Limn0"/><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" valign="top" width="14.28%"><a href="https://github.com/0xdf223"><img src="https://avatars.githubusercontent.com/u/76954092?v=4?s=100" width="100px;" alt="0xdf"/><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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Flangyver"><img src="https://avatars.githubusercontent.com/u/59575870?v=4?s=100" width="100px;" alt="Flangyver"/><br /><sub><b>Flangyver</b></sub></a><br /><a href="#ideas-Flangyver" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DonatoReis"><img src="https://avatars.githubusercontent.com/u/93531354?v=4?s=100" width="100px;" alt="PeakyBlinder"/><br /><sub><b>PeakyBlinder</b></sub></a><br /><a href="#ideas-DonatoReis" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://postmodern.github.io/"><img src="https://avatars.githubusercontent.com/u/12671?v=4?s=100" width="100px;" alt="Postmodern"/><br /><sub><b>Postmodern</b></sub></a><br /><a href="#ideas-postmodern" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/herrcykel"><img src="https://avatars.githubusercontent.com/u/1936757?v=4?s=100" width="100px;" alt="O"/><br /><sub><b>O</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=herrcykel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://udoprog.github.io/"><img src="https://avatars.githubusercontent.com/u/111092?v=4?s=100" width="100px;" alt="John-John Tedro"/><br /><sub><b>John-John Tedro</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=udoprog" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kmanc"><img src="https://avatars.githubusercontent.com/u/14863147?v=4?s=100" width="100px;" alt="kmanc"/><br /><sub><b>kmanc</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Akmanc" title="Bug reports">🐛</a> <a href="https://github.com/epi052/feroxbuster/commits?author=kmanc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hakdogpinas"><img src="https://avatars.githubusercontent.com/u/71529469?v=4?s=100" width="100px;" alt="hakdogpinas"/><br /><sub><b>hakdogpinas</b></sub></a><br /><a href="#ideas-hakdogpinas" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/duokebei"><img src="https://avatars.githubusercontent.com/u/75022552?v=4?s=100" width="100px;" alt="多可悲"/><br /><sub><b>多可悲</b></sub></a><br /><a href="#ideas-duokebei" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.ah34.net/"><img src="https://avatars.githubusercontent.com/u/58670593?v=4?s=100" width="100px;" alt="Aidan Hall"/><br /><sub><b>Aidan Hall</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aidanhall34" title="Code">💻</a> <a href="#infra-aidanhall34" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hachyderm.io/@JohnnyCiocca"><img src="https://avatars.githubusercontent.com/u/6473725?v=4?s=100" width="100px;" alt="João Ciocca"/><br /><sub><b>João Ciocca</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Ajoaociocca" title="Bug reports">🐛</a> <a href="#ideas-joaociocca" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/f3rn0s"><img src="https://avatars.githubusercontent.com/u/1351279?v=4?s=100" width="100px;" alt="f3rn0s"/><br /><sub><b>f3rn0s</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Af3rn0s" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://sth.sh"><img src="https://avatars.githubusercontent.com/u/2099767?v=4?s=100" width="100px;" alt="LongCat"/><br /><sub><b>LongCat</b></sub></a><br /><a href="#ideas-pich4ya" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xaeroborg"><img src="https://avatars.githubusercontent.com/u/33274680?v=4?s=100" width="100px;" alt="xaeroborg"/><br /><sub><b>xaeroborg</b></sub></a><br /><a href="#ideas-xaeroborg" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Luoooio"><img src="https://avatars.githubusercontent.com/u/26653157?v=4?s=100" width="100px;" alt="Luoooio"/><br /><sub><b>Luoooio</b></sub></a><br /><a href="#ideas-Luoooio" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://petruknisme.com"><img src="https://avatars.githubusercontent.com/u/6284204?v=4?s=100" width="100px;" alt="Aan"/><br /><sub><b>Aan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aancw" title="Code">💻</a> <a href="#infra-aancw" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
@@ -242,4 +273,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- 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

@@ -1,5 +1,5 @@
use std::fs::{copy, create_dir_all, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::{Read, Seek, Write};
use clap_complete::{generate_to, shells};
@@ -30,7 +30,7 @@ fn main() {
let mut bash_file = OpenOptions::new()
.read(true)
.write(true)
.open(format!("{}/feroxbuster.bash", outdir))
.open(format!("{outdir}/feroxbuster.bash"))
.expect("Couldn't open bash completion script");
bash_file
@@ -40,7 +40,7 @@ fn main() {
contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster");
bash_file
.seek(SeekFrom::Start(0))
.rewind()
.expect("Couldn't seek to position 0 in bash completion script");
bash_file
@@ -59,15 +59,11 @@ fn main() {
if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if
// they are missing
if !config_dir.exists() {
// recursively create the feroxbuster directory and all of its parent components if
// they are missing
if create_dir_all(&config_dir).is_err() {
// only copy the config file when we're not running in the CI/CD pipeline
// which fails with permission denied
eprintln!("Couldn't create one or more directories needed to copy the config file");
return;
}
if create_dir_all(&config_dir).is_err() {
// only copy the config file when we're not running in the CI/CD pipeline
// which fails with permission denied
eprintln!("Couldn't create one or more directories needed to copy the config file");
return;
}
}

View File

@@ -45,6 +45,7 @@
# dont_filter = true
# extract_links = true
# depth = 1
# force_recursion = true
# filter_size = [5174]
# filter_regex = ["^ignore me$"]
# filter_similar = ["https://somesite.com/soft404"]
@@ -53,6 +54,7 @@
# queries = [["name","value"], ["rick", "astley"]]
# save_state = false
# time_limit = "10m"
# update_app = false
# headers can be specified on multiple lines or as an inline table
#

BIN
img/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -24,8 +24,8 @@ _feroxbuster() {
'--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: ' \
'*--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: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.2)]:USER_AGENT: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.10.0)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.10.0)]:USER_AGENT: ' \
'*-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: ' \
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
@@ -46,11 +46,11 @@ _feroxbuster() {
'*--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: ' \
'*--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: ' \
'*--filter-status=[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: ' \
'(-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' \
'*-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: ' \
'*-s+[Status Codes to include (allow list) (default: All Status Codes)]:STATUS_CODE: ' \
'*--status-codes=[Status Codes to include (allow list) (default: All Status Codes)]:STATUS_CODE: ' \
'-T+[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \
'--timeout=[Number of seconds before a client'\''s request times out (default: 7)]:SECONDS: ' \
'-t+[Number of concurrent threads (default: 50)]:THREADS: ' \
@@ -69,15 +69,11 @@ _feroxbuster() {
'-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
'--output=[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
'--debug-log=[Output file to write log entries (use w/ --json for JSON entries)]:FILE:_files' \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
'(-u --url)--stdin[Read url(s) from STDIN]' \
'(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'--smart[Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true]' \
'--thorough[Use the same settings as --smart and set --collect-extensions to true]' \
'(--rate-limit --auto-bail)--smart[Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true]' \
'(--rate-limit --auto-bail)--thorough[Use the same settings as --smart and set --collect-extensions to true]' \
'-A[Use a random User-Agent]' \
'--random-agent[Use a random User-Agent]' \
'-f[Append / to each request'\''s URL]' \
@@ -88,6 +84,7 @@ _feroxbuster() {
'--insecure[Disables TLS certificate validation in the client]' \
'-n[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]' \
'--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]' \
@@ -107,6 +104,12 @@ _feroxbuster() {
'--quiet[Hide progress bars and banner (good for tmux windows w/ notifications)]' \
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'--no-state[Disable state output file (*.state)]' \
'(-u --url -w --wordlist)-U[Update the app to the latest version]' \
'(-u --url -w --wordlist)--update[Update the app to the latest version]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
&& ret=0
}
@@ -116,4 +119,8 @@ _feroxbuster_commands() {
_describe -t commands 'feroxbuster commands' commands "$@"
}
_feroxbuster "$@"
if [ "$funcstack[1]" = "_feroxbuster" ]; then
_feroxbuster "$@"
else
compdef _feroxbuster feroxbuster
fi

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('-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('-a', 'a', [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.6.2)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.10.0)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.10.0)')
[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('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
@@ -55,8 +55,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('-C', 'C', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[CompletionResult]::new('--filter-status', 'filter-status', [CompletionResultType]::ParameterName, 'Filter out status codes (deny list) (ex: -C 200 -C 401)')
[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('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)')
[CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)')
[CompletionResult]::new('--status-codes', 'status-codes', [CompletionResultType]::ParameterName, 'Status Codes to include (allow list) (default: All Status Codes)')
[CompletionResult]::new('-T', 'T', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)')
[CompletionResult]::new('--timeout', 'timeout', [CompletionResultType]::ParameterName, 'Number of seconds before a client''s request times out (default: 7)')
[CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'Number of concurrent threads (default: 50)')
@@ -75,10 +75,6 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--output', 'output', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
[CompletionResult]::new('--debug-log', 'debug-log', [CompletionResultType]::ParameterName, 'Output file to write log entries (use w/ --json for JSON entries)')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information')
[CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN')
[CompletionResult]::new('--burp', 'burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true')
[CompletionResult]::new('--burp-replay', 'burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true')
@@ -94,6 +90,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[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('--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('--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')
@@ -113,6 +110,12 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Hide progress bars and banner (good for tmux windows w/ notifications)')
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('--no-state', 'no-state', [CompletionResultType]::ParameterName, 'Disable state output file (*.state)')
[CompletionResult]::new('-U', 'U', [CompletionResultType]::ParameterName, 'Update the app to the latest version')
[CompletionResult]::new('--update', 'update', [CompletionResultType]::ParameterName, 'Update the app to the latest version')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})

View File

@@ -1,5 +1,5 @@
_feroxbuster() {
local i cur prev opts cmds
local i cur prev opts cmd
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
@@ -8,8 +8,8 @@ _feroxbuster() {
for i in ${COMP_WORDS[@]}
do
case "${i}" in
"$1")
case "${cmd},${i}" in
",$1")
cmd="feroxbuster"
;;
*)
@@ -19,7 +19,7 @@ _feroxbuster() {
case "${cmd}" in
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="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --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 --update --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
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 -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 -a 'Sets the User-Agent (default: feroxbuster/2.6.2)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.2)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.10.0)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.10.0)'
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 -m 'Which HTTP request method(s) should be sent (default: GET)'
@@ -52,8 +52,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand -C 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
cand --filter-status 'Filter out status codes (deny list) (ex: -C 200 -C 401)'
cand --filter-similar-to 'Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)'
cand -s 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
cand --status-codes 'Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)'
cand -s 'Status Codes to include (allow list) (default: All Status Codes)'
cand --status-codes 'Status Codes to include (allow list) (default: All Status Codes)'
cand -T 'Number of seconds before a client''s request times out (default: 7)'
cand --timeout 'Number of seconds before a client''s request times out (default: 7)'
cand -t 'Number of concurrent threads (default: 50)'
@@ -72,10 +72,6 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand -o 'Output file to write results to (use w/ --json for JSON entries)'
cand --output 'Output file to write results to (use w/ --json for JSON entries)'
cand --debug-log 'Output file to write log entries (use w/ --json for JSON entries)'
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
cand --stdin 'Read url(s) from STDIN'
cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true'
cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true'
@@ -91,6 +87,7 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --insecure 'Disables TLS certificate validation in the client'
cand -n '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 --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'
@@ -110,6 +107,12 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --quiet 'Hide progress bars and banner (good for tmux windows w/ notifications)'
cand --json 'Emit JSON logs to --output and --debug-log instead of normal text'
cand --no-state 'Disable state output file (*.state)'
cand -U 'Update the app to the latest version'
cand --update 'Update the app to the latest version'
cand -h 'Print help (see more with ''--help'')'
cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]

View File

@@ -3,7 +3,7 @@ use crate::{
config::Configuration,
event_handlers::Handles,
utils::{logged_request, status_colorizer},
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, VERSION,
DEFAULT_IGNORED_EXTENSIONS, DEFAULT_METHOD, DEFAULT_STATUS_CODES, VERSION,
};
use anyhow::{bail, Result};
use console::{style, Emoji};
@@ -163,6 +163,12 @@ pub struct Banner {
/// represents Configuration.collect_words
collect_words: BannerEntry,
/// represents Configuration.collect_words
force_recursion: BannerEntry,
/// represents Configuration.update_app
update_app: BannerEntry,
}
/// implementation of Banner
@@ -201,12 +207,25 @@ impl Banner {
));
}
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
}
let status_codes =
BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", ")));
// the +2 is for the 2 experimental status codes we add to the default list manually
let status_codes = if config.status_codes.len() == DEFAULT_STATUS_CODES.len() + 2 {
let all_str = format!(
"{} {} {}{}",
style("All").cyan(),
style("Status").green(),
style("Codes").yellow(),
style("!").red()
);
BannerEntry::new("👌", "Status Codes", &all_str)
} else {
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
}
BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", ")))
};
for code in &config.filter_status {
code_filters.push(status_colorizer(&code.to_string()))
@@ -230,7 +249,7 @@ impl Banner {
headers.push(BannerEntry::new(
"🤯",
"Header",
&format!("{}: {}", name, value),
&format!("{name}: {value}"),
));
}
@@ -300,9 +319,11 @@ impl Banner {
&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 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());
let cfg = BannerEntry::new("💉", "Config File", &config.config);
let proxy = BannerEntry::new("💎", "Proxy", &config.proxy);
let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string());
@@ -315,6 +336,7 @@ impl Banner {
let json = BannerEntry::new("🧔", "JSON Output", &config.json.to_string());
let output = BannerEntry::new("💾", "Output File", &config.output);
let debug_log = BannerEntry::new("🪲", "Debugging Log", &config.debug_log);
let update_app = BannerEntry::new("🔥", "Update app", &config.update_app.to_string());
let extensions = BannerEntry::new(
"💲",
"Extensions",
@@ -409,6 +431,7 @@ impl Banner {
no_recursion,
rate_limit,
scan_limit,
force_recursion,
time_limit,
url_denylist,
collect_extensions,
@@ -418,6 +441,7 @@ impl Banner {
config: cfg,
version: VERSION.to_string(),
update_status: UpdateStatus::Unknown,
update_app,
}
}
@@ -435,7 +459,7 @@ by Ben "epi" Risher {} ver: {}"#,
let top = "───────────────────────────┬──────────────────────";
format!("{}\n{}", artwork, top)
format!("{artwork}\n{top}")
}
/// get a fancy footer for the banner
@@ -449,7 +473,7 @@ by Ben "epi" Risher {} ver: {}"#,
style("Scan Management Menu").bright().yellow(),
);
format!("{}\n{}\n{}", bottom, instructions, addl_section)
format!("{bottom}\n{instructions}\n{addl_section}")
}
/// Makes a request to the given url, expecting to receive a JSON response that contains a field
@@ -502,20 +526,21 @@ by Ben "epi" Risher {} ver: {}"#,
// begin with always printed items
for target in &self.targets {
writeln!(&mut writer, "{}", target)?;
writeln!(&mut writer, "{target}")?;
}
for denied_url in &self.url_denylist {
writeln!(&mut writer, "{}", denied_url)?;
writeln!(&mut writer, "{denied_url}")?;
}
writeln!(&mut writer, "{}", self.threads)?;
writeln!(&mut writer, "{}", self.wordlist)?;
writeln!(&mut writer, "{}", self.status_codes)?;
if !config.filter_status.is_empty() {
// exception here for an optional print in the middle of always printed values is due
// to me wanting the allows and denys to be printed one after the other
if config.filter_status.is_empty() {
// -C and -s are mutually exclusive, and -s meaning changes when -C is used
// so only print one or the other
writeln!(&mut writer, "{}", self.status_codes)?;
} else {
writeln!(&mut writer, "{}", self.filter_status)?;
}
@@ -544,27 +569,27 @@ by Ben "epi" Risher {} ver: {}"#,
}
for header in &self.headers {
writeln!(&mut writer, "{}", header)?;
writeln!(&mut writer, "{header}")?;
}
for filter in &self.filter_size {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_similar {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_word_count {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_line_count {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
for filter in &self.filter_regex {
writeln!(&mut writer, "{}", filter)?;
writeln!(&mut writer, "{filter}")?;
}
if config.extract_links {
@@ -576,7 +601,7 @@ by Ben "epi" Risher {} ver: {}"#,
}
for query in &self.queries {
writeln!(&mut writer, "{}", query)?;
writeln!(&mut writer, "{query}")?;
}
if !config.output.is_empty() {
@@ -642,6 +667,14 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", self.no_recursion)?;
if config.force_recursion {
writeln!(&mut writer, "{}", self.force_recursion)?;
}
if config.update_app {
writeln!(&mut writer, "{}", self.update_app)?;
}
if config.scan_limit > 0 {
writeln!(&mut writer, "{}", self.scan_limit)?;
}
@@ -664,7 +697,7 @@ by Ben "epi" Risher {} ver: {}"#,
"New Version Available",
"https://github.com/epi052/feroxbuster/releases/latest",
);
writeln!(&mut writer, "{}", update)?;
writeln!(&mut writer, "{update}")?;
}
writeln!(&mut writer, "{}", self.footer())?;

View File

@@ -9,7 +9,7 @@ use crate::{
DEFAULT_CONFIG_NAME,
};
use anyhow::{anyhow, Context, Result};
use clap::ArgMatches;
use clap::{parser::ValueSource, ArgMatches};
use regex::Regex;
use reqwest::{Client, Method, StatusCode, Url};
use serde::{Deserialize, Serialize};
@@ -22,15 +22,10 @@ use std::{
/// macro helper to abstract away repetitive configuration updates
macro_rules! update_config_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr) => {
match $matches.value_of_t($arg_name) {
Ok(value) => *$conf_val = value, // Update value
Err(err) => {
if !matches!(err.kind(), clap::ErrorKind::ArgumentNotFound) {
// Do nothing if argument not found
err.exit() // Exit with error on any other parse error
}
}
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
match $matches.get_one::<$arg_type>($arg_name) {
Some(value) => *$conf_val = value.to_owned(), // Update value
None => {}
}
};
}
@@ -44,6 +39,35 @@ macro_rules! update_if_not_default {
};
}
/// macro helper to abstract away repetitive checks to see if the user has specified a value
/// for a given argument from the commandline or if we just had a default value in the parser
macro_rules! came_from_cli {
($matches:ident, $arg_name:expr) => {
matches!(
$matches.value_source($arg_name),
Some(ValueSource::CommandLine)
)
};
}
/// macro helper to abstract away repetitive if not default: update checks, specifically for
/// values that are number types, i.e. usize, u64, etc
macro_rules! update_config_with_num_type_if_present {
($conf_val:expr, $matches:ident, $arg_name:expr, $arg_type:ty) => {
if let Some(val) = $matches.get_one::<String>($arg_name) {
match val.parse::<$arg_type>() {
Ok(v) => *$conf_val = v,
Err(_) => {
report_and_exit(&format!(
"Invalid value for --{}, must be a positive integer",
$arg_name
));
}
}
}
};
}
/// Represents the final, global configuration of the program.
///
/// This struct is the combination of the following:
@@ -281,6 +305,14 @@ pub struct Configuration {
/// Automatically discover important words from within responses and add them to the wordlist
#[serde(default)]
pub collect_words: bool,
/// override recursion logic to always attempt recursion, still respects --depth
#[serde(default)]
pub force_recursion: bool,
/// Auto update app feature
#[serde(default)]
pub update_app: bool,
}
impl Default for Configuration {
@@ -329,6 +361,8 @@ impl Default for Configuration {
collect_backups: false,
collect_words: false,
save_state: true,
force_recursion: false,
update_app: false,
proxy: String::new(),
config: String::new(),
output: String::new(),
@@ -405,12 +439,14 @@ impl Configuration {
/// - **json**: `false`
/// - **dont_filter**: `false` (auto filter wildcard responses)
/// - **depth**: `4` (maximum recursion depth)
/// - **force_recursion**: `false` (still respects recursion depth)
/// - **scan_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)
/// - **update_app**: `false`
///
/// After which, any values defined in a
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
@@ -454,7 +490,7 @@ impl Configuration {
// --resume-from used, need to first read the Configuration from disk, and then
// merge the cli_config into the resumed config
if let Some(filename) = args.value_of("resume_from") {
if let Some(filename) = args.get_one::<String>("resume_from") {
// when resuming a scan, instead of normal configuration loading, we just
// load the config from disk by calling resume_scan
let mut previous_config = resume_scan(filename);
@@ -540,18 +576,21 @@ impl Configuration {
fn parse_cli_args(args: &ArgMatches) -> Self {
let mut config = Configuration::default();
update_config_if_present!(&mut config.threads, args, "threads");
update_config_if_present!(&mut config.depth, args, "depth");
update_config_if_present!(&mut config.scan_limit, args, "scan_limit");
update_config_if_present!(&mut config.parallel, args, "parallel");
update_config_if_present!(&mut config.rate_limit, args, "rate_limit");
update_config_if_present!(&mut config.wordlist, args, "wordlist");
update_config_if_present!(&mut config.output, args, "output");
update_config_if_present!(&mut config.debug_log, args, "debug_log");
update_config_if_present!(&mut config.time_limit, args, "time_limit");
update_config_if_present!(&mut config.resume_from, args, "resume_from");
update_config_with_num_type_if_present!(&mut config.threads, args, "threads", usize);
update_config_with_num_type_if_present!(&mut config.parallel, args, "parallel", usize);
update_config_with_num_type_if_present!(&mut config.depth, args, "depth", usize);
update_config_with_num_type_if_present!(&mut config.scan_limit, args, "scan_limit", usize);
update_config_with_num_type_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);
update_config_if_present!(&mut config.debug_log, args, "debug_log", String);
update_config_if_present!(&mut config.resume_from, args, "resume_from", String);
if let Some(arg) = args.values_of("status_codes") {
if let Ok(Some(inner)) = args.try_get_one::<String>("time_limit") {
config.time_limit = inner.to_owned();
}
if let Some(arg) = args.get_many::<String>("status_codes") {
config.status_codes = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
@@ -561,7 +600,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("replay_codes") {
if let Some(arg) = args.get_many::<String>("replay_codes") {
// replay codes passed in by the user
config.replay_codes = arg
.map(|code| {
@@ -575,7 +614,7 @@ impl Configuration {
config.replay_codes = config.status_codes.clone();
}
if let Some(arg) = args.values_of("filter_status") {
if let Some(arg) = args.get_many::<String>("filter_status") {
config.filter_status = arg
.map(|code| {
StatusCode::from_bytes(code.as_bytes())
@@ -585,15 +624,17 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("extensions") {
config.extensions = arg.map(|val| val.to_string()).collect();
if let Some(arg) = args.get_many::<String>("extensions") {
config.extensions = arg
.map(|val| val.trim_start_matches('.').to_string())
.collect();
}
if let Some(arg) = args.values_of("dont_collect") {
if let Some(arg) = args.get_many::<String>("dont_collect") {
config.dont_collect = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("methods") {
if let Some(arg) = args.get_many::<String>("methods") {
config.methods = arg
.map(|val| {
// Check methods if they are correct
@@ -605,7 +646,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.value_of("data") {
if let Some(arg) = args.get_one::<String>("data") {
if let Some(stripped) = arg.strip_prefix('@') {
config.data =
std::fs::read(stripped).unwrap_or_else(|e| report_and_exit(&e.to_string()));
@@ -614,13 +655,13 @@ impl Configuration {
}
}
if args.is_present("stdin") {
if came_from_cli!(args, "stdin") {
config.stdin = true;
} else if let Some(url) = args.value_of("url") {
config.target_url = String::from(url);
} else if let Some(url) = args.get_one::<String>("url") {
config.target_url = url.into();
}
if let Some(arg) = args.values_of("url_denylist") {
if let Some(arg) = args.get_many::<String>("url_denylist") {
// compile all regular expressions and absolute urls used for --dont-scan
//
// when --dont-scan is used, the should_deny_url function is called at least once per
@@ -664,15 +705,15 @@ impl Configuration {
}
}
if let Some(arg) = args.values_of("filter_regex") {
if let Some(arg) = args.get_many::<String>("filter_regex") {
config.filter_regex = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_similar") {
if let Some(arg) = args.get_many::<String>("filter_similar") {
config.filter_similar = arg.map(|val| val.to_string()).collect();
}
if let Some(arg) = args.values_of("filter_size") {
if let Some(arg) = args.get_many::<String>("filter_size") {
config.filter_size = arg
.map(|size| {
size.parse::<u64>()
@@ -681,7 +722,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("filter_words") {
if let Some(arg) = args.get_many::<String>("filter_words") {
config.filter_word_count = arg
.map(|size| {
size.parse::<usize>()
@@ -690,7 +731,7 @@ impl Configuration {
.collect();
}
if let Some(arg) = args.values_of("filter_lines") {
if let Some(arg) = args.get_many::<String>("filter_lines") {
config.filter_line_count = arg
.map(|size| {
size.parse::<usize>()
@@ -699,7 +740,7 @@ impl Configuration {
.collect();
}
if args.is_present("silent") {
if came_from_cli!(args, "silent") {
// the reason this is protected by an if statement:
// consider a user specifying silent = true in ferox-config.toml
// if the line below is outside of the if, we'd overwrite true with
@@ -708,102 +749,115 @@ impl Configuration {
config.output_level = OutputLevel::Silent;
}
if args.is_present("quiet") {
if came_from_cli!(args, "quiet") {
config.quiet = true;
config.output_level = OutputLevel::Quiet;
}
if args.is_present("auto_tune") || args.is_present("smart") || args.is_present("thorough") {
if came_from_cli!(args, "auto_tune")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.auto_tune = true;
config.requester_policy = RequesterPolicy::AutoTune;
}
if args.is_present("auto_bail") {
if came_from_cli!(args, "auto_bail") {
config.auto_bail = true;
config.requester_policy = RequesterPolicy::AutoBail;
}
if args.is_present("no_state") {
if came_from_cli!(args, "no_state") {
config.save_state = false;
}
if args.is_present("dont_filter") {
if came_from_cli!(args, "dont_filter") {
config.dont_filter = true;
}
if args.is_present("collect_extensions") || args.is_present("thorough") {
if came_from_cli!(args, "collect_extensions") || came_from_cli!(args, "thorough") {
config.collect_extensions = true;
}
if args.is_present("collect_backups")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "collect_backups")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.collect_backups = true;
}
if args.is_present("collect_words")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "collect_words")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.collect_words = true;
}
if args.occurrences_of("verbosity") > 0 {
if args.get_count("verbosity") > 0 {
// occurrences_of returns 0 if none are found; this is protected in
// an if block for the same reason as the quiet option
config.verbosity = args.occurrences_of("verbosity") as u8;
config.verbosity = args.get_count("verbosity");
}
if args.is_present("no_recursion") {
if came_from_cli!(args, "no_recursion") {
config.no_recursion = true;
}
if args.is_present("add_slash") {
if came_from_cli!(args, "add_slash") {
config.add_slash = true;
}
if args.is_present("extract_links")
|| args.is_present("smart")
|| args.is_present("thorough")
if came_from_cli!(args, "extract_links")
|| came_from_cli!(args, "smart")
|| came_from_cli!(args, "thorough")
{
config.extract_links = true;
}
if args.is_present("json") {
if came_from_cli!(args, "json") {
config.json = true;
}
if came_from_cli!(args, "force_recursion") {
config.force_recursion = true;
}
if came_from_cli!(args, "update_app") {
config.update_app = true;
}
////
// organizational breakpoint; all options below alter the Client configuration
////
update_config_if_present!(&mut config.proxy, args, "proxy");
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy");
update_config_if_present!(&mut config.user_agent, args, "user_agent");
update_config_if_present!(&mut config.timeout, args, "timeout");
update_config_if_present!(&mut config.proxy, args, "proxy", String);
update_config_if_present!(&mut config.replay_proxy, args, "replay_proxy", String);
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
update_config_with_num_type_if_present!(&mut config.timeout, args, "timeout", u64);
if args.is_present("burp") {
if came_from_cli!(args, "burp") {
config.proxy = String::from("http://127.0.0.1:8080");
}
if args.is_present("burp_replay") {
if came_from_cli!(args, "burp_replay") {
config.replay_proxy = String::from("http://127.0.0.1:8080");
}
if args.is_present("random_agent") {
if came_from_cli!(args, "random_agent") {
config.random_agent = true;
}
if args.is_present("redirects") {
if came_from_cli!(args, "redirects") {
config.redirects = true;
}
if args.is_present("insecure") || args.is_present("burp") || args.is_present("burp_replay")
if came_from_cli!(args, "insecure")
|| came_from_cli!(args, "burp")
|| came_from_cli!(args, "burp_replay")
{
config.insecure = true;
}
if let Some(headers) = args.values_of("headers") {
if let Some(headers) = args.get_many::<String>("headers") {
for val in headers {
let mut split_val = val.split(':');
@@ -817,7 +871,7 @@ impl Configuration {
}
}
if let Some(cookies) = args.values_of("cookies") {
if let Some(cookies) = args.get_many::<String>("cookies") {
config.headers.insert(
// we know the header name is always "cookie"
"Cookie".to_string(),
@@ -833,7 +887,7 @@ impl Configuration {
);
}
if let Some(queries) = args.values_of("queries") {
if let Some(queries) = args.get_many::<String>("queries") {
for val in queries {
// same basic logic used as reading in the headers HashMap above
let mut split_val = val.split('=');
@@ -942,11 +996,13 @@ impl Configuration {
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.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.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, methods());
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());
update_if_not_default!(&mut conf.url_denylist, new.url_denylist, Vec::<Url>::new());
update_if_not_default!(&mut conf.update_app, new.update_app, false);
if !new.regex_denylist.is_empty() {
// cant use the update_if_not_default macro due to the following error
//
@@ -1017,7 +1073,17 @@ impl Configuration {
/// uses serde to deserialize the toml into a `Configuration` struct
pub(super) fn parse_config(config_file: PathBuf) -> Result<Self> {
let content = read_to_string(config_file)?;
let config: Self = toml::from_str(content.as_str())?;
let mut config: Self = toml::from_str(content.as_str())?;
if !config.extensions.is_empty() {
// remove leading periods, if any are found
config.extensions = config
.extensions
.iter()
.map(|ext| ext.trim_start_matches('.').to_string())
.collect();
}
Ok(config)
}
}

View File

@@ -49,12 +49,14 @@ fn setup_config_test() -> Configuration {
json = true
save_state = false
depth = 1
force_recursion = true
filter_size = [4120]
filter_regex = ["^ignore me$"]
filter_similar = ["https://somesite.com/soft404"]
filter_word_count = [994, 992]
filter_line_count = [34]
filter_status = [201]
update_app = false
"#;
let tmp_dir = TempDir::new().unwrap();
let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME);
@@ -95,12 +97,14 @@ fn default_configuration() {
assert!(config.save_state);
assert!(!config.stdin);
assert!(!config.add_slash);
assert!(!config.force_recursion);
assert!(!config.redirects);
assert!(!config.extract_links);
assert!(!config.insecure);
assert!(!config.collect_extensions);
assert!(!config.collect_backups);
assert!(!config.collect_words);
assert!(!config.update_app);
assert!(config.regex_denylist.is_empty());
assert_eq!(config.queries, Vec::new());
assert_eq!(config.filter_size, Vec::<u64>::new());
@@ -208,6 +212,13 @@ fn config_reads_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]
/// parse the test config and see that the value parsed is correct
fn config_reads_quiet() {
@@ -461,6 +472,13 @@ fn config_default_not_random_agent() {
assert!(!config.random_agent);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_update_app() {
let config = setup_config_test();
assert!(!config.update_app);
}
#[test]
#[should_panic]
/// test that an error message is printed and panic is called when report_and_exit is called
@@ -473,7 +491,7 @@ fn config_report_and_exit_works() {
fn as_str_returns_string_with_newline() {
let config = Configuration::new().unwrap();
let config_str = config.as_str();
println!("{}", config_str);
println!("{config_str}");
assert!(config_str.starts_with("Configuration {"));
assert!(config_str.ends_with("}\n"));
assert!(config_str.contains("replay_codes:"));

View File

@@ -49,6 +49,10 @@ pub(super) fn status_codes() -> Vec<u16> {
DEFAULT_STATUS_CODES
.iter()
.map(|code| code.as_u16())
// add experimental codes not found in reqwest
// - 103 - EARLY_HINTS
// - 425 - TOO_EARLY
.chain([103, 425])
.collect()
}
@@ -72,7 +76,7 @@ pub(super) fn wordlist() -> String {
/// default user-agent
pub(super) fn user_agent() -> String {
format!("feroxbuster/{}", VERSION)
format!("feroxbuster/{VERSION}")
}
/// default recursion depth
@@ -81,7 +85,7 @@ pub(super) fn depth() -> usize {
}
/// enum representing the three possible states for informational output (not logging verbosity)
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OutputLevel {
/// normal scan, no --quiet|--silent
Default,
@@ -116,7 +120,7 @@ pub fn determine_output_level(quiet: bool, silent: bool) -> OutputLevel {
}
/// represents actions the Requester should take in certain situations
#[derive(Debug, PartialEq, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum RequesterPolicy {
/// automatically try to lower request rate in order to reduce errors
AutoTune,

View File

@@ -5,6 +5,7 @@ use tokio::sync::oneshot::Sender;
use crate::response::FeroxResponse;
use crate::{
event_handlers::Handles,
message::FeroxMessage,
statistics::{StatError, StatField},
traits::FeroxFilter,
@@ -23,7 +24,9 @@ pub enum Command {
AddStatus(StatusCode),
/// Create the progress bar (`BarType::Total`) that is updated from the stats thread
CreateBar,
///
/// the u64 value is the offset at which to start the progress bar (can be 0)
CreateBar(u64),
/// Add to a `Stats` field that corresponds to the given `StatField` by the given `usize` value
AddToUsizeField(StatField, usize),
@@ -78,4 +81,8 @@ pub enum Command {
/// Break out of the (infinite) mpsc receive loop
Exit,
/// Give a handler access to an Arc<Handles> instance after the handler has
/// already been initialized
AddHandles(Arc<Handles>),
}

View File

@@ -101,10 +101,13 @@ impl TermInputHandler {
handles.filters.data.clone(),
);
let state_file = open_file(&filename);
// User didn't set the --no-state flag (so saved_state is still the default true)
if handles.config.save_state {
let state_file = open_file(&filename);
let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?;
let mut buffered_file = state_file?;
write_to(&state, &mut buffered_file, true)?;
}
log::trace!("exit: sigint_handler (end of program)");
std::process::exit(1);

View File

@@ -5,14 +5,13 @@ use anyhow::{Context, Result};
use futures::future::{BoxFuture, FutureExt};
use tokio::sync::{mpsc, oneshot};
use crate::statistics::StatField::TotalExpected;
use crate::{
config::Configuration,
progress::PROGRESS_PRINTER,
response::FeroxResponse,
scanner::RESPONSES,
send_command, skip_fail,
statistics::StatField::ResourcesDiscovered,
statistics::StatField::{ResourcesDiscovered, TotalExpected},
traits::FeroxSerialize,
utils::{ferox_print, fmt_err, make_request, open_file, write_to},
CommandReceiver, CommandSender, Joiner,
@@ -144,6 +143,9 @@ pub struct TermOutHandler {
/// pointer to "global" configuration struct
config: Arc<Configuration>,
/// handles instance
handles: Option<Arc<Handles>>,
}
/// implementation of TermOutHandler
@@ -161,6 +163,7 @@ impl TermOutHandler {
tx_file,
file_task,
config,
handles: None,
}
}
@@ -212,6 +215,9 @@ impl TermOutHandler {
Command::Sync(sender) => {
sender.send(true).unwrap_or_default();
}
Command::AddHandles(handles) => {
self.handles = Some(handles);
}
Command::Exit => {
if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() {
self.file_task.as_mut().unwrap().await??; // wait for death
@@ -236,9 +242,26 @@ impl TermOutHandler {
log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
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, tx_stats.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 should_process_response = contains_sentry && unknown_sentry;
let should_process_response = contains_sentry && unknown_sentry && !should_filter;
if should_process_response {
// print to stdout
@@ -251,7 +274,7 @@ impl TermOutHandler {
self.tx_file
.send(Command::Report(resp.clone()))
.with_context(|| {
fmt_err(&format!("Could not send {} to file handler", resp))
fmt_err(&format!("Could not send {resp} to file handler"))
})?;
}
}
@@ -284,7 +307,7 @@ impl TermOutHandler {
&& matches!(call_type, ProcessResponseCall::Recursive)
{
// --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;
// need to manually adjust stats
@@ -371,11 +394,11 @@ impl TermOutHandler {
if !filename.is_empty() {
// append rules
for suffix in ["~", ".bak", ".bak2", ".old", ".1"] {
self.add_new_url_to_vec(url, &format!("{}{}", filename, suffix), &mut urls);
self.add_new_url_to_vec(url, &format!("{filename}{suffix}"), &mut urls);
}
// vim swap rule
self.add_new_url_to_vec(url, &format!(".{}.swp", filename), &mut urls);
self.add_new_url_to_vec(url, &format!(".{filename}.swp"), &mut urls);
// replace original extension rule
let parts: Vec<_> = filename
@@ -398,6 +421,7 @@ impl TermOutHandler {
#[cfg(test)]
mod tests {
use super::*;
use crate::event_handlers::Command;
#[test]
/// try to hit struct field coverage of FileOutHandler
@@ -408,7 +432,7 @@ mod tests {
config,
receiver: rx,
};
println!("{:?}", foh);
println!("{foh:?}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -417,15 +441,17 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
println!("{:?}", toh);
println!("{toh:?}");
tx.send(Command::Exit).unwrap();
}
@@ -435,12 +461,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
let expected: Vec<_> = vec![
@@ -478,12 +506,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
let expected: Vec<_> = vec![
@@ -521,12 +551,14 @@ mod tests {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let handles = Arc::new(Handles::for_testing(None, None).0);
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
handles: Some(handles),
};
let expected: Vec<_> = vec![

View File

@@ -6,7 +6,7 @@ use tokio::sync::{mpsc, Semaphore};
use crate::{
response::FeroxResponse,
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
scanner::FeroxScanner,
scanner::{FeroxScanner, RESPONSES},
statistics::StatField::TotalScans,
url::FeroxUrl,
utils::should_deny_url,
@@ -218,7 +218,7 @@ impl ScanHandler {
// current number of requests expected per scan
// ExpectedPerScan and TotalExpected are a += action, so we need the wordlist length to
// update them while the other updates use expected_num_requests_per_dir
let num_words = self.get_wordlist()?.len();
let num_words = self.get_wordlist(0)?.len();
let current_expectation = self.handles.expected_num_requests_per_dir() as u64;
// used in the calculation of bar width down below, see explanation there
@@ -290,10 +290,19 @@ impl ScanHandler {
}
/// Helper to easily get the (locked) underlying wordlist
pub fn get_wordlist(&self) -> Result<Arc<Vec<String>>> {
pub fn get_wordlist(&self, offset: usize) -> Result<Arc<Vec<String>>> {
if let Ok(guard) = self.wordlist.lock().as_ref() {
if let Some(list) = guard.as_ref() {
return Ok(list.clone());
return if offset > 0 {
// the offset could be off a bit, so we'll adjust it backwards by 10%
// of the overall wordlist size to ensure we don't miss any words
// (hopefully)
let adjusted_offset = offset - ((offset as f64 * 0.10) as usize);
Ok(Arc::new(list[adjusted_offset..].to_vec()))
} else {
Ok(list.clone())
};
}
}
@@ -328,7 +337,7 @@ impl ScanHandler {
continue;
}
let list = self.get_wordlist()?;
let list = self.get_wordlist(scan.requests() as usize)?;
log::info!("scan handler received {} - beginning scan", target);
@@ -368,8 +377,8 @@ impl ScanHandler {
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
log::trace!("enter: try_recursion({:?})", response,);
if !response.is_directory() {
// not a directory, quick exit
if !self.handles.config.force_recursion && !response.is_directory() {
// not a directory and --force-recursion wasn't used, quick exit
return Ok(());
}
@@ -386,6 +395,58 @@ impl ScanHandler {
return Ok(());
}
if let Ok(responses) = RESPONSES.responses.read() {
for maybe_wild in responses.iter() {
if !maybe_wild.wildcard() || !maybe_wild.is_directory() {
// if the stored response isn't a wildcard, skip it
// if the stored response isn't a directory, skip it
// we're only interested in preventing recursion into wildcard directories
continue;
}
if maybe_wild.method() != response.method() {
// methods don't match, skip it
continue;
}
// methods match and is a directory wildcard
// need to check the wildcard's parent directory
// for equality with the incoming response's parent directory
//
// if the parent directories match, we need to prevent recursion
// into the wildcard directory
match (
maybe_wild.url().path_segments(),
response.url().path_segments(),
) {
// both urls must have path segments
(Some(mut maybe_wild_segments), Some(mut response_segments)) => {
match (
maybe_wild_segments.nth_back(1),
response_segments.nth_back(1),
) {
// both urls must have at least 2 path segments, the next to last being the parent
(Some(maybe_wild_parent), Some(response_parent)) => {
if maybe_wild_parent == response_parent {
// the parent directories match, so we need to prevent recursion
return Ok(());
}
}
_ => {
// we couldn't get the parent directory, so we'll skip this
continue;
}
}
}
_ => {
// we couldn't get the path segments, so we'll skip this
continue;
}
}
}
}
let targets = vec![response.url().to_string()];
self.ordered_scan_url(targets, ScanOrder::Latest).await?;

View File

@@ -115,8 +115,9 @@ impl StatsHandler {
}
}
Command::AddToF64Field(field, value) => self.stats.update_f64_field(field, value),
Command::CreateBar => {
Command::CreateBar(offset) => {
self.bar = add_bar("", self.stats.total_expected() as u64, BarType::Total);
self.bar.set_position(offset);
}
Command::LoadStats(filename) => {
self.stats.merge_from(&filename)?;

View File

@@ -13,6 +13,11 @@ pub(super) const LINKFINDER_REGEX: &str = r#"(?:"|')(((?:[a-zA-Z]{1,10}://|//)[^
pub(super) const ROBOTS_TXT_REGEX: &str =
r#"(?m)^ *(Allow|Disallow): *(?P<url_path>[a-zA-Z0-9._/?#@!&'()+,;%=-]+?)$"#; // multi-line (?m)
/// Regular expression to filter bad characters from extracted url paths
///
/// ref: https://www.rfc-editor.org/rfc/rfc3986#section-2
pub(super) const URL_CHARS_REGEX: &str = r#"["<>\\^`{|} ]"#;
/// Which type of extraction should be performed
#[derive(Debug, Copy, Clone)]
pub enum ExtractionTarget {
@@ -90,6 +95,7 @@ impl<'a> ExtractorBuilder<'a> {
Ok(Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: if self.response.is_some() {
Some(self.response.unwrap())
} else {

View File

@@ -2,7 +2,6 @@ use super::*;
use crate::{
client,
event_handlers::{
Command,
Command::{AddError, AddToUsizeField},
Handles,
},
@@ -12,14 +11,13 @@ use crate::{
StatField::{LinksExtracted, TotalExpected},
},
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,
};
use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url};
use scraper::{Html, Selector};
use std::collections::HashSet;
use tokio::sync::oneshot;
use std::{borrow::Cow, collections::HashSet};
/// Whether an active scan is recursive or not
#[derive(Debug)]
@@ -40,6 +38,9 @@ pub struct Extractor<'a> {
/// `ROBOTS_TXT_REGEX` as a regex::Regex type
pub(super) robots_regex: Regex,
/// regex to validate a url
pub(super) url_regex: Regex,
/// Response from which to extract links
pub(super) response: Option<&'a FeroxResponse>,
@@ -186,11 +187,21 @@ impl<'a> Extractor<'a> {
resp.set_url(&format!("{}/", resp.url()));
}
self.handles
.send_scan_command(Command::TryRecursion(Box::new(resp)))?;
let (tx, rx) = oneshot::channel::<bool>();
self.handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
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(&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");
@@ -212,6 +223,7 @@ impl<'a> Extractor<'a> {
self.extract_links_by_attr(resp_url, links, html, "div", "src");
self.extract_links_by_attr(resp_url, links, html, "frame", "src");
self.extract_links_by_attr(resp_url, links, html, "embed", "src");
self.extract_links_by_attr(resp_url, links, html, "link", "href");
}
/// Given the body of a `reqwest::Response`, perform the following actions
@@ -324,8 +336,9 @@ impl<'a> Extractor<'a> {
let normalized_path = self.normalize_url_path(path);
// filter out any empty strings caused by .split
let mut parts: Vec<&str> = normalized_path
let mut parts: Vec<Cow<_>> = normalized_path
.split('/')
.map(|s| self.url_regex.replace_all(s, ""))
.filter(|s| !s.is_empty())
.collect();
@@ -349,7 +362,7 @@ impl<'a> Extractor<'a> {
// this isn't the last index of the parts array
// ex: /buried/misc/stupidfile.php
// this block skips the file but sees all parent folders
possible_path = format!("{}/", possible_path);
possible_path = format!("{possible_path}/");
}
paths.push(possible_path); // good sub-path found
@@ -382,7 +395,18 @@ impl<'a> Extractor<'a> {
let new_url = old_url
.join(link)
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
.with_context(|| format!("Could not join {old_url} with {link}"))?;
if old_url.domain() != new_url.domain() || old_url.host() != new_url.host() {
// domains/ips are not the same, don't scan things that aren't part of the original
// target url
log::debug!(
"Skipping {} because it's not part of the original target",
new_url
);
log::trace!("exit: add_link_to_set_of_links");
return Ok(());
}
links.insert(new_url.to_string());
@@ -405,7 +429,7 @@ impl<'a> Extractor<'a> {
let scanned_urls = self.handles.ferox_scans()?;
if scanned_urls.get_scan_by_url(&new_url.to_string()).is_some() {
if scanned_urls.get_scan_by_url(new_url.as_ref()).is_some() {
//we've seen the url before and don't need to scan again
log::trace!("exit: request_link -> None");
bail!("previously seen url");

View File

@@ -1,4 +1,4 @@
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX};
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX};
use super::*;
use crate::config::{Configuration, OutputLevel};
use crate::scan_manager::ScanOrder;
@@ -273,6 +273,7 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: Some(&ferox_response),
url: String::new(),
target: ExtractionTarget::ResponseBody,
@@ -301,6 +302,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let extractor = Extractor {
links_regex: Regex::new(LINKFINDER_REGEX).unwrap(),
robots_regex: Regex::new(ROBOTS_TXT_REGEX).unwrap(),
url_regex: Regex::new(URL_CHARS_REGEX).unwrap(),
response: None,
url: srv.url("/api/users/stuff/things"),
target: ExtractionTarget::RobotsTxt,
@@ -310,7 +312,7 @@ async fn request_robots_txt_without_proxy() -> Result<()> {
let resp = extractor.make_extract_request("/robots.txt").await?;
assert!(matches!(resp.status(), &StatusCode::OK));
println!("{}", resp);
println!("{resp}");
assert_eq!(resp.content_length(), 14);
assert_eq!(mock.hits(), 1);
Ok(())

View File

@@ -3,16 +3,16 @@ use std::sync::RwLock;
use anyhow::Result;
use serde::{ser::SerializeSeq, Serialize, Serializer};
use crate::{
event_handlers::Command::AddToUsizeField, response::FeroxResponse,
statistics::StatField::WildcardsFiltered, CommandSender,
};
use crate::response::FeroxResponse;
use super::{
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
};
use crate::{
event_handlers::Command::AddToUsizeField, statistics::StatField::WildcardsFiltered,
CommandSender,
};
/// Container around a collection of `FeroxFilters`s
#[derive(Debug, Default)]
pub struct FeroxFilters {
@@ -76,6 +76,7 @@ impl FeroxFilters {
for filter in filters.iter() {
// wildcard.should_filter goes here
if filter.should_filter_response(response) {
log::debug!("filtering response due to: {:?}", filter);
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
tx_stats
.send(AddToUsizeField(WildcardsFiltered, 1))
@@ -104,6 +105,10 @@ impl Serialize for FeroxFilters {
seq.serialize_element(word_filter).unwrap_or_default();
} else if let Some(size_filter) = filter.as_any().downcast_ref::<SizeFilter>() {
seq.serialize_element(size_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
} else if let Some(status_filter) =
filter.as_any().downcast_ref::<StatusCodeFilter>()
{
@@ -114,10 +119,6 @@ impl Serialize for FeroxFilters {
filter.as_any().downcast_ref::<SimilarityFilter>()
{
seq.serialize_element(similarity_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
}
}
seq.end()

View File

@@ -1,7 +1,7 @@
use super::*;
/// Dummy filter for internal shenanigans
#[derive(Default, Debug, PartialEq)]
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EmptyFilter {}
impl FeroxFilter for EmptyFilter {

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of lines
/// in a Response body; specified using -N|--filter-lines
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinesFilter {
/// Number of lines in a Response's body that should be filtered
pub line_count: usize,

View File

@@ -4,21 +4,20 @@ use std::any::Any;
use std::fmt::Debug;
use crate::response::FeroxResponse;
use crate::traits::{FeroxFilter, FeroxSerialize};
use crate::traits::FeroxFilter;
pub use self::container::FeroxFilters;
pub(crate) use self::empty::EmptyFilter;
pub use self::init::initialize;
pub use self::lines::LinesFilter;
pub use self::regex::RegexFilter;
pub use self::similarity::SimilarityFilter;
pub use self::similarity::{SimilarityFilter, SIM_HASHER};
pub use self::size::SizeFilter;
pub use self::status_code::StatusCodeFilter;
pub(crate) use self::utils::{create_similarity_filter, filter_lookup};
pub use self::wildcard::WildcardFilter;
pub use self::words::WordsFilter;
mod wildcard;
mod status_code;
mod words;
mod lines;
@@ -30,4 +29,5 @@ mod container;
mod tests;
mod init;
mod utils;
mod wildcard;
mod empty;

View File

@@ -1,15 +1,26 @@
use super::*;
use fuzzyhash::FuzzyHash;
use crate::nlp::preprocess;
use gaoya::simhash::{SimHash, SimHashBits, SimSipHasher64};
use lazy_static::lazy_static;
lazy_static! {
/// single instance of the sip hasher used in similarity filtering
pub static ref SIM_HASHER: SimHash<SimSipHasher64, u64, 64> =
SimHash::<SimSipHasher64, u64, 64>::new(SimSipHasher64::new(1, 2));
}
/// maximum hamming distance allowed between two signatures
///
/// ref: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/33026.pdf
/// section: 4.1 Choice of Parameters
const MAX_HAMMING_DISTANCE: usize = 3;
/// Simple implementor of FeroxFilter; used to filter out responses based on the similarity of a
/// Response body with a known response; specified using --filter-similar-to
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimilarityFilter {
/// Hash of Response's body to be used during similarity comparison
pub hash: String,
/// Percentage of similarity at which a page is determined to be a near-duplicate of another
pub threshold: u32,
pub hash: u64,
/// Url originally requested for the similarity filter
pub original_url: String,
@@ -20,20 +31,15 @@ impl FeroxFilter for SimilarityFilter {
/// Check `FeroxResponse::text` against what was requested from the site passed in via
/// --filter-similar-to
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
let other = FuzzyHash::new(&response.text());
if let Ok(result) = FuzzyHash::compare(&self.hash, &other.to_string()) {
return result >= self.threshold;
}
// couldn't hash the response, don't filter
log::warn!("Could not hash body from {}", response.as_str());
false
let other = SIM_HASHER.create_signature(preprocess(response.text()).iter());
self.hash.hamming_distance(&other) <= MAX_HAMMING_DISTANCE
}
/// Compare one SimilarityFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
other
.downcast_ref::<Self>()
.map_or(false, |a| self.hash == a.hash)
}
/// Return self as Any for dynamic dispatch purposes

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the length of a
/// Response body; specified using -S|--filter-size
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SizeFilter {
/// Overall length of a Response's body that should be filtered
pub content_length: u64,

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out status codes specified using
/// -C|--filter-status
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusCodeFilter {
/// Status code that should not be displayed to the user
pub filter_code: u16,

View File

@@ -1,26 +1,38 @@
use super::*;
use ::fuzzyhash::FuzzyHash;
use crate::nlp::preprocess;
use crate::DEFAULT_METHOD;
use ::regex::Regex;
#[test]
/// simply test the default values for wildcardfilter, expect 0, 0
/// simply test the default values for wildcardfilter
fn wildcard_filter_default() {
let wcf = WildcardFilter::default();
assert_eq!(wcf.size, u64::MAX);
assert_eq!(wcf.dynamic, u64::MAX);
assert_eq!(wcf.content_length, None);
assert_eq!(wcf.line_count, None);
assert_eq!(wcf.word_count, None);
assert_eq!(wcf.method, DEFAULT_METHOD.to_string());
assert_eq!(wcf.status_code, 0);
assert!(!wcf.dont_filter);
}
#[test]
/// just a simple test to increase code coverage by hitting as_any and the inner value
fn wildcard_filter_as_any() {
let filter = WildcardFilter::default();
let mut filter = WildcardFilter::default();
let filter2 = WildcardFilter::default();
assert!(filter.box_eq(filter2.as_any()));
assert_eq!(
*filter.as_any().downcast_ref::<WildcardFilter>().unwrap(),
filter
filter2
);
filter.content_length = Some(1);
assert_ne!(
*filter.as_any().downcast_ref::<WildcardFilter>().unwrap(),
filter2
);
}
@@ -111,18 +123,21 @@ fn regex_filter_as_any() {
#[test]
/// test should_filter on WilcardFilter where static logic matches
fn wildcard_should_filter_when_static_wildcard_found() {
let body =
"pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus";
let mut resp = FeroxResponse::default();
resp.set_wildcard(true);
resp.set_url("http://localhost");
resp.set_text(
"pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus",
);
resp.set_text(body);
let filter = WildcardFilter {
size: 83,
dynamic: 0,
content_length: Some(body.len() as u64),
line_count: Some(1),
word_count: Some(10),
method: DEFAULT_METHOD.to_string(),
status_code: 200,
dont_filter: false,
method: "GET".to_owned(),
};
assert!(filter.should_filter_response(&resp));
@@ -136,7 +151,14 @@ fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
resp.set_url("http://localhost");
// default WildcardFilter is used in the code that executes when response.content_length() == 0
let filter = WildcardFilter::new(false);
let filter = WildcardFilter {
content_length: Some(0),
line_count: Some(0),
word_count: Some(0),
method: DEFAULT_METHOD.to_string(),
status_code: 200,
dont_filter: false,
};
assert!(filter.should_filter_response(&resp));
}
@@ -150,17 +172,16 @@ fn wildcard_should_filter_when_dynamic_wildcard_found() {
resp.set_text("pellentesque diam volutpat commodo sed egestas egestas fringilla");
let filter = WildcardFilter {
size: 0,
dynamic: 59, // content-length - 5 (len('stuff'))
content_length: None,
line_count: None,
word_count: Some(8),
method: DEFAULT_METHOD.to_string(),
status_code: 200,
dont_filter: false,
method: "GET".to_owned(),
};
println!("resp: {:?}: filter: {:?}", resp, filter);
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on RegexFilter where regex matches body
fn regexfilter_should_filter_when_regex_matches_on_response_body() {
@@ -186,8 +207,7 @@ fn similarity_filter_is_accurate() {
resp.set_text("sitting");
let mut filter = SimilarityFilter {
hash: FuzzyHash::new("kitten").to_string(),
threshold: 95,
hash: SIM_HASHER.create_signature(["kitten"].iter()),
original_url: "".to_string(),
};
@@ -195,15 +215,15 @@ fn similarity_filter_is_accurate() {
assert!(!filter.should_filter_response(&resp));
resp.set_text("");
filter.hash = String::new();
filter.threshold = 100;
filter.hash = SIM_HASHER.create_signature([""].iter());
// two empty strings are the same, however ssdeep doesn't accept empty strings, expect false
// two empty strings are the same
assert!(!filter.should_filter_response(&resp));
resp.set_text("some data to hash for the purposes of running a test");
filter.hash = FuzzyHash::new("some data to hash for the purposes of running a te").to_string();
filter.threshold = 17;
resp.set_text("some data hash purposes running test");
filter.hash = SIM_HASHER.create_signature(
preprocess("some data to hash for the purposes of running a test").iter(),
);
assert!(filter.should_filter_response(&resp));
}
@@ -212,20 +232,17 @@ fn similarity_filter_is_accurate() {
/// just a simple test to increase code coverage by hitting as_any and the inner value
fn similarity_filter_as_any() {
let filter = SimilarityFilter {
hash: String::from("stuff"),
threshold: 95,
hash: 1,
original_url: "".to_string(),
};
let filter2 = SimilarityFilter {
hash: String::from("stuff"),
threshold: 95,
hash: 1,
original_url: "".to_string(),
};
assert!(filter.box_eq(filter2.as_any()));
assert_eq!(filter.hash, "stuff");
assert_eq!(
*filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
filter

View File

@@ -1,11 +1,12 @@
use super::FeroxFilter;
use super::SimilarityFilter;
use crate::event_handlers::Handles;
use crate::filters::similarity::SIM_HASHER;
use crate::nlp::preprocess;
use crate::response::FeroxResponse;
use crate::utils::logged_request;
use crate::{DEFAULT_METHOD, SIMILARITY_THRESHOLD};
use crate::DEFAULT_METHOD;
use anyhow::Result;
use fuzzyhash::FuzzyHash;
use regex::Regex;
use reqwest::Url;
use std::sync::Arc;
@@ -40,12 +41,10 @@ pub(crate) async fn create_similarity_filter(
fr.parse_extension(handles.clone())?;
}
// hash the response body and store the resulting hash in the filter object
let hash = FuzzyHash::new(&fr.text()).to_string();
let hash = SIM_HASHER.create_signature(preprocess(fr.text()).iter());
Ok(SimilarityFilter {
hash,
threshold: SIMILARITY_THRESHOLD,
original_url: similarity_filter.to_string(),
})
}
@@ -95,8 +94,7 @@ pub(crate) fn filter_lookup(filter_type: &str, filter_value: &str) -> Option<Box
}
"similarity" => {
return Some(Box::new(SimilarityFilter {
hash: String::new(),
threshold: SIMILARITY_THRESHOLD,
hash: 0,
original_url: filter_value.to_string(),
}));
}
@@ -157,8 +155,7 @@ mod tests {
assert_eq!(
filter.as_any().downcast_ref::<SimilarityFilter>().unwrap(),
&SimilarityFilter {
hash: String::new(),
threshold: SIMILARITY_THRESHOLD,
hash: 0,
original_url: "http://localhost".to_string()
}
);
@@ -195,8 +192,7 @@ mod tests {
assert_eq!(
filter,
SimilarityFilter {
hash: "3:YKEpn:Yfp".to_string(),
threshold: SIMILARITY_THRESHOLD,
hash: 14897447612059286329,
original_url: srv.url("/")
}
);

View File

@@ -1,28 +1,29 @@
use console::style;
use super::*;
use crate::{url::FeroxUrl, DEFAULT_METHOD};
use crate::utils::create_report_string;
use crate::{config::OutputLevel, DEFAULT_METHOD};
/// Data holder for two pieces of data needed when auto-filtering out wildcard responses
///
/// `dynamic` is the size of the response that will later be combined with the length
/// of the path of the url requested and used to determine interesting pages from custom
/// 404s where the requested url is reflected back in the response
///
/// `size` is size of the response that should be included with filters passed via runtime
/// configuration and any static wildcard lengths.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
/// Data holder for all relevant data needed when auto-filtering out wildcard responses
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WildcardFilter {
/// size of the response that will later be combined with the length of the path of the url
/// requested
pub dynamic: u64,
/// The content-length of this response, if known
pub content_length: Option<u64>,
/// size of the response that should be included with filters passed via runtime configuration
pub size: u64,
/// The number of lines contained in the body of this response, if known
pub line_count: Option<usize>,
/// The number of words contained in the body of this response, if known
pub word_count: Option<usize>,
/// method used in request that should be included with filters passed via runtime configuration
pub method: String,
/// the status code returned in the response
pub status_code: u16,
/// whether or not the user passed -D on the command line
pub(super) dont_filter: bool,
pub dont_filter: bool,
}
/// implementation of WildcardFilter
@@ -36,22 +37,23 @@ impl WildcardFilter {
}
}
/// implement default that populates both values with u64::MAX
/// implement default that populates `method` with its default value
impl Default for WildcardFilter {
/// populate both values with u64::MAX
fn default() -> Self {
Self {
content_length: None,
line_count: None,
word_count: None,
method: DEFAULT_METHOD.to_string(),
status_code: 0,
dont_filter: false,
size: u64::MAX,
method: DEFAULT_METHOD.to_owned(),
dynamic: u64::MAX,
}
}
}
/// implementation of FeroxFilter for WildcardFilter
impl FeroxFilter for WildcardFilter {
/// Examine size, dynamic, and content_len to determine whether or not the response received
/// Examine size/words/lines and method to determine whether or not the response received
/// is a wildcard response and therefore should be filtered out
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
@@ -64,44 +66,78 @@ impl FeroxFilter for WildcardFilter {
return false;
}
if self.size != u64::MAX
&& self.size == response.content_length()
&& self.method == response.method().as_str()
{
// static wildcard size found during testing
// size isn't default, size equals response length, and auto-filter is on
log::debug!("static wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
if self.method != response.method().as_str() {
// method's don't match, so this response should not be filtered out
log::trace!("exit: should_filter_response -> false");
return false;
}
if self.size == u64::MAX
&& response.content_length() == 0
&& self.method == response.method().as_str()
{
// static wildcard size found during testing
// but response length was zero; pointed out by @Tib3rius
log::debug!("static wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
if self.status_code != response.status().as_u16() {
// status codes don't match, so this response should not be filtered out
log::trace!("exit: should_filter_response -> false");
return false;
}
if self.dynamic != u64::MAX {
// dynamic wildcard offset found during testing
// methods and status codes match at this point, just need to check the other fields
// I'm about to manually split this url path instead of using reqwest::Url's
// builtin parsing. The reason is that they call .split() on the url path
// except that I don't want an empty string taking up the last index in the
// event that the url ends with a forward slash. It's ugly enough to be split
// into its own function for readability.
let url_len = FeroxUrl::path_length_of_url(response.url());
if url_len + self.dynamic == response.content_length() {
log::debug!("dynamic wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
match (self.content_length, self.word_count, self.line_count) {
(Some(cl), Some(wc), Some(lc)) => {
if cl == response.content_length()
&& wc == response.word_count()
&& lc == response.line_count()
{
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(Some(cl), Some(wc), None) => {
if cl == response.content_length() && wc == response.word_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(Some(cl), None, Some(lc)) => {
if cl == response.content_length() && lc == response.line_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, Some(wc), Some(lc)) => {
if wc == response.word_count() && lc == response.line_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(Some(cl), None, None) => {
if cl == response.content_length() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, Some(wc), None) => {
if wc == response.word_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, None, Some(lc)) => {
if lc == response.line_count() {
log::debug!("filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
(None, None, None) => {
unreachable!("wildcard filter without any filters set");
}
}
log::trace!("exit: should_filter_response -> false");
false
}
@@ -116,3 +152,29 @@ impl FeroxFilter for WildcardFilter {
self
}
}
impl std::fmt::Display for WildcardFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = create_report_string(
self.status_code.to_string().as_str(),
self.method.as_str(),
&self
.line_count
.map_or_else(|| "-".to_string(), |x| x.to_string()),
&self
.word_count
.map_or_else(|| "-".to_string(), |x| x.to_string()),
&self
.content_length
.map_or_else(|| "-".to_string(), |x| x.to_string()),
&format!(
"{} found {}-like response and created new filter; toggle off with {}",
style("Auto-filtering").bright().green(),
style("404").red(),
style("--dont-filter").yellow()
),
OutputLevel::Default,
);
write!(f, "{}", msg)
}
}

View File

@@ -2,7 +2,7 @@ use super::*;
/// Simple implementor of FeroxFilter; used to filter out responses based on the number of words
/// in a Response body; specified using -W|--filter-words
#[derive(Default, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WordsFilter {
/// Number of words in a Response's body that should be filtered
pub word_count: usize,

View File

@@ -1,43 +1,24 @@
use std::sync::Arc;
use anyhow::{bail, Result};
use console::style;
use scraper::{Html, Selector};
use uuid::Uuid;
use crate::filters::{SimilarityFilter, WildcardFilter, SIM_HASHER};
use crate::message::FeroxMessage;
use crate::nlp::preprocess;
use crate::scanner::RESPONSES;
use crate::{
config::OutputLevel,
event_handlers::{Command, Handles},
filters::WildcardFilter,
progress::PROGRESS_PRINTER,
response::FeroxResponse,
skip_fail,
url::FeroxUrl,
utils::{ferox_print, fmt_err, logged_request, status_colorizer},
utils::{ferox_print, fmt_err, logged_request},
DEFAULT_METHOD,
};
/// length of a standard UUID, used when determining wildcard responses
const UUID_LENGTH: u64 = 32;
/// wrapper around ugly string formatting
macro_rules! format_template {
($template:expr, $method:expr, $length:expr) => {
format!(
$template,
status_colorizer("WLD"),
$method,
"-",
"-",
"-",
style("auto-filtering").yellow(),
style($length).cyan(),
style("--dont-filter").yellow()
)
};
}
/// enum representing the different servers that `parse_html` can detect when directory listing is
/// enabled
#[derive(Copy, Debug, Clone)]
@@ -68,6 +49,16 @@ pub struct DirListingResult {
pub response: FeroxResponse,
}
/// wrapper around the results of running a wildcard detection against a target web page
#[derive(Copy, Debug, Clone)]
pub enum WildcardResult {
/// variant that represents a wildcard directory
WildcardDirectory(usize),
/// variant that represents the presence of a 404-like response
FourOhFourLike(usize),
}
/// container for heuristics related info
pub struct HeuristicTests {
/// Handles object for event handler interaction
@@ -91,7 +82,7 @@ impl HeuristicTests {
let mut ids = vec![];
for _ in 0..length {
ids.push(Uuid::new_v4().to_simple().to_string());
ids.push(Uuid::new_v4().as_simple().to_string());
}
let unique_id = ids.join("");
@@ -100,171 +91,6 @@ impl HeuristicTests {
unique_id
}
/// wrapper for sending a filter to the filters event handler
fn send_filter(&self, filter: WildcardFilter) -> Result<()> {
self.handles
.filters
.send(Command::AddFilter(Box::new(filter)))
}
/// Tests the given url to see if it issues a wildcard response
///
/// In the event that url returns a wildcard response, a
/// [WildcardFilter](struct.WildcardFilter.html) is created and sent to the filters event
/// handler.
///
/// Returns the number of times to increment the caller's progress bar
pub async fn wildcard(&self, target_url: &str) -> Result<u64> {
log::trace!("enter: wildcard_test({:?})", target_url);
if self.handles.config.dont_filter {
// early return, dont_filter scans don't need tested
log::trace!("exit: wildcard_test -> 0");
return Ok(0);
}
let data = match self.handles.config.data.is_empty() {
true => None,
false => Some(self.handles.config.data.as_slice()),
};
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
for method in self.handles.config.methods.iter() {
let ferox_response = self
.make_wildcard_request(&ferox_url, method.as_str(), data, 1)
.await?;
// found a wildcard response
let mut wildcard = WildcardFilter::new(self.handles.config.dont_filter);
let wc_length = ferox_response.content_length();
if wc_length == 0 {
log::trace!("exit: wildcard_test -> 1");
self.send_filter(wildcard)?;
return Ok(1);
}
// content length of wildcard is non-zero, perform additional tests:
// make a second request, with a known-sized (64) longer request
let resp_two = self
.make_wildcard_request(&ferox_url, method.as_str(), data, 3)
.await?;
let wc2_length = resp_two.content_length();
wildcard.method = resp_two.method().as_str().to_owned();
if wc2_length == wc_length + (UUID_LENGTH * 2) {
// second length is what we'd expect to see if the requested url is
// reflected in the response along with some static content; aka custom 404
let url_len = ferox_url.path_length()?;
wildcard.dynamic = wc_length - url_len;
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let msg = format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n", method, wildcard.dynamic);
ferox_print(&msg, &PROGRESS_PRINTER);
}
} else if wc_length == wc2_length {
wildcard.size = wc_length;
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let msg = format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n", method, wildcard.size);
ferox_print(&msg, &PROGRESS_PRINTER);
}
}
self.send_filter(wildcard)?;
}
log::trace!("exit: wildcard_test");
Ok(2)
}
/// Generates a uuid and appends it to the given target url. The reasoning is that the randomly
/// generated unique string should not exist on and be served by the target web server.
///
/// Once the unique url is created, the request is sent to the server. If the server responds
/// back with a valid status code, the response is considered to be a wildcard response. If that
/// wildcard response has a 3xx status code, that redirection location is displayed to the user.
async fn make_wildcard_request(
&self,
target: &FeroxUrl,
method: &str,
data: Option<&[u8]>,
length: usize,
) -> Result<FeroxResponse> {
log::trace!("enter: make_wildcard_request({}, {})", target, length);
let unique_str = self.unique_string(length);
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
let nonexistent_url = target.format(&unique_str, slash)?;
let response = logged_request(
&nonexistent_url.to_owned(),
method,
data,
self.handles.clone(),
)
.await?;
if self
.handles
.config
.status_codes
.contains(&response.status().as_u16())
{
// found a wildcard response
let mut ferox_response = FeroxResponse::from(
response,
&target.target,
method,
self.handles.config.output_level,
)
.await;
ferox_response.set_wildcard(true);
if self
.handles
.filters
.data
.should_filter_response(&ferox_response, self.handles.stats.tx.clone())
{
bail!("filtered response")
}
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let boxed = Box::new(ferox_response.clone());
self.handles.output.send(Command::Report(boxed))?;
}
log::trace!("exit: make_wildcard_request -> {}", ferox_response);
return Ok(ferox_response);
}
log::trace!("exit: make_wildcard_request -> Err");
bail!("uninteresting status code")
}
/// Simply tries to connect to all given sites before starting to scan
///
/// In the event that no sites can be reached, the program will exit.
@@ -292,12 +118,12 @@ impl HeuristicTests {
) {
if e.to_string().contains(":SSL") {
ferox_print(
&format!("Could not connect to {} due to SSL errors (run with -k to ignore), skipping...", target_url),
&format!("Could not connect to {target_url} due to SSL errors (run with -k to ignore), skipping..."),
&PROGRESS_PRINTER,
);
} else {
ferox_print(
&format!("Could not connect to {}, skipping...", target_url),
&format!("Could not connect to {target_url}, skipping..."),
&PROGRESS_PRINTER,
);
}
@@ -325,7 +151,7 @@ impl HeuristicTests {
// so, instead of `directory_listing("http://localhost") -> None` we get
// `directory_listing("http://localhost/") -> Some(DirListingResult)` if there is
// directory listing beyond the redirect
format!("{}/", target_url)
format!("{target_url}/")
} else {
target_url.to_string()
};
@@ -419,6 +245,268 @@ impl HeuristicTests {
log::trace!("exit: detect_directory_listing -> None");
None
}
/// given a target's base url, attempt to automatically detect its 404 response
/// pattern(s), and then set filters that will exclude those patterns from future
/// responses
pub async fn detect_404_like_responses(
&self,
target_url: &str,
) -> Result<Option<WildcardResult>> {
log::trace!("enter: detect_404_like_responses({:?})", target_url);
if self.handles.config.dont_filter {
// early return, dont_filter scans don't need tested
log::trace!("exit: detect_404_like_responses -> dont_filter is true");
return Ok(None);
}
let mut req_counter = 0;
let data = if self.handles.config.data.is_empty() {
None
} else {
Some(self.handles.config.data.as_slice())
};
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
// 4 is due to the array in the nested for loop below
let mut responses = Vec::with_capacity(4);
// for every method, attempt to id its 404 response
//
// a good example of one where the GET/POST differ is on hackthebox:
// - http://prd.m.rendering-api.interface.htb/api
for method in self.handles.config.methods.iter() {
for (prefix, length) in [("", 1), ("", 3), (".htaccess", 1), ("admin", 1)] {
let path = format!("{prefix}{}", self.unique_string(length));
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
let nonexistent_url = ferox_url.format(&path, slash)?;
// example requests:
// - http://localhost/2fc1077836ad43ab98b7a31c2ca28fea
// - http://localhost/92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
// - http://localhost/.htaccessa005a2131e68449aa26e99029c914c09
// - http://localhost/adminf1d2541e73c44dcb9d1fb7d93334b280
let response =
logged_request(&nonexistent_url, method, data, self.handles.clone()).await;
req_counter += 1;
// continue to next on error
let response = skip_fail!(response);
if !self
.handles
.config
.status_codes
.contains(&response.status().as_u16())
{
// if the response code isn't one that's accepted via -s values, then skip to the next
//
// the default value for -s is all status codes, so unless the user says otherwise
// this won't fire
continue;
}
let ferox_response = FeroxResponse::from(
response,
&ferox_url.target,
method,
self.handles.config.output_level,
)
.await;
responses.push(ferox_response);
}
if responses.len() < 2 {
// don't have enough responses to make a determination, continue to next method
responses.clear();
continue;
}
// Command::AddFilter, &str (bytes/words/lines), usize (i.e. length associated with the type)
let Some(filter) = self.examine_404_like_responses(&responses) else {
// no match was found during analysis of responses
responses.clear();
continue;
};
// report to the user, if appropriate
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
// sentry value to control whether or not to print the filter
// used because we only want to print the same filter once
let mut print_sentry = true;
if let Ok(filters) = self.handles.filters.data.filters.read() {
for other in filters.iter() {
if let Some(other_wildcard) =
other.as_any().downcast_ref::<WildcardFilter>()
{
if &*filter == other_wildcard {
print_sentry = false;
break;
}
}
}
}
if print_sentry {
ferox_print(&format!("{}", filter), &PROGRESS_PRINTER);
}
}
// create the new filter
self.handles.filters.send(Command::AddFilter(filter))?;
// if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line
//
// in addition, we'll create a similarity filter as a fallback
let hash = SIM_HASHER.create_signature(preprocess(responses[0].text()).iter());
let sim_filter = SimilarityFilter {
hash,
original_url: responses[0].url().to_string(),
};
self.handles
.filters
.send(Command::AddFilter(Box::new(sim_filter)))?;
if responses[0].is_directory() {
// response is either a 3XX with a Location header that matches url + '/'
// or it's a 2XX that ends with a '/'
// or it's a 403 that ends with a '/'
// set the wildcard flag to true, so we can check it when preventing
// recursion in event_handlers/scans.rs
responses[0].set_wildcard(true);
// add the response to the global list of responses
RESPONSES.insert(responses[0].clone());
// function-internal magic number, indicates that we've detected a wildcard directory
req_counter += 100;
}
// reset the responses for the next method, if it exists
responses.clear();
}
log::trace!("exit: detect_404_like_responses");
let retval = if req_counter > 100 {
WildcardResult::WildcardDirectory(req_counter)
} else {
WildcardResult::FourOhFourLike(req_counter)
};
Ok(Some(retval))
}
/// for all responses, examine chars/words/lines
/// if all responses respective lengths match each other, we can assume
/// that will remain true for subsequent non-existent urls
///
/// values are examined from most to least specific (content length, word count, line count)
fn examine_404_like_responses(
&self,
responses: &[FeroxResponse],
) -> Option<Box<WildcardFilter>> {
let mut size_sentry = true;
let mut word_sentry = true;
let mut line_sentry = true;
let method = responses[0].method();
let status_code = responses[0].status();
let content_length = responses[0].content_length();
let word_count = responses[0].word_count();
let line_count = responses[0].line_count();
for response in &responses[1..] {
// if any of the responses differ in length, that particular
// response length type is no longer a candidate for filtering
if response.content_length() != content_length {
size_sentry = false;
}
if response.word_count() != word_count {
word_sentry = false;
}
if response.line_count() != line_count {
line_sentry = false;
}
}
if !size_sentry && !word_sentry && !line_sentry {
// none of the response lengths match, so we can't filter on any of them
return None;
}
let mut wildcard = WildcardFilter {
content_length: None,
line_count: None,
word_count: None,
method: method.to_string(),
status_code: status_code.as_u16(),
dont_filter: self.handles.config.dont_filter,
};
match (size_sentry, word_sentry, line_sentry) {
(true, true, true) => {
// all three types of length match, so we can't filter on any of them
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, true, false) => {
// content length and word count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
}
(true, false, true) => {
// content length and line count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.line_count = Some(line_count);
}
(false, true, true) => {
// word count and line count match, so we can filter on either
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, false, false) => {
// content length matches, so we can filter on that
wildcard.content_length = Some(content_length);
}
(false, true, false) => {
// word count matches, so we can filter on that
wildcard.word_count = Some(word_count);
}
(false, false, true) => {
// line count matches, so we can filter on that
wildcard.line_count = Some(line_count);
}
(false, false, false) => {
// none of the length types match, so we can't filter on any of them
unreachable!("no wildcard size matches; handled by the if statement above");
}
};
Some(Box::new(wildcard))
}
}
#[cfg(test)]

View File

@@ -52,9 +52,6 @@ pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Maximum number of file descriptors that can be opened during a scan
pub const DEFAULT_OPEN_FILE_LIMIT: u64 = 8192;
/// Default value used to determine near-duplicate web pages (equivalent to 95%)
pub const SIMILARITY_THRESHOLD: u32 = 95;
/// Default set of extensions to Ignore when auto-collecting extensions during scans
pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
"tif", "tiff", "ico", "cur", "bmp", "webp", "svg", "png", "jpg", "jpeg", "jfif", "gif", "avif",
@@ -65,10 +62,19 @@ pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
/// 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.
///
/// 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`
///
/// 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 =
"/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";
pub const SECONDARY_WORDLIST: &str =
"/usr/local/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
/// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
pub(crate) const SLEEP_DURATION: u64 = 500;
@@ -76,29 +82,73 @@ pub(crate) const SLEEP_DURATION: u64 = 500;
/// The percentage of requests as errors it takes to be deemed too high
pub const HIGH_ERROR_RATIO: f64 = 0.90;
/// Default list of status codes to report
///
/// * 200 Ok
/// * 204 No Content
/// * 301 Moved Permanently
/// * 302 Found
/// * 307 Temporary Redirect
/// * 308 Permanent Redirect
/// * 401 Unauthorized
/// * 403 Forbidden
/// * 405 Method Not Allowed
/// * 500 Internal Server Error
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
/// Default list of status codes to report (all of them)
pub const DEFAULT_STATUS_CODES: [StatusCode; 60] = [
// all 1XX response codes
StatusCode::CONTINUE,
StatusCode::SWITCHING_PROTOCOLS,
StatusCode::PROCESSING,
// all 2XX response codes
StatusCode::OK,
StatusCode::CREATED,
StatusCode::ACCEPTED,
StatusCode::NON_AUTHORITATIVE_INFORMATION,
StatusCode::NO_CONTENT,
StatusCode::RESET_CONTENT,
StatusCode::PARTIAL_CONTENT,
StatusCode::MULTI_STATUS,
StatusCode::ALREADY_REPORTED,
StatusCode::IM_USED,
// all 3XX response codes
StatusCode::MULTIPLE_CHOICES,
StatusCode::MOVED_PERMANENTLY,
StatusCode::FOUND,
StatusCode::SEE_OTHER,
StatusCode::NOT_MODIFIED,
StatusCode::USE_PROXY,
StatusCode::TEMPORARY_REDIRECT,
StatusCode::PERMANENT_REDIRECT,
// all 4XX response codes
StatusCode::BAD_REQUEST,
StatusCode::UNAUTHORIZED,
StatusCode::PAYMENT_REQUIRED,
StatusCode::FORBIDDEN,
StatusCode::NOT_FOUND,
StatusCode::METHOD_NOT_ALLOWED,
StatusCode::NOT_ACCEPTABLE,
StatusCode::PROXY_AUTHENTICATION_REQUIRED,
StatusCode::REQUEST_TIMEOUT,
StatusCode::CONFLICT,
StatusCode::GONE,
StatusCode::LENGTH_REQUIRED,
StatusCode::PRECONDITION_FAILED,
StatusCode::PAYLOAD_TOO_LARGE,
StatusCode::URI_TOO_LONG,
StatusCode::UNSUPPORTED_MEDIA_TYPE,
StatusCode::RANGE_NOT_SATISFIABLE,
StatusCode::EXPECTATION_FAILED,
StatusCode::IM_A_TEAPOT,
StatusCode::MISDIRECTED_REQUEST,
StatusCode::UNPROCESSABLE_ENTITY,
StatusCode::LOCKED,
StatusCode::FAILED_DEPENDENCY,
StatusCode::UPGRADE_REQUIRED,
StatusCode::PRECONDITION_REQUIRED,
StatusCode::TOO_MANY_REQUESTS,
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
// all 5XX response codes
StatusCode::INTERNAL_SERVER_ERROR,
StatusCode::NOT_IMPLEMENTED,
StatusCode::BAD_GATEWAY,
StatusCode::SERVICE_UNAVAILABLE,
StatusCode::GATEWAY_TIMEOUT,
StatusCode::HTTP_VERSION_NOT_SUPPORTED,
StatusCode::VARIANT_ALSO_NEGOTIATES,
StatusCode::INSUFFICIENT_STORAGE,
StatusCode::LOOP_DETECTED,
StatusCode::NOT_EXTENDED,
StatusCode::NETWORK_AUTHENTICATION_REQUIRED,
];
/// Default method for requests

View File

@@ -65,7 +65,7 @@ pub fn initialize(config: Arc<Configuration>) -> Result<()> {
kind: "log".to_string(),
};
PROGRESS_PRINTER.println(&log_entry.as_str());
PROGRESS_PRINTER.println(log_entry.as_str());
if let Some(buffered_file) = file.clone() {
if let Ok(mut unlocked) = buffered_file.write() {

View File

@@ -1,11 +1,14 @@
use std::io::stdin;
use std::{
env::args,
env::{
args,
consts::{ARCH, OS},
},
fs::{create_dir, remove_file, File},
io::{stderr, BufRead, BufReader},
ops::Index,
path::Path,
process::Command,
process::{exit, Command},
sync::{atomic::Ordering, Arc},
};
@@ -17,36 +20,40 @@ use tokio::{
};
use tokio_util::codec::{FramedRead, LinesCodec};
use feroxbuster::scan_manager::ScanType;
use feroxbuster::{
banner::{Banner, UPDATE_URL},
config::{Configuration, OutputLevel},
event_handlers::{
Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist},
Command::{
AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist,
},
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
TermOutHandler, SCAN_COMPLETE,
},
filters, heuristics, logger,
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
scan_manager::{self},
scan_manager::{self, ScanType},
scanner,
utils::{fmt_err, slugify_filename},
SECONDARY_WORDLIST,
};
#[cfg(not(target_os = "windows"))]
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
use lazy_static::lazy_static;
use regex::Regex;
use self_update::cargo_crate_version;
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
/// Create a Vec of Strings from the given wordlist then stores it inside an Arc
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
log::trace!("enter: get_unique_words_from_wordlist({})", path);
let mut trimmed_word = false;
let file = File::open(&path).with_context(|| format!("Could not open {}", path))?;
let file = File::open(path).with_context(|| format!("Could not open {path}"))?;
let reader = BufReader::new(file);
@@ -59,12 +66,21 @@ fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<Vec<String>>> {
for line in reader.lines() {
line.map(|result| {
if !result.starts_with('#') && !result.is_empty() {
words.push(result);
if result.starts_with('/') {
words.push(result.trim_start_matches('/').to_string());
trimmed_word = true;
} else {
words.push(result);
}
}
})
.ok();
}
if trimmed_word {
log::warn!("Some words in the wordlist started with a leading forward-slash; those words were trimmed (i.e. /word -> word)");
}
log::trace!(
"exit: get_unique_words_from_wordlist -> Arc<wordlist[{} words...]>",
words.len()
@@ -90,8 +106,20 @@ async fn scan(targets: Vec<String>, handles: Arc<Handles>) -> Result<()> {
// having been set, makes it so the progress bar doesn't flash as full before anything has
// even happened
if matches!(handles.config.output_level, OutputLevel::Default) {
let mut total_offset = 0;
if let Ok(guard) = handles.scans.read() {
if let Some(handle) = &*guard {
if let Ok(scans) = handle.data.scans.read() {
for scan in scans.iter() {
total_offset += scan.requests_made_so_far();
}
}
}
}
// only create the bar if no --silent|--quiet
handles.stats.send(CreateBar)?;
handles.stats.send(CreateBar(total_offset))?;
// blocks until the bar is created / avoids race condition in first two bars
handles.stats.sync().await?;
@@ -148,7 +176,7 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
}
// remove footgun that arises if a --dont-scan value matches on a base url
for target in &targets {
for target in targets.iter_mut() {
for denier in &handles.config.regex_denylist {
if denier.is_match(target) {
bail!(
@@ -167,6 +195,11 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
);
}
}
if !target.starts_with("http") && !target.starts_with("https") {
// --url hackerone.com
*target = format!("https://{target}");
}
}
log::trace!("exit: get_targets -> {:?}", targets);
@@ -190,10 +223,46 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
PROGRESS_BAR.join().unwrap();
});
// check if update_app is true
if config.update_app {
let target_os = format!("{}-{}", ARCH, OS);
tokio::task::spawn_blocking(move || {
let status = self_update::backends::github::Update::configure()
.repo_owner("epi052")
.repo_name("feroxbuster")
.bin_name("feroxbuster")
.target(target_os.as_str())
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()
.unwrap()
.update()
.unwrap();
println!("Updated version: `{}`!", status.version());
})
.await
.unwrap();
exit(0);
}
// cloning an Arc is cheap (it's basically a pointer into the heap)
// so that will allow for cheap/safe sharing of a single wordlist across multi-target scans
// as well as additional directories found as part of recursion
let words = get_unique_words_from_wordlist(&config.wordlist)?;
let words = match get_unique_words_from_wordlist(&config.wordlist) {
Ok(w) => w,
Err(err) => {
let secondary = Path::new(SECONDARY_WORDLIST);
if secondary.exists() {
eprintln!("Found wordlist in secondary location");
get_unique_words_from_wordlist(SECONDARY_WORDLIST)?
} else {
return Err(err);
}
}
};
if words.len() <= 1 {
// the check is now <= 1 due to the initial empty string added in 2.6.0
@@ -220,6 +289,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone());
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
@@ -308,7 +378,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
let final_path = output_path.with_file_name(&new_folder);
let final_path = output_path.with_file_name(new_folder);
// create the directory or fail silently, assuming the reason for failure is that
// the path exists already
@@ -439,7 +509,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
Ok(_) => {}
Err(e) => {
clean_up(handles, tasks).await?;
bail!(fmt_err(&format!("Failed while scanning: {}", e)));
bail!(fmt_err(&format!("Failed while scanning: {e}")));
}
}
@@ -506,7 +576,7 @@ fn main() -> Result<()> {
{
let future = wrapped_main(config.clone());
if let Err(e) = runtime.block_on(future) {
eprintln!("{}", e);
eprintln!("{e}");
// the code below is to facilitate testing tests/test_banner entries. Since it's an
// integration test, normal test detection (cfg!(test), etc...) won't work. So, in

View File

@@ -23,7 +23,7 @@ impl Document {
document.number_of_terms += processed.len();
for normalized in processed {
if normalized.len() > 2 {
if normalized.len() >= 2 {
document.add_term(&normalized)
}
}
@@ -85,7 +85,7 @@ impl Document {
// at this point, we have a non-empty Text element with a non-script|style parent;
// now we can return the trimmed up string
return Some(format!("{} ", trimmed));
return Some(format!("{trimmed} "));
}
// not an Element node

View File

@@ -8,3 +8,4 @@ mod utils;
pub(crate) use self::document::Document;
pub(crate) use self::model::TfIdf;
pub(crate) use self::utils::preprocess;

View File

@@ -4,7 +4,7 @@ use std::borrow::Cow;
/// pre-processing pipeline wrapper that removes punctuation, normalizes word case (utf-8 included)
/// to lowercase, and remove stop words
pub(super) fn preprocess(text: &str) -> Vec<String> {
pub(crate) fn preprocess(text: &str) -> Vec<String> {
let text = remove_punctuation(text);
let text = normalize_case(text);
let text = remove_stop_words(&text);
@@ -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 {
// 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(
[
'!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?',
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '', '', '', '',
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '', '', '', '', '/',
'', '—', '.',
],
"",
" ",
)
.replace(['/', '', '—', '.'], " ")
}
/// remove stop words from the given string
@@ -86,7 +83,10 @@ mod tests {
fn test_remove_punctuation() {
let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n";
// 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]
@@ -115,7 +115,7 @@ mod tests {
/// ensure preprocess
fn test_preprocess_results() {
let tester = "WHY are Y'all YELLing?";
assert_eq!(&preprocess(tester), &["yall", "yelling"]);
assert_eq!(&preprocess(tester), &["y", "all", "yelling"]);
}
#[test]

View File

@@ -1,3 +1,4 @@
use clap::ArgAction;
use clap::{
crate_authors, crate_description, crate_name, crate_version, Arg, ArgGroup, Command, ValueHint,
};
@@ -25,7 +26,7 @@ lazy_static! {
}
/// Create and return an instance of [clap::App](https://docs.rs/clap/latest/clap/struct.App.html), i.e. the Command Line Interface's configuration
pub fn initialize() -> Command<'static> {
pub fn initialize() -> Command {
let app = Command::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
@@ -39,7 +40,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("url")
.short('u')
.long("url")
.required_unless_present_any(&["stdin", "resume_from"])
.required_unless_present_any(["stdin", "resume_from", "update_app"])
.help_heading("Target selection")
.value_name("URL")
.use_value_delimiter(true)
@@ -50,7 +51,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("stdin")
.long("stdin")
.help_heading("Target selection")
.takes_value(false)
.num_args(0)
.help("Read url(s) from STDIN")
.conflicts_with("url")
)
@@ -62,7 +63,7 @@ pub fn initialize() -> Command<'static> {
.help_heading("Target selection")
.help("State file from which to resume a partially complete scan (ex. --resume-from ferox-1606586780.state)")
.conflicts_with("url")
.takes_value(true),
.num_args(1),
);
/////////////////////////////////////////////////////////////////////
@@ -72,26 +73,32 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("burp")
.long("burp")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(&["proxy", "insecure", "burp_replay"])
.conflicts_with_all(["proxy", "insecure", "burp_replay"])
.help("Set --proxy to http://127.0.0.1:8080 and set --insecure to true"),
)
.arg(
Arg::new("burp_replay")
.long("burp-replay")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(&["replay_proxy", "insecure"])
.conflicts_with_all(["replay_proxy", "insecure"])
.help("Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true"),
)
.arg(
Arg::new("smart")
.long("smart")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"),
).arg(
Arg::new("thorough")
.long("thorough")
.num_args(0)
.help_heading("Composite settings")
.conflicts_with_all(["rate_limit", "auto_bail"])
.help("Use the same settings as --smart and set --collect-extensions to true"),
);
@@ -103,7 +110,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("proxy")
.short('p')
.long("proxy")
.takes_value(true)
.num_args(1)
.value_name("PROXY")
.value_hint(ValueHint::Url)
.help_heading("Proxy settings")
@@ -115,7 +122,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("replay_proxy")
.short('P')
.long("replay-proxy")
.takes_value(true)
.num_args(1)
.value_hint(ValueHint::Url)
.value_name("REPLAY_PROXY")
.help_heading("Proxy settings")
@@ -128,9 +135,8 @@ pub fn initialize() -> Command<'static> {
.short('R')
.long("replay-codes")
.value_name("REPLAY_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.requires("replay_proxy")
.help_heading("Proxy settings")
@@ -148,7 +154,7 @@ pub fn initialize() -> Command<'static> {
.short('a')
.long("user-agent")
.value_name("USER_AGENT")
.takes_value(true)
.num_args(1)
.help_heading("Request settings")
.help(&**DEFAULT_USER_AGENT),
)
@@ -156,7 +162,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("random_agent")
.short('A')
.long("random-agent")
.takes_value(false)
.num_args(0)
.help_heading("Request settings")
.help("Use a random User-Agent"),
)
@@ -165,9 +171,8 @@ pub fn initialize() -> Command<'static> {
.short('x')
.long("extensions")
.value_name("FILE_EXTENSION")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -179,9 +184,8 @@ pub fn initialize() -> Command<'static> {
.short('m')
.long("methods")
.value_name("HTTP_METHODS")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -192,7 +196,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("data")
.long("data")
.value_name("DATA")
.takes_value(true)
.num_args(1)
.help_heading("Request settings")
.help(
"Request's Body; can read data from a file if input starts with an @ (ex: @post.bin)",
@@ -203,10 +207,9 @@ pub fn initialize() -> Command<'static> {
.short('H')
.long("headers")
.value_name("HEADER")
.takes_value(true)
.num_args(1..)
.action(ArgAction::Append)
.help_heading("Request settings")
.multiple_values(true)
.multiple_occurrences(true)
.use_value_delimiter(true)
.help(
"Specify HTTP headers to be used in each request (ex: -H Header:val -H 'stuff: things')",
@@ -217,9 +220,8 @@ pub fn initialize() -> Command<'static> {
.short('b')
.long("cookies")
.value_name("COOKIE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -231,9 +233,8 @@ pub fn initialize() -> Command<'static> {
.short('Q')
.long("query")
.value_name("QUERY")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request settings")
.help(
@@ -245,7 +246,7 @@ pub fn initialize() -> Command<'static> {
.short('f')
.long("add-slash")
.help_heading("Request settings")
.takes_value(false)
.num_args(0)
.help("Append / to each request's URL")
);
@@ -256,9 +257,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("url_denylist")
.long("dont-scan")
.value_name("URL")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Request filters")
.help("URL(s) or Regex Pattern(s) to exclude from recursion/scans"),
@@ -273,9 +273,8 @@ pub fn initialize() -> Command<'static> {
.short('S')
.long("filter-size")
.value_name("SIZE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -287,9 +286,8 @@ pub fn initialize() -> Command<'static> {
.short('X')
.long("filter-regex")
.value_name("REGEX")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -301,9 +299,8 @@ pub fn initialize() -> Command<'static> {
.short('W')
.long("filter-words")
.value_name("WORDS")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -315,9 +312,8 @@ pub fn initialize() -> Command<'static> {
.short('N')
.long("filter-lines")
.value_name("LINES")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
@@ -329,10 +325,10 @@ pub fn initialize() -> Command<'static> {
.short('C')
.long("filter-status")
.value_name("STATUS_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.conflicts_with("status_codes")
.help_heading("Response filters")
.help(
"Filter out status codes (deny list) (ex: -C 200 -C 401)",
@@ -342,9 +338,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("filter_similar")
.long("filter-similar-to")
.value_name("UNWANTED_PAGE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.value_hint(ValueHint::Url)
.use_value_delimiter(true)
.help_heading("Response filters")
@@ -357,13 +352,12 @@ pub fn initialize() -> Command<'static> {
.short('s')
.long("status-codes")
.value_name("STATUS_CODE")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Response filters")
.help(
"Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)",
"Status Codes to include (allow list) (default: All Status Codes)",
),
);
@@ -376,7 +370,7 @@ pub fn initialize() -> Command<'static> {
.short('T')
.long("timeout")
.value_name("SECONDS")
.takes_value(true)
.num_args(1)
.help_heading("Client settings")
.help("Number of seconds before a client's request times out (default: 7)"),
)
@@ -384,7 +378,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("redirects")
.short('r')
.long("redirects")
.takes_value(false)
.num_args(0)
.help_heading("Client settings")
.help("Allow client to follow redirects"),
)
@@ -392,7 +386,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("insecure")
.short('k')
.long("insecure")
.takes_value(false)
.num_args(0)
.help_heading("Client settings")
.help("Disables TLS certificate validation in the client"),
);
@@ -406,7 +400,7 @@ pub fn initialize() -> Command<'static> {
.short('t')
.long("threads")
.value_name("THREADS")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Number of concurrent threads (default: 50)"),
)
@@ -414,7 +408,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("no_recursion")
.short('n')
.long("no-recursion")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Do not scan recursively"),
)
@@ -423,14 +417,21 @@ pub fn initialize() -> Command<'static> {
.short('d')
.long("depth")
.value_name("RECURSION_DEPTH")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
).arg(
Arg::new("force_recursion")
.long("force-recursion")
.num_args(0)
.conflicts_with("no_recursion")
.help_heading("Scan settings")
.help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
).arg(
Arg::new("extract_links")
.short('e')
.long("extract-links")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings")
)
@@ -439,7 +440,7 @@ pub fn initialize() -> Command<'static> {
.short('L')
.long("scan-limit")
.value_name("SCAN_LIMIT")
.takes_value(true)
.num_args(1)
.help_heading("Scan settings")
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
)
@@ -447,7 +448,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("parallel")
.long("parallel")
.value_name("PARALLEL_SCANS")
.takes_value(true)
.num_args(1)
.requires("stdin")
.help_heading("Scan settings")
.help("Run parallel feroxbuster instances (one child process per url passed via stdin)")
@@ -456,7 +457,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("rate_limit")
.long("rate-limit")
.value_name("RATE_LIMIT")
.takes_value(true)
.num_args(1)
.conflicts_with("auto_tune")
.help_heading("Scan settings")
.help("Limit number of requests per second (per directory) (default: 0, i.e. no limit)")
@@ -465,8 +466,8 @@ pub fn initialize() -> Command<'static> {
Arg::new("time_limit")
.long("time-limit")
.value_name("TIME_SPEC")
.takes_value(true)
.validator(valid_time_spec)
.num_args(1)
.value_parser(valid_time_spec)
.help_heading("Scan settings")
.help("Limit total run time of all scans (ex: --time-limit 10m)")
)
@@ -474,15 +475,16 @@ pub fn initialize() -> Command<'static> {
Arg::new("wordlist")
.short('w')
.long("wordlist")
.required_unless_present_any(["update_app"])
.value_hint(ValueHint::FilePath)
.value_name("FILE")
.help("Path to the wordlist")
.help_heading("Scan settings")
.takes_value(true),
.num_args(1),
).arg(
Arg::new("auto_tune")
.long("auto-tune")
.takes_value(false)
.num_args(0)
.conflicts_with("auto_bail")
.help_heading("Scan settings")
.help("Automatically lower scan rate when an excessive amount of errors are encountered")
@@ -490,35 +492,35 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("auto_bail")
.long("auto-bail")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Automatically stop scanning when an excessive amount of errors are encountered")
).arg(
Arg::new("dont_filter")
.short('D')
.long("dont-filter")
.takes_value(false)
.num_args(0)
.help_heading("Scan settings")
.help("Don't auto-filter wildcard responses")
).arg(
Arg::new("collect_extensions")
.short('E')
.long("collect-extensions")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically discover extensions and add them to --extensions (unless they're in --dont-collect)")
).arg(
Arg::new("collect_backups")
.short('B')
.long("collect-backups")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically request likely backup extensions for \"found\" urls")
).arg(
Arg::new("collect_words")
.short('g')
.long("collect-words")
.takes_value(false)
.num_args(0)
.help_heading("Dynamic collection settings")
.help("Automatically discover important words from within responses and add them to the wordlist")
).arg(
@@ -526,9 +528,8 @@ pub fn initialize() -> Command<'static> {
.short('I')
.long("dont-collect")
.value_name("FILE_EXTENSION")
.takes_value(true)
.multiple_values(true)
.multiple_occurrences(true)
.num_args(1..)
.action(ArgAction::Append)
.use_value_delimiter(true)
.help_heading("Dynamic collection settings")
.help(
@@ -544,15 +545,15 @@ pub fn initialize() -> Command<'static> {
Arg::new("verbosity")
.short('v')
.long("verbosity")
.takes_value(false)
.multiple_occurrences(true)
.num_args(0)
.action(ArgAction::Count)
.conflicts_with("silent")
.help_heading("Output settings")
.help("Increase verbosity level (use -vv or more for greater effect. [CAUTION] 4 -v's is probably too much)"),
).arg(
Arg::new("silent")
.long("silent")
.takes_value(false)
.num_args(0)
.conflicts_with("quiet")
.help_heading("Output settings")
.help("Only print URLs + turn off logging (good for piping a list of urls to other commands)")
@@ -561,7 +562,7 @@ pub fn initialize() -> Command<'static> {
Arg::new("quiet")
.short('q')
.long("quiet")
.takes_value(false)
.num_args(0)
.help_heading("Output settings")
.help("Hide progress bars and banner (good for tmux windows w/ notifications)")
)
@@ -569,7 +570,7 @@ pub fn initialize() -> Command<'static> {
.arg(
Arg::new("json")
.long("json")
.takes_value(false)
.num_args(0)
.requires("output_files")
.help_heading("Output settings")
.help("Emit JSON logs to --output and --debug-log instead of normal text")
@@ -581,7 +582,7 @@ pub fn initialize() -> Command<'static> {
.value_name("FILE")
.help_heading("Output settings")
.help("Output file to write results to (use w/ --json for JSON entries)")
.takes_value(true),
.num_args(1),
)
.arg(
Arg::new("debug_log")
@@ -590,12 +591,12 @@ pub fn initialize() -> Command<'static> {
.value_hint(ValueHint::FilePath)
.help_heading("Output settings")
.help("Output file to write log entries (use w/ --json for JSON entries)")
.takes_value(true),
.num_args(1),
)
.arg(
Arg::new("no_state")
.long("no-state")
.takes_value(false)
.num_args(0)
.help_heading("Output settings")
.help("Disable state output file (*.state)")
);
@@ -606,9 +607,18 @@ pub fn initialize() -> Command<'static> {
let mut app = app
.group(
ArgGroup::new("output_files")
.args(&["debug_log", "output"])
.args(["debug_log", "output"])
.multiple(true),
)
.arg(
Arg::new("update_app")
.short('U')
.long("update")
.conflicts_with_all(["url", "wordlist"])
.num_args(0)
.help_heading("Update settings")
.help("Update the app to the latest version"),
)
.after_long_help(EPILOGUE);
/////////////////////////////////////////////////////////////////////
@@ -634,13 +644,12 @@ pub fn initialize() -> Command<'static> {
}
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
fn valid_time_spec(time_spec: &str) -> Result<(), String> {
fn valid_time_spec(time_spec: &str) -> Result<String, String> {
match TIMESPEC_REGEX.is_match(time_spec) {
true => Ok(()),
true => Ok(time_spec.to_string()),
false => {
let msg = format!(
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {}",
time_spec
"Expected a non-negative, whole number followed by s, m, h, or d (case insensitive); received {time_spec}"
);
Err(msg)
}
@@ -668,7 +677,7 @@ EXAMPLES:
cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
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
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
@@ -680,7 +689,7 @@ EXAMPLES:
./feroxbuster -u http://127.1 --extract-links
Ludicrous speed... go!
./feroxbuster -u http://127.1 -threads 200
./feroxbuster -u http://127.1 --threads 200
Limit to a total of 60 active requests at any given time (threads * scan limit)
./feroxbuster -u http://127.1 --threads 30 --scan-limit 2

View File

@@ -223,6 +223,14 @@ impl FeroxResponse {
.with_context(|| "Could not parse body from response")
.unwrap_or_default();
// in the event that the content_length was 0, we can try to get the length
// of the body we just parsed. At worst, it's still 0; at best we've accounted
// for sites that reply without a content-length header and yet still have
// contents in the body.
//
// thanks to twitter use @f3rn0s for pointing out the possibility
let content_length = content_length.max(text.len() as u64);
let line_count = text.lines().count();
let word_count = text.lines().map(|s| s.split_whitespace().count()).sum();
@@ -279,7 +287,9 @@ impl FeroxResponse {
if handles
.config
.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
// status codes are handled by should_filter, but we need to still check against
@@ -413,7 +423,18 @@ impl FeroxSerialize for FeroxResponse {
.get("Location")
.unwrap() // known Some() already
.to_str()
.unwrap_or("Unknown");
.unwrap_or("Unknown")
.to_string();
let loc = if loc.starts_with('/') {
if let Ok(joined) = self.url().join(&loc) {
joined.to_string()
} else {
loc
}
} else {
loc
};
// prettify the redirect target
let loc = style(loc).yellow();
@@ -432,7 +453,7 @@ impl FeroxSerialize for FeroxResponse {
// create the base message
let mut message = format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c Got {} for {} (url length: {})\n",
"{} {:>8} {:>8}l {:>8}w {:>8}c Got {} for {}\n",
wild_status,
method,
lines,
@@ -440,7 +461,6 @@ impl FeroxSerialize for FeroxResponse {
chars,
status_colorizer(status),
self.url(),
FeroxUrl::path_length_of_url(&self.url)
);
if self.status().is_redirection() {

View File

@@ -116,8 +116,8 @@ impl Menu {
let padded_name = pad_str(&name, longest, Alignment::Center, None);
let header = format!("{}\n{}\n{}", border, padded_name, border);
let footer = format!("{}\n{}", commands, border);
let header = format!("{border}\n{padded_name}\n{border}");
let footer = format!("{commands}\n{border}");
Self {
header,
@@ -174,7 +174,7 @@ impl Menu {
.to_string()
.parse::<usize>()
.unwrap_or_else(|e| {
self.println(&format!("Found non-numeric input: {}: {:?}", e, value));
self.println(&format!("Found non-numeric input: {e}: {value:?}"));
0
})
}
@@ -198,7 +198,7 @@ impl Menu {
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));
self.println(&format!("Found invalid range of scans: {value}"));
continue;
}
@@ -290,8 +290,7 @@ impl Menu {
/// Given a url, confirm with user that we should cancel
pub(super) fn confirm_cancellation(&self, url: &str) -> char {
self.println(&format!(
"You sure you wanna cancel this scan: {}? [Y/n]",
url
"You sure you wanna cancel this scan: {url}? [Y/n]"
));
self.term.read_char().unwrap_or('n')

View File

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

View File

@@ -32,6 +32,9 @@ pub struct FeroxScan {
/// The URL that to be scanned
pub(super) url: String,
/// A url used solely for comparison to other URLs
pub(super) normalized_url: String,
/// The type of scan
pub scan_type: ScanType,
@@ -41,6 +44,12 @@ pub struct FeroxScan {
/// Number of requests to populate the progress bar with
pub(super) num_requests: u64,
/// Number of requests made so far, only used during deserialization
///
/// serialization: saves self.requests() to this field
/// deserialization: sets self.requests_made_so_far to this field
pub(super) requests_made_so_far: u64,
/// Status of this scan
pub status: Mutex<ScanStatus>,
@@ -70,15 +79,17 @@ pub struct FeroxScan {
impl Default for FeroxScan {
/// Create a default FeroxScan, populates ID with a new UUID
fn default() -> Self {
let new_id = Uuid::new_v4().to_simple().to_string();
let new_id = Uuid::new_v4().as_simple().to_string();
FeroxScan {
id: new_id,
task: sync::Mutex::new(None), // tokio mutex
status: Mutex::new(ScanStatus::default()),
num_requests: 0,
requests_made_so_far: 0,
scan_order: ScanOrder::Latest,
url: String::new(),
normalized_url: String::new(),
progress_bar: Mutex::new(None),
scan_type: ScanType::File,
output_level: Default::default(),
@@ -118,6 +129,11 @@ impl FeroxScan {
&self.url
}
/// getter for number of requests made during previously saved scans (i.e. --resume-from used)
pub fn requests_made_so_far(&self) -> u64 {
self.requests_made_so_far
}
/// small wrapper to set the JoinHandle
pub async fn set_task(&self, task: JoinHandle<()>) -> Result<()> {
let mut guard = self.task.lock().await;
@@ -158,6 +174,8 @@ impl FeroxScan {
let pb = add_bar(&self.url, self.num_requests, bar_type);
pb.reset_elapsed();
pb.set_position(self.requests_made_so_far);
let _ = std::mem::replace(&mut *guard, Some(pb.clone()));
pb
@@ -191,6 +209,7 @@ impl FeroxScan {
) -> Arc<Self> {
Arc::new(Self {
url: url.to_string(),
normalized_url: format!("{}/", url.trim_end_matches('/')),
scan_type,
scan_order,
num_requests,
@@ -228,6 +247,14 @@ impl FeroxScan {
false
}
/// small wrapper to inspect ScanStatus and see if it's Cancelled
pub fn is_cancelled(&self) -> bool {
if let Ok(guard) = self.status.lock() {
return matches!(*guard, ScanStatus::Cancelled);
}
false
}
/// await a task's completion, similar to a thread's join; perform necessary bookkeeping
pub async fn join(&self) {
log::trace!("enter join({:?})", self);
@@ -264,6 +291,7 @@ impl FeroxScan {
PolicyTrigger::Status403 => self.status_403s(),
PolicyTrigger::Status429 => self.status_429s(),
PolicyTrigger::Errors => self.errors(),
PolicyTrigger::TryAdjustUp => 0,
}
}
@@ -332,13 +360,15 @@ impl Serialize for FeroxScan {
where
S: Serializer,
{
let mut state = serializer.serialize_struct("FeroxScan", 4)?;
let mut state = serializer.serialize_struct("FeroxScan", 6)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("url", &self.url)?;
state.serialize_field("normalized_url", &self.normalized_url)?;
state.serialize_field("scan_type", &self.scan_type)?;
state.serialize_field("status", &self.status)?;
state.serialize_field("num_requests", &self.num_requests)?;
state.serialize_field("requests_made_so_far", &self.requests())?;
state.end()
}
@@ -387,11 +417,21 @@ impl<'de> Deserialize<'de> for FeroxScan {
scan.url = url.to_string();
}
}
"normalized_url" => {
if let Some(normalized_url) = value.as_str() {
scan.normalized_url = normalized_url.to_string();
}
}
"num_requests" => {
if let Some(num_requests) = value.as_u64() {
scan.num_requests = num_requests;
}
}
"requests_made_so_far" => {
if let Some(requests_made_so_far) = value.as_u64() {
scan.requests_made_so_far = requests_made_so_far;
}
}
_ => {}
}
}
@@ -480,9 +520,11 @@ mod tests {
let scan = FeroxScan {
id: "".to_string(),
url: "".to_string(),
normalized_url: String::from("/"),
scan_type: ScanType::Directory,
scan_order: ScanOrder::Initial,
num_requests: 0,
requests_made_so_far: 0,
status: Mutex::new(ScanStatus::Running),
task: Default::default(),
progress_bar: Mutex::new(None),

View File

@@ -8,6 +8,7 @@ use crate::filters::{
use crate::traits::FeroxFilter;
use crate::Command::AddFilter;
use crate::{
banner::Banner,
config::OutputLevel,
progress::PROGRESS_PRINTER,
progress::{add_bar, BarType},
@@ -75,7 +76,7 @@ impl Serialize for FeroxScans {
let mut seq = serializer.serialize_seq(Some(scans.len() + 1))?;
for scan in scans.iter() {
seq.serialize_element(&*scan).unwrap_or_default();
seq.serialize_element(scan).unwrap_or_default();
}
seq.end()
}
@@ -138,6 +139,15 @@ impl FeroxScans {
let mut deser_scan: FeroxScan =
serde_json::from_value(scan.clone()).unwrap_or_default();
if deser_scan.is_cancelled() {
// if the scan was cancelled by the user, mark it as complete. This will
// prevent the scan from being resumed as well as prevent the wordlist
// from requesting it again
if let Ok(mut guard) = deser_scan.status.lock() {
*guard = ScanStatus::Complete;
}
}
// FeroxScans gets -q value from config as usual; the FeroxScans themselves
// rely on that value being passed in. If the user starts a scan without -q
// and resumes the scan but adds -q, FeroxScan will not have the proper value
@@ -213,8 +223,10 @@ impl FeroxScans {
/// on the given URL
pub fn contains(&self, url: &str) -> bool {
if let Ok(scans) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in scans.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return true;
}
}
@@ -225,8 +237,10 @@ impl FeroxScans {
/// Find and return a `FeroxScan` based on the given URL
pub fn get_scan_by_url(&self, url: &str) -> Option<Arc<FeroxScan>> {
if let Ok(guard) = self.scans.read() {
let normalized = format!("{}/", url.trim_end_matches('/'));
for scan in guard.iter() {
if scan.url == url {
if scan.normalized_url == normalized {
return Some(scan.clone());
}
}
@@ -258,7 +272,7 @@ impl FeroxScans {
for (idx, _) in &matches {
for scan in guard.iter() {
let slice = url.index(0..*idx);
if slice == scan.url || format!("{}/", slice).as_str() == scan.url {
if slice == scan.url || format!("{slice}/").as_str() == scan.url {
log::trace!("enter: get_base_scan_by_url -> {}", scan);
return Some(scan.clone());
}
@@ -323,7 +337,7 @@ impl FeroxScans {
}
// we're only interested in displaying directory scans, as those are
// the only ones that make sense to be stopped
let scan_msg = format!("{:3}: {}", i, scan);
let scan_msg = format!("{i:3}: {scan}");
self.menu.println(&scan_msg);
printed += 1;
}
@@ -347,7 +361,7 @@ impl FeroxScans {
if num >= u_scans.len() {
// usize can't be negative, just need to handle exceeding bounds
self.menu
.println(&format!("The number {} is not a valid choice.", num));
.println(&format!("The number {num} is not a valid choice."));
sleep(menu_pause_duration);
continue;
}
@@ -437,6 +451,12 @@ impl FeroxScans {
};
self.menu.clear_screen();
let banner = Banner::new(&[handles.config.target_url.clone()], &handles.config);
banner
.print_to(&self.menu.term, handles.config.clone())
.unwrap_or_default();
self.menu.show_progress_bars();
result
@@ -589,7 +609,8 @@ impl FeroxScans {
///
/// Also return a reference to the new `FeroxScan`
pub fn add_directory_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
self.add_scan(url, ScanType::Directory, scan_order)
let normalized = format!("{}/", url.trim_end_matches('/'));
self.add_scan(&normalized, ScanType::Directory, scan_order)
}
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan

View File

@@ -58,7 +58,7 @@ impl FeroxState {
impl FeroxSerialize for FeroxState {
/// Simply return debug format of FeroxState to satisfy as_str
fn as_str(&self) -> String {
format!("{:?}", self)
format!("{self:?}")
}
/// Simple call to produce a JSON string using the given FeroxState

View File

@@ -10,7 +10,7 @@ use crate::{
scanner::RESPONSES,
statistics::Stats,
traits::FeroxSerialize,
SIMILARITY_THRESHOLD, SLEEP_DURATION, VERSION,
SLEEP_DURATION, VERSION,
};
use indicatif::ProgressBar;
use predicates::prelude::*;
@@ -224,7 +224,7 @@ fn ferox_scan_get_progress_bar_when_none_is_set() {
/// given a JSON entry representing a FeroxScan, test that it deserializes into the proper type
/// with the right attributes
fn ferox_scan_deserialize() {
let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","status":"Complete"}"#;
let fs_json = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Directory","status":"Complete","requests_made_so_far":500}"#;
let fs_json_two = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"Cancelled"}"#;
let fs_json_three = r#"{"id":"057016a14769414aac9a7a62707598cb","url":"https://spiritanimal.com","scan_type":"Not Correct","status":"","num_requests":42}"#;
@@ -246,9 +246,13 @@ fn ferox_scan_deserialize() {
ScanType::File => {}
}
match *fs.progress_bar.lock().unwrap() {
None => {}
Some(_) => {
match fs.progress_bar.lock() {
Ok(guard) => {
if guard.is_some() {
panic!();
}
}
Err(_) => {
panic!();
}
}
@@ -277,7 +281,7 @@ fn ferox_scan_serialize() {
None,
);
let fs_json = format!(
r#"{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}"#,
r#"{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}"#,
fs.id
);
assert_eq!(fs_json, serde_json::to_string(&*fs).unwrap());
@@ -296,7 +300,7 @@ fn ferox_scans_serialize() {
);
let ferox_scans = FeroxScans::default();
let ferox_scans_json = format!(
r#"[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}]"#,
r#"[{{"id":"{}","url":"https://spiritanimal.com","normalized_url":"https://spiritanimal.com/","scan_type":"Directory","status":"NotStarted","num_requests":0,"requests_made_so_far":0}}]"#,
ferox_scan.id
);
ferox_scans.scans.write().unwrap().push(ferox_scan);
@@ -317,7 +321,7 @@ fn ferox_responses_serialize() {
// responses has a response now
// serialized should be a list of responses
let expected = format!("[{}]", json_response);
let expected = format!("[{json_response}]");
let serialized = serde_json::to_string(&responses).unwrap();
assert_eq!(expected, serialized);
@@ -399,8 +403,7 @@ fn feroxstates_feroxserialize_implementation() {
.unwrap();
filters
.push(Box::new(SimilarityFilter {
hash: "3:YKEpn:Yfp".to_string(),
threshold: SIMILARITY_THRESHOLD,
hash: 1,
original_url: "http://localhost:12345/".to_string(),
}))
.unwrap();
@@ -426,11 +429,11 @@ fn feroxstates_feroxserialize_implementation() {
let json_state = ferox_state.as_json().unwrap();
println!("echo '{}'|jq", json_state); // for debugging, if the test fails, can see what's going on
println!("echo '{json_state}'|jq"); // for debugging, if the test fails, can see what's going on
for expected in [
r#""scans""#,
&format!(r#""id":"{}""#, saved_id),
&format!(r#""id":"{saved_id}""#),
r#""url":"https://spiritanimal.com""#,
r#""scan_type":"Directory""#,
r#""status":"NotStarted""#,
@@ -442,8 +445,8 @@ fn feroxstates_feroxserialize_implementation() {
r#""proxy":"""#,
r#""replay_proxy":"""#,
r#""target_url":"""#,
r#""status_codes":[200,204,301,302,307,308,401,403,405,500]"#,
r#""replay_codes":[200,204,301,302,307,308,401,403,405,500]"#,
r#""status_codes":[100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511,103,425]"#,
r#""replay_codes":[100,101,102,200,201,202,203,204,205,206,207,208,226,300,301,302,303,304,305,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511,103,425]"#,
r#""filter_status":[]"#,
r#""threads":50"#,
r#""timeout":7"#,
@@ -452,10 +455,11 @@ fn feroxstates_feroxserialize_implementation() {
r#""quiet":false"#,
r#""auto_bail":false"#,
r#""auto_tune":false"#,
r#""force_recursion":false"#,
r#""json":false"#,
r#""output":"""#,
r#""debug_log":"""#,
&format!(r#""user_agent":"feroxbuster/{}""#, VERSION),
&format!(r#""user_agent":"feroxbuster/{VERSION}""#),
r#""random_agent":false"#,
r#""redirects":false"#,
r#""insecure":false"#,
@@ -498,7 +502,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""collect_extensions":true"#,
r#""collect_backups":false"#,
r#""collect_words":false"#,
r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":"3:YKEpn:Yfp","threshold":95,"original_url":"http://localhost:12345/"}]"#,
r#""filters":[{"filter_code":100},{"word_count":200},{"content_length":300},{"line_count":400},{"compiled":".*","raw_string":".*"},{"hash":1,"original_url":"http://localhost:12345/"}]"#,
r#""collected_extensions":["php"]"#,
r#""dont_collect":["tif","tiff","ico","cur","bmp","webp","svg","png","jpg","jpeg","jfif","gif","avif","apng","pjpeg","pjp","mov","wav","mpg","mpeg","mp3","mp4","m4a","m4p","m4v","ogg","webm","ogv","oga","flac","aac","3gp","css","zip","xls","xml","gz","tgz"]"#,
]
@@ -555,9 +559,11 @@ fn feroxscan_display() {
let scan = FeroxScan {
id: "".to_string(),
url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest,
scan_type: Default::default(),
num_requests: 0,
requests_made_so_far: 0,
start_time: Instant::now(),
output_level: OutputLevel::Default,
status_403s: Default::default(),
@@ -568,26 +574,26 @@ fn feroxscan_display() {
errors: Default::default(),
};
let not_started = format!("{}", scan);
let not_started = format!("{scan}");
assert!(predicate::str::contains("not started")
.and(predicate::str::contains("localhost"))
.eval(&not_started));
scan.set_status(ScanStatus::Complete).unwrap();
let complete = format!("{}", scan);
let complete = format!("{scan}");
assert!(predicate::str::contains("complete")
.and(predicate::str::contains("localhost"))
.eval(&complete));
scan.set_status(ScanStatus::Cancelled).unwrap();
let cancelled = format!("{}", scan);
let cancelled = format!("{scan}");
assert!(predicate::str::contains("cancelled")
.and(predicate::str::contains("localhost"))
.eval(&cancelled));
scan.set_status(ScanStatus::Running).unwrap();
let running = format!("{}", scan);
let running = format!("{scan}");
assert!(predicate::str::contains("running")
.and(predicate::str::contains("localhost"))
.eval(&running));
@@ -599,9 +605,11 @@ async fn ferox_scan_abort() {
let scan = FeroxScan {
id: "".to_string(),
url: String::from("http://localhost"),
normalized_url: String::from("http://localhost/"),
scan_order: ScanOrder::Latest,
scan_type: Default::default(),
num_requests: 0,
requests_made_so_far: 0,
start_time: Instant::now(),
output_level: OutputLevel::Default,
status_403s: Default::default(),
@@ -633,10 +641,7 @@ fn menu_print_header_and_footer() {
let menu_cmd_2 = MenuCmd::Cancel(vec![0], false);
let menu_cmd_res_1 = MenuCmdResult::Url(String::from("http://localhost"));
let menu_cmd_res_2 = MenuCmdResult::NumCancelled(2);
println!(
"{:?}{:?}{:?}{:?}",
menu_cmd_1, menu_cmd_2, menu_cmd_res_1, menu_cmd_res_2
);
println!("{menu_cmd_1:?}{menu_cmd_2:?}{menu_cmd_res_1:?}{menu_cmd_res_2:?}");
menu.clear_screen();
menu.print_header();
menu.print_footer();
@@ -653,9 +658,9 @@ fn menu_get_command_input_from_user_returns_cancel() {
let force = idx % 2 == 0;
let full_cmd = if force {
format!("{} -f {}\n", cmd, idx)
format!("{cmd} -f {idx}\n")
} else {
format!("{} {}\n", cmd, idx)
format!("{cmd} {idx}\n")
};
let result = menu.get_command_input_from_user(&full_cmd).unwrap();
@@ -680,7 +685,7 @@ fn menu_get_command_input_from_user_returns_add() {
for cmd in ["add", "Addd", "a", "A", "None"] {
let test_url = "http://happyfuntimes.commmm";
let full_cmd = format!("{} {}\n", cmd, test_url);
let full_cmd = format!("{cmd} {test_url}\n");
if cmd != "None" {
let result = menu.get_command_input_from_user(&full_cmd).unwrap();

View File

@@ -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());
}

View File

@@ -1,3 +1,4 @@
use std::fmt::Write as _;
use std::sync::atomic::AtomicBool;
use std::{ops::Deref, sync::atomic::Ordering, sync::Arc, time::Instant};
@@ -9,6 +10,7 @@ use lazy_static::lazy_static;
use tokio::sync::Semaphore;
use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter};
use crate::heuristics::WildcardResult;
use crate::Command::AddFilter;
use crate::{
event_handlers::{
@@ -181,6 +183,7 @@ impl FeroxScanner {
Err(e) => {
log::warn!("error awaiting a response: {}", e);
self.handles.stats.send(AddError(Other)).unwrap_or_default();
std::process::exit(1);
}
}
});
@@ -242,13 +245,9 @@ impl FeroxScanner {
}
{
// heuristics test block
// heuristics test block:
let test = heuristics::HeuristicTests::new(self.handles.clone());
if let Ok(num_reqs) = test.wildcard(&self.target_url).await {
progress_bar.inc(num_reqs);
}
if let Ok(dirlist_result) = test.directory_listing(&self.target_url).await {
if dirlist_result.is_some() {
let dirlist_result = dirlist_result.unwrap();
@@ -284,8 +283,7 @@ impl FeroxScanner {
let mut message = format!("=> {}", style("Directory listing").blue().bright());
if !self.handles.config.extract_links {
message
.push_str(&format!(" (add {} to scan)", style("-e").bright().yellow()))
write!(message, " (add {} to scan)", style("-e").bright().yellow())?;
}
progress_bar.reset_eta();
@@ -293,9 +291,34 @@ impl FeroxScanner {
ferox_scan.finish()?;
return Ok(());
return Ok(()); // nothing left to do if we found a dir listing
}
}
// now that we haven't found a directory listing, we'll attempt to derive whatever
// the server is using to respond to resources that don't exist (could be a
// traditional 404, or a custom response)
//
// `detect_404_like_responses` will make the requests that the wildcard test used to
// perform pre-2.8 in addition to new detection techniques, superseding the old
// wildcard test
let num_reqs_made = test.detect_404_like_responses(&self.target_url).await?;
match num_reqs_made {
Some(WildcardResult::WildcardDirectory(num_reqs)) => {
let message = format!(
"=> {} dir! {} recursion",
style("Wildcard").blue().bright(),
style("stopped").red()
);
progress_bar.set_message(&message);
progress_bar.inc(num_reqs as u64);
}
Some(WildcardResult::FourOhFourLike(num_reqs)) => {
progress_bar.inc(num_reqs as u64);
}
_ => {}
}
}
// Arc clones to be passed around to the various scans
@@ -328,7 +351,7 @@ impl FeroxScanner {
log::info!(
"requesting {} collected words: {:?}...",
new_words_len,
&new_words[..new_words_len.min(3) as usize]
&new_words[..new_words_len.min(3)]
);
self.stream_requests(

View File

@@ -41,7 +41,7 @@ impl Debug for LimitHeap {
"LimitHeap {{ original: {}, current: {}, inner: [{}...] }}",
self.original, self.current, self.inner[0]
);
write!(f, "{}", msg)
write!(f, "{msg}")
}
}

View File

@@ -84,10 +84,6 @@ impl PolicyData {
if heap.has_parent() && heap.parent_value() > current {
// all nodes except 0th node (root)
heap.move_up();
} else if !heap.has_parent() {
// been here enough that we can try resuming the scan to its original
// speed (no limiting at all)
atomic_store!(self.remove_limit, true);
}
}
} else if heap.has_children() {
@@ -103,6 +99,12 @@ impl PolicyData {
heap.move_up();
}
}
if !heap.has_parent() {
// been here enough that we can try resuming the scan to its original
// speed (no limiting at all)
atomic_store!(self.remove_limit, true);
}
self.set_limit(heap.value() as usize);
}
}

View File

@@ -1,14 +1,19 @@
use std::{
cmp::max,
collections::HashSet,
sync::{self, atomic::Ordering, Arc, Mutex},
sync::{
self,
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use anyhow::Result;
use console::style;
use lazy_static::lazy_static;
use leaky_bucket::LeakyBucket;
use leaky_bucket::RateLimiter;
use tokio::{
sync::{oneshot, RwLock},
sync::RwLock,
time::{sleep, Duration},
};
@@ -16,7 +21,7 @@ use crate::{
atomic_load, atomic_store,
config::RequesterPolicy,
event_handlers::{
Command::{self, AddError, SubtractFromUsizeField},
Command::{AddError, SubtractFromUsizeField},
Handles,
},
extractor::{ExtractionTarget, ExtractorBuilder},
@@ -25,7 +30,7 @@ use crate::{
scan_manager::{FeroxScan, ScanStatus},
statistics::{StatError::Other, StatField::TotalExpected},
url::FeroxUrl,
utils::{logged_request, should_deny_url},
utils::{logged_request, send_try_recursion_command, should_deny_url},
HIGH_ERROR_RATIO,
};
@@ -45,7 +50,7 @@ pub(super) struct Requester {
target_url: String,
/// limits requests per second if present
rate_limiter: RwLock<Option<LeakyBucket>>,
rate_limiter: RwLock<Option<RateLimiter>>,
/// data regarding policy and metadata about last enforced trigger etc...
policy_data: PolicyData,
@@ -64,6 +69,8 @@ pub(super) struct Requester {
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
/// the need for a counter)
tuning_lock: Mutex<usize>,
policy_triggered: AtomicBool,
}
/// Requester implementation
@@ -91,21 +98,22 @@ impl Requester {
handles: scanner.handles.clone(),
target_url: scanner.target_url.to_owned(),
tuning_lock: Mutex::new(0),
policy_triggered: AtomicBool::new(false),
})
}
/// build a LeakyBucket, given a rate limit (as requests per second)
fn build_a_bucket(limit: usize) -> Result<LeakyBucket> {
/// build a RateLimiter, given a rate limit (as requests per second)
fn build_a_bucket(limit: usize) -> Result<RateLimiter> {
let refill = max((limit as f64 / 10.0).round() as usize, 1); // minimum of 1 per second
let tokens = max((limit as f64 / 2.0).round() as usize, 1);
let interval = if refill == 1 { 1000 } else { 100 }; // 1 second if refill is 1
Ok(LeakyBucket::builder()
.refill_interval(Duration::from_millis(interval)) // add tokens every 0.1s
.refill_amount(refill) // ex: 100 req/s -> 10 tokens per 0.1s
.tokens(tokens) // reduce initial burst, 2 is arbitrary, but felt good
Ok(RateLimiter::builder()
.interval(Duration::from_millis(interval)) // add tokens every 0.1s
.refill(refill) // ex: 100 req/s -> 10 tokens per 0.1s
.initial(tokens) // reduce initial burst, 2 is arbitrary, but felt good
.max(limit)
.build()?)
.build())
}
/// sleep and set a flag that can be checked by other threads
@@ -118,19 +126,19 @@ impl Requester {
atomic_store!(self.policy_data.cooling_down, true, Ordering::SeqCst);
sleep(Duration::from_millis(self.policy_data.wait_time)).await;
self.ferox_scan.progress_bar().set_message("");
atomic_store!(self.policy_data.cooling_down, false, Ordering::SeqCst);
}
/// limit the number of requests per second
pub async fn limit(&self) -> Result<()> {
self.rate_limiter
.read()
.await
.as_ref()
.unwrap()
.acquire_one()
.await?;
let guard = self.rate_limiter.read().await;
if guard.is_some() {
guard.as_ref().unwrap().acquire_one().await;
}
Ok(())
}
@@ -204,18 +212,34 @@ impl Requester {
*guard = 0; // reset streak counter to 0
if atomic_load!(self.policy_data.errors) != 0 {
self.policy_data.adjust_down();
let styled_direction = style("reduced").red();
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",));
}
self.policy_data.set_errors(scan_errors);
} else {
// errors can only be incremented, so an else is sufficient
*guard += 1;
self.policy_data.adjust_up(&*guard);
self.policy_data.adjust_up(&guard);
let styled_direction = style("increased").green();
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",));
}
}
if atomic_load!(self.policy_data.remove_limit) {
self.set_rate_limiter(None).await?;
atomic_store!(self.policy_data.remove_limit, false);
self.ferox_scan
.progress_bar()
.set_message("=> 🚦 removed rate limiter 🚀");
} else if create_limiter {
// create_limiter is really just used for unit testing situations, it's true anytime
// during actual execution
@@ -254,8 +278,15 @@ impl Requester {
let reqs_sec = self.ferox_scan.requests_per_second() as usize;
self.policy_data.set_reqs_sec(reqs_sec);
// set the flag to indicate that we have triggered the rate limiter
// at least once
atomic_store!(self.policy_triggered, true);
let new_limit = self.policy_data.get_limit();
self.set_rate_limiter(Some(new_limit)).await?;
self.ferox_scan
.progress_bar()
.set_message(&format!("=> 🚦 set rate limit ({new_limit}/s)"));
}
self.adjust_limit(trigger, true).await?;
@@ -292,6 +323,14 @@ impl Requester {
let pb = self.ferox_scan.progress_bar();
let num_skipped = pb.length().saturating_sub(pb.position()) as usize;
let styled_trigger = style(format!("{trigger:?}")).red();
pb.set_message(&format!(
"=> 💀 too many {} ({}) 💀 bailing",
styled_trigger,
self.ferox_scan.num_errors(trigger),
));
// update the overall scan bar by subtracting the number of skipped requests from
// the total
self.handles
@@ -358,6 +397,9 @@ impl Requester {
RequesterPolicy::AutoTune => {
if let Some(trigger) = self.should_enforce_policy() {
self.tune(trigger).await?;
} else if atomic_load!(self.policy_triggered) {
self.adjust_limit(PolicyTrigger::TryAdjustUp, true).await?;
self.cool_down().await;
}
}
RequesterPolicy::AutoBail => {
@@ -379,14 +421,14 @@ impl Requester {
.await;
// do recursion if appropriate
if !self.handles.config.no_recursion {
self.handles
.send_scan_command(Command::TryRecursion(Box::new(
ferox_response.clone(),
)))?;
let (tx, rx) = oneshot::channel::<bool>();
self.handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
if !self.handles.config.no_recursion && !self.handles.config.force_recursion {
// to support --force-recursion, we want to limit recursive calls to only
// 'found' assets. That means we need to either gate or delay the call.
//
// this branch will retain the 'old' behavior by checking that
// --force-recursion isn't turned on
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
.await?;
}
// purposefully doing recursion before filtering. the thought process is that
@@ -400,6 +442,33 @@ impl Requester {
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 {
ferox_response.parse_extension(self.handles.clone())?;
}
@@ -469,6 +538,7 @@ mod tests {
use crate::{
config::Configuration,
config::OutputLevel,
event_handlers::Command::AddStatus,
event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler},
filters,
scan_manager::{ScanOrder, ScanType},
@@ -509,10 +579,7 @@ mod tests {
/// helper to stay DRY
async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) {
for _ in 0..num_errors {
handles
.stats
.send(Command::AddError(StatError::Other))
.unwrap();
handles.stats.send(AddError(StatError::Other)).unwrap();
scan.add_error();
}
@@ -524,7 +591,7 @@ mod tests {
let scans = handles.ferox_scans().unwrap();
for _ in 0..num_errors {
scans.increment_error(format!("{}/", url).as_str());
scans.increment_error(format!("{url}/").as_str());
}
}
@@ -537,7 +604,7 @@ mod tests {
) {
let scans = handles.ferox_scans().unwrap();
for _ in 0..num_errors {
scans.increment_status_code(format!("{}/", url).as_str(), code);
scans.increment_status_code(format!("{url}/").as_str(), code);
}
}
@@ -549,7 +616,7 @@ mod tests {
code: StatusCode,
) {
for _ in 0..num_codes {
handles.stats.send(Command::AddStatus(code)).unwrap();
handles.stats.send(AddStatus(code)).unwrap();
if code == StatusCode::FORBIDDEN {
scan.add_403();
} else {
@@ -603,6 +670,7 @@ mod tests {
PolicyTrigger::Errors => {
increment_scan_errors(handles.clone(), url, num_errors).await;
}
_ => {}
}
assert_eq!(scan.num_errors(trigger), num_errors);
@@ -623,6 +691,7 @@ mod tests {
ferox_scan: Arc::new(FeroxScan::default()),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
let ferox_scan = Arc::new(FeroxScan::default());
@@ -651,6 +720,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
increment_errors(requester.handles.clone(), ferox_scan.clone(), 25).await;
@@ -676,6 +746,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
increment_status_codes(
@@ -716,6 +787,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
increment_status_codes(
@@ -771,6 +843,7 @@ mod tests {
target_url: "http://one/one/stuff.php".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
requester.bail(PolicyTrigger::Errors).await.unwrap();
@@ -805,6 +878,7 @@ mod tests {
target_url: "http://one/one/stuff.php".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
let result = requester.bail(PolicyTrigger::Status403).await;
@@ -827,6 +901,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: Default::default(),
policy_triggered: AtomicBool::new(false),
};
requester
@@ -850,6 +925,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
});
let start = Instant::now();
@@ -880,6 +956,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.policy_data.set_reqs_sec(400);
@@ -901,10 +978,10 @@ mod tests {
/// decrease the scan rate
async fn adjust_limit_resets_streak_counter_on_downward_movement() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let scan = FeroxScan::default();
scan.add_error();
@@ -918,14 +995,16 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.policy_data.set_reqs_sec(400);
requester.policy_data.set_errors(1);
let mut guard = requester.tuning_lock.lock().unwrap();
*guard = 2;
drop(guard);
{
let mut guard = requester.tuning_lock.lock().unwrap();
*guard = 2;
}
requester
.adjust_limit(PolicyTrigger::Errors, false)
@@ -954,6 +1033,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.policy_data.set_reqs_sec(400);
@@ -982,6 +1062,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(None),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
@@ -1012,10 +1093,10 @@ mod tests {
/// set_rate_limiter should exit early when new limit equals the current bucket's max
async fn set_rate_limiter_early_exit() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let requester = Requester {
handles,
@@ -1025,6 +1106,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
policy_triggered: AtomicBool::new(false),
};
requester.set_rate_limiter(Some(200)).await.unwrap();
@@ -1044,10 +1126,10 @@ mod tests {
async fn tune_sets_expected_values_and_then_waits() {
let (handles, _) = setup_requester_test(None).await;
let mut buckets = leaky_bucket::LeakyBuckets::new();
let coordinator = buckets.coordinate().unwrap();
tokio::spawn(async move { coordinator.await.expect("coordinator errored") });
let limiter = buckets.rate_limiter().max(200).build().unwrap();
let limiter = RateLimiter::builder()
.interval(Duration::from_secs(1))
.max(200)
.build();
let scan = FeroxScan::new(
"http://localhost",
@@ -1068,6 +1150,7 @@ mod tests {
target_url: "http://localhost".to_string(),
rate_limiter: RwLock::new(Some(limiter)),
policy_data: PolicyData::new(RequesterPolicy::AutoTune, 4),
policy_triggered: AtomicBool::new(false),
};
let start = Instant::now();

View File

@@ -1,4 +1,4 @@
#[derive(Copy, Clone, PartialEq, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
/// represents different situations where different criteria can trigger auto-tune/bail behavior
pub enum PolicyTrigger {
/// excessive 403 trigger
@@ -9,4 +9,7 @@ pub enum PolicyTrigger {
/// excessive general errors
Errors,
/// dummy error for upward rate adjustment
TryAdjustUp,
}

View File

@@ -667,8 +667,8 @@ impl Stats {
///
/// This is only ever called when resuming a scan from disk
pub fn merge_from(&self, filename: &str) -> Result<()> {
let file = File::open(filename)
.with_context(|| fmt_err(&format!("Could not open {}", filename)))?;
let file =
File::open(filename).with_context(|| fmt_err(&format!("Could not open {filename}")))?;
let reader = BufReader::new(file);
let state: serde_json::Value = serde_json::from_reader(reader)?;

View File

@@ -4,6 +4,7 @@ use crate::filters::{
WordsFilter,
};
use crate::response::FeroxResponse;
use crate::utils::status_colorizer;
use anyhow::Result;
use crossterm::style::{style, Stylize};
use serde::Serialize;
@@ -38,11 +39,43 @@ impl Display for dyn FeroxFilter {
} else if let Some(filter) = self.as_any().downcast_ref::<RegexFilter>() {
write!(f, "Regex: {}", style(&filter.raw_string).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() {
if filter.dynamic != u64::MAX {
write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan())
} else {
write!(f, "Static wildcard: {}", style(filter.size).cyan())
let mut msg = format!(
"{} requests with {} responses ",
style(&filter.method).cyan(),
status_colorizer(&filter.status_code.to_string())
);
match (filter.content_length, filter.word_count, filter.line_count) {
(None, None, None) => {
unreachable!("wildcard filter without any filters set");
}
(None, None, Some(lc)) => {
msg.push_str(&format!("containing {} lines", lc));
}
(None, Some(wc), None) => {
msg.push_str(&format!("containing {} words", wc));
}
(None, Some(wc), Some(lc)) => {
msg.push_str(&format!("containing {} words and {} lines", wc, lc));
}
(Some(cl), None, None) => {
msg.push_str(&format!("containing {} bytes", cl));
}
(Some(cl), None, Some(lc)) => {
msg.push_str(&format!("containing {} bytes and {} lines", cl, lc));
}
(Some(cl), Some(wc), None) => {
msg.push_str(&format!("containing {} bytes and {} words", cl, wc));
}
(Some(cl), Some(wc), Some(lc)) => {
msg.push_str(&format!(
"containing {} bytes, {} words, and {} lines",
cl, wc, lc
));
}
}
write!(f, "{}", msg)
} else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() {
write!(f, "Status code: {}", style(filter.filter_code).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() {
@@ -52,7 +85,7 @@ impl Display for dyn FeroxFilter {
style(&filter.original_url).cyan()
)
} else {
write!(f, "Filter: {:?}", self)
write!(f, "Filter: {self:?}")
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::{event_handlers::Handles, statistics::StatError::UrlFormat, Command::
use anyhow::{anyhow, bail, Result};
use reqwest::Url;
use std::collections::HashSet;
use std::{convert::TryInto, fmt, sync::Arc};
use std::{fmt, sync::Arc};
/// abstraction around target urls; collects all Url related shenanigans in one place
#[derive(Debug)]
@@ -90,7 +90,7 @@ impl FeroxUrl {
//
// in order to resolve the issue, we check if the word from the wordlist is a parsable URL
// and if so, don't do any further processing
let message = format!("word ({}) from wordlist is a URL, skipping...", word);
let message = format!("word ({word}) from wordlist is a URL, skipping...");
log::warn!("{}", message);
log::trace!("exit: format -> Err({})", message);
bail!(message);
@@ -122,9 +122,9 @@ impl FeroxUrl {
// We handle the special case of forward slash
// That allow us to treat it as an extension with a particular format
if ext == "/" {
format!("{}/", word)
format!("{word}/")
} else {
format!("{}.{}", word, ext)
format!("{word}.{ext}")
}
} else {
String::from(word)
@@ -157,47 +157,6 @@ impl FeroxUrl {
}
}
/// Gets the length of a url's path
pub fn path_length(&self) -> Result<u64> {
let parsed = Url::parse(&self.target)?;
Ok(FeroxUrl::path_length_of_url(&parsed))
}
/// Gets the length of a url's path
///
/// example: http://localhost/stuff -> 5
pub fn path_length_of_url(url: &Url) -> u64 {
log::trace!("enter: get_path_length({})", url);
let path = url.path();
let segments = if let Some(split) = path.strip_prefix('/') {
split.split_terminator('/')
} else {
log::trace!("exit: get_path_length -> 0");
return 0;
};
if let Some(last) = segments.last() {
// failure on conversion should be very unlikely. While a usize can absolutely overflow a
// u64, the generally accepted maximum for the length of a url is ~2000. so the value we're
// putting into the u64 should never realistically be anywhere close to producing an
// overflow.
// usize max: 18,446,744,073,709,551,615
// u64 max: 9,223,372,036,854,775,807
let url_len: u64 = last
.len()
.try_into()
.expect("Failed usize -> u64 conversion");
log::trace!("exit: get_path_length -> {}", url_len);
return url_len;
}
log::trace!("exit: get_path_length -> 0");
0
}
/// Simple helper to abstract away adding a forward-slash to a url if not present
///
/// used mostly for deduplication purposes and url state tracking
@@ -483,7 +442,7 @@ mod tests {
let handles = Arc::new(Handles::for_testing(None, None).0);
let url = FeroxUrl::from_string("http://localhost", handles);
for ext in ["rocks", "fun"] {
let to_check = format!("http://localhost/upload/ferox.{}", ext);
let to_check = format!("http://localhost/upload/ferox.{ext}");
assert_eq!(
url.format("//upload/ferox", Some(ext)).unwrap(),
reqwest::Url::parse(&to_check[..]).unwrap()

View File

@@ -12,16 +12,17 @@ use std::{
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::{mpsc::UnboundedSender, oneshot};
use crate::config::Configuration;
use crate::{
config::Configuration,
config::OutputLevel,
event_handlers::{
Command::{self, AddError, AddStatus},
Handles,
},
progress::PROGRESS_PRINTER,
response::FeroxResponse,
send_command,
statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
traits::FeroxSerialize,
@@ -40,7 +41,7 @@ pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
.create(true)
.append(true)
.open(filename)
.with_context(|| fmt_err(&format!("Could not open {}", filename)))?;
.with_context(|| fmt_err(&format!("Could not open {filename}")))?;
let writer = BufWriter::new(file); // std io
@@ -67,6 +68,20 @@ pub fn fmt_err(msg: &str) -> String {
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
///
/// mainly putting this here in case i want to change the color later, making any changes easy
@@ -89,7 +104,7 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
bar.println(msg);
} else {
let stripped = strip_ansi_codes(msg);
println!("{}", stripped);
println!("{stripped}");
}
}
@@ -250,19 +265,17 @@ pub fn create_report_string(
) -> String {
if matches!(output_level, OutputLevel::Silent) {
// --silent used, just need the url
format!("{}\n", url)
format!("{url}\n")
} else {
// normal printing with status and sizes
let color_status = status_colorizer(status);
if status.contains("MSG") {
format!(
"{} {:>8} {:>9} {:>9} {:>9} {}\n",
color_status, method, line_count, word_count, content_length, url
"{color_status} {method:>8} {line_count:>9} {word_count:>9} {content_length:>9} {url}\n"
)
} else {
format!(
"{} {:>8} {:>8}l {:>8}w {:>8}c {}\n",
color_status, method, line_count, word_count, content_length, url
"{color_status} {method:>8} {line_count:>8}l {word_count:>8}w {content_length:>8}c {url}\n"
)
}
}
@@ -408,7 +421,7 @@ fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>)
// to a scanned url that is also a parent of the given url
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
let scanner = Url::parse(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {} as a url", ferox_scan))?;
.with_context(|| format!("Could not parse {ferox_scan} as a url"))?;
if let Some(scan_host) = scanner.host() {
// same domain/ip check we perform on the denier above
@@ -506,14 +519,14 @@ pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
.as_secs();
let altered_prefix = if !prefix.is_empty() {
format!("{}-", prefix)
format!("{prefix}-")
} else {
String::new()
};
let slug = url.replace("://", "_").replace('/', "_").replace('.', "_");
let slug = url.replace("://", "_").replace(['/', '.'], "_");
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);
let filename = format!("{altered_prefix}{slug}-{ts}.{suffix}");
log::trace!("exit: slugify -> {}", filename);
filename

View File

@@ -784,7 +784,6 @@ fn banner_prints_filter_status() {
.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("Status Code Filters"))
@@ -1394,3 +1393,46 @@ fn banner_prints_all_composite_settings_burp_replay() {
.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("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + force recursion
fn banner_prints_update_app() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--update")
.assert()
.success()
.stderr(
predicate::str::contains("─┬─")
.and(predicate::str::contains("Update app"))
.and(predicate::str::contains("─┴─")),
);
}

View File

@@ -45,7 +45,7 @@ fn deny_list_works_during_extraction() {
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {
@@ -90,17 +90,17 @@ fn deny_list_works_during_recursion() {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
then.status(301).header("Location", srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
then.status(301).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -155,7 +155,7 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
then.status(301).header("Location", srv.url("/js/"));
});
let api_mock = srv.mock(|when, then| {
@@ -165,12 +165,12 @@ fn deny_list_works_during_recursion_with_inverted_parents() {
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
then.status(301).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {

View File

@@ -16,7 +16,7 @@ fn extractor_finds_absolute_url() -> Result<(), Box<dyn std::error::Error>> {
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {
@@ -136,13 +136,13 @@ fn extractor_finds_same_relative_url_twice() {
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_two = srv.mock(|when, then| {
when.method(GET).path("/README");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_three = srv.mock(|when, then| {
@@ -185,7 +185,7 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
.body(srv.url("\"/homepage/assets/img/icons/handshake.svg\""));
});
let mock_two = srv.mock(|when, then| {
@@ -413,7 +413,7 @@ fn extractor_finds_directory_listing_links_and_displays_files() {
let mock_dir_redir = srv.mock(|when, then| {
when.method(GET).path("/misc");
then.status(301).header("Location", &srv.url("/misc/"));
then.status(301).header("Location", srv.url("/misc/"));
});
let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/misc/");
@@ -522,7 +522,7 @@ fn extractor_finds_directory_listing_links_and_displays_files_non_recursive() {
let mock_dir_redir = srv.mock(|when, then| {
when.method(GET).path("/misc");
then.status(301).header("Location", &srv.url("/misc/"));
then.status(301).header("Location", srv.url("/misc/"));
});
let mock_dir = srv.mock(|when, then| {
when.method(GET).path("/misc/");
@@ -600,7 +600,7 @@ fn extractor_recurses_into_403_directories() -> Result<(), Box<dyn std::error::E
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
.body(srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {

View File

@@ -180,68 +180,18 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
predicate::str::contains("GET")
.and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter",
))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)")),
.and(predicate::str::contains("1l")),
);
assert_eq!(mock.hits(), 1);
Ok(())
}
#[test]
/// test finds a dynamic wildcard and reports as much to stdout and a file
fn test_dynamic_wildcard_request_found() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = srv.mock(|when, then| {
when.method(GET).path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200).body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
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())
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
teardown_tmp_directory(tmp_dir);
assert!(contents.contains("WLD"));
assert!(contents.contains("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)")),
);
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
#[test]
/// uses dont_filter, so the normal wildcard test should never happen
fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn std::error::Error>> {
@@ -269,51 +219,53 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
Ok(())
}
#[test]
/// test finds a static wildcard and reports as much to stdout
fn heuristics_wildcard_test_with_two_static_wildcards() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
// #[test]
// /// test finds a static wildcard and reports as much to stdout
// fn heuristics_wildcard_test_with_two_static_wildcards() {
// let srv = MockServer::start();
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock2 = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.unwrap();
// let cmd = Command::cargo_bin("feroxbuster")
// .unwrap()
// .arg("--url")
// .arg(srv.url("/"))
// .arg("--wordlist")
// .arg(file.as_os_str())
// .arg("--add-slash")
// .arg("--threads")
// .arg("1")
// .unwrap();
teardown_tmp_directory(tmp_dir);
// teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)"))
.and(predicate::str::contains(
"Wildcard response is static; auto-filtering 46",
)),
);
// cmd.assert().success().stdout(
// predicate::str::contains("WLD")
// .and(predicate::str::contains("Got"))
// .and(predicate::str::contains("200"))
// .and(predicate::str::contains("(url length: 32)"))
// .and(predicate::str::contains("(url length: 96)"))
// .and(predicate::str::contains(
// "Wildcard response is static; auto-filtering 46",
// )),
// );
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
// assert_eq!(mock.hits(), 1);
// assert_eq!(mock2.hits(), 1);
// }
#[test]
/// test finds a static wildcard and reports nothing to stdout
@@ -324,14 +276,14 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
let mock = srv.mock(|when, then| {
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)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
.path_matches(Regex::new("/LICENSE").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
@@ -342,132 +294,142 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.arg("--silent")
.arg("--threads")
.arg("1")
.unwrap();
teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(predicate::str::is_empty());
cmd.assert()
.success()
.stdout(predicate::str::contains(srv.url("/")));
assert_eq!(mock.hits(), 1);
assert_eq!(mock.hits(), 4);
assert_eq!(mock2.hits(), 1);
Ok(())
}
#[test]
/// test finds a static wildcard and reports as much to stdout and a file
fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
let outfile = tmp_dir.path().join("outfile");
// #[test]
// /// test finds a static wildcard and reports as much to stdout and a file
// fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
// let srv = MockServer::start();
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
// let outfile = tmp_dir.path().join("outfile");
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(200)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock2 = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
// then.status(200)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
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())
.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();
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("Got"));
assert!(contents.contains("200"));
assert!(contents.contains("(url length: 32)"));
assert!(contents.contains("(url length: 96)"));
// assert!(contents.contains("WLD"));
// assert!(contents.contains("Got"));
// assert!(contents.contains("200"));
// assert!(contents.contains("(url length: 32)"));
// assert!(contents.contains("(url length: 96)"));
cmd.assert().success().stdout(
predicate::str::contains("WLD")
.and(predicate::str::contains("Got"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("(url length: 32)"))
.and(predicate::str::contains("(url length: 96)"))
.and(predicate::str::contains(
"Wildcard response is static; auto-filtering 46",
)),
);
// cmd.assert().success().stdout(
// predicate::str::contains("WLD")
// .and(predicate::str::contains("Got"))
// .and(predicate::str::contains("200"))
// .and(predicate::str::contains("(url length: 32)"))
// .and(predicate::str::contains("(url length: 96)"))
// .and(predicate::str::contains(
// "Wildcard response is static; auto-filtering 46",
// )),
// );
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
}
// assert_eq!(mock.hits(), 1);
// assert_eq!(mock2.hits(), 1);
// }
#[test]
/// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
/// in the output file
fn heuristics_wildcard_test_with_redirect_as_response_code(
) -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
let outfile = tmp_dir.path().join("outfile");
// #[test]
// /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
// /// in the output file
// fn heuristics_wildcard_test_with_redirect_as_response_code(
// ) -> Result<(), Box<dyn std::error::Error>> {
// let srv = MockServer::start();
let mock = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
then.status(301)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
// let outfile = tmp_dir.path().join("outfile");
let mock2 = srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
then.status(301)
.header("Location", &srv.url("/some-redirect"))
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
// let mock = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
// then.status(301)
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
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())
.unwrap();
// let mock2 = srv.mock(|when, then| {
// when.method(GET)
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
// then.status(301)
// .header("Location", &srv.url("/some-redirect"))
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// });
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"));
assert!(contents.contains("301"));
assert!(contents.contains("/some-redirect"));
assert!(contents.contains(" => "));
assert!(contents.contains(&srv.url("/")));
assert!(contents.contains("(url length: 32)"));
// teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains(" => ")
.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!(contents.contains("WLD"));
// assert!(contents.contains("301"));
// assert!(contents.contains("/some-redirect"));
// assert!(contents.contains(" => "));
// assert!(contents.contains(&srv.url("/")));
// assert!(contents.contains("(url length: 32)"));
assert_eq!(mock.hits(), 1);
assert_eq!(mock2.hits(), 1);
Ok(())
}
// cmd.assert().success().stdout(
// predicate::str::contains(" => ")
// .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

@@ -127,7 +127,7 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
);
let contents = read_to_string(outfile).unwrap();
println!("contents: {}", contents);
println!("contents: {contents}");
assert!(contents.contains("parallel branch && wrapped main")); // exits parallel branch
@@ -196,10 +196,10 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
let file_regex = Regex::new("ferox-[a-zA-Z_:0-9]+-[0-9]+.log").unwrap();
let dir_regex = Regex::new("output-file-[0-9]+.logs").unwrap();
let sub_dir = output_dir.as_ref().join(&sub_dir);
let sub_dir = output_dir.as_ref().join(sub_dir);
// created directory like output-file-1627845741.logs/
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
assert!(dir_regex.is_match(&sub_dir.to_string_lossy()));
for entry in sub_dir.read_dir()? {
let entry = entry?;

View File

@@ -42,7 +42,13 @@ fn parser_incorrect_param_with_tack_h() {
.arg("-h")
.assert()
.success()
.stdout(predicate::str::contains(
"[CAUTION] 4 -v's is probably too much",
));
.stdout(
predicate::str::contains("[CAUTION]")
.and(predicate::str::contains("4"))
.and(predicate::str::contains("-v's"))
.and(predicate::str::contains("is"))
.and(predicate::str::contains("probably"))
.and(predicate::str::contains("too"))
.and(predicate::str::contains("much")),
);
}

View File

@@ -92,7 +92,7 @@ fn auto_bail_cancels_scan_with_timeouts() {
.parse::<usize>()
.unwrap();
println!("expected: {}", total_expected);
println!("expected: {total_expected}");
// without bailing, should be 6180; after bail decreases significantly
assert!(total_expected < 5000);
}
@@ -161,7 +161,7 @@ fn auto_bail_cancels_scan_with_403s() {
let str_msg = message.as_str().unwrap_or_default().to_string();
if str_msg.starts_with("Stats") {
println!("{}", str_msg);
println!("{str_msg}");
let re = Regex::new("total_expected: ([0-9]+),").unwrap();
assert!(re.is_match(&str_msg));
let total_expected = re
@@ -171,7 +171,7 @@ fn auto_bail_cancels_scan_with_403s() {
.map_or("", |m| m.as_str())
.parse::<usize>()
.unwrap();
println!("total_expected: {}", total_expected);
println!("total_expected: {total_expected}");
assert!(total_expected < 5000);
}
}
@@ -243,7 +243,7 @@ fn auto_bail_cancels_scan_with_429s() {
let str_msg = message.as_str().unwrap_or_default().to_string();
if str_msg.starts_with("Stats") {
println!("{}", str_msg);
println!("{str_msg}");
let re = Regex::new("total_expected: ([0-9]+),").unwrap();
assert!(re.is_match(&str_msg));
let total_expected = re
@@ -253,7 +253,7 @@ fn auto_bail_cancels_scan_with_429s() {
.map_or("", |m| m.as_str())
.parse::<usize>()
.unwrap();
println!("total_expected: {}", total_expected);
println!("total_expected: {total_expected}");
assert!(total_expected < 5000);
}
}

View File

@@ -20,14 +20,16 @@ fn resume_scan_works() {
// localhost:PORT/ <- complete
// localhost:PORT/js <- will get scanned with /css and /stuff
let complete_scan = format!(
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","scan_type":"Directory","status":"Complete"}}"#,
srv.url("/")
r#"{{"id":"057016a14769414aac9a7a62707598cb","url":"{}","normalized_url":"{}","scan_type":"Directory","status":"Complete","num_requests":4174,"requests_made_so_far":0}}"#,
srv.url("/"),
srv.url("/"),
);
let incomplete_scan = format!(
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","scan_type":"Directory","status":"NotStarted"}}"#,
r#"{{"id":"400b2323a16f43468a04ffcbbeba34c6","url":"{}","normalized_url":"{}/","scan_type":"Directory","status":"NotStarted","num_requests":4174,"requests_made_so_far":0}}"#,
srv.url("/js"),
srv.url("/js")
);
let scans = format!(r#""scans":[{},{}]"#, complete_scan, incomplete_scan);
let scans = format!(r#""scans":[{complete_scan},{incomplete_scan}]"#);
let config = format!(
r#""config": {{"type":"configuration","wordlist":"{}","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,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/1.9.0","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":2,"scan_limit":1,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false}}"#,
@@ -40,7 +42,7 @@ fn resume_scan_works() {
r#"{{"type":"response","url":"{}","path":"/js/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}"#,
srv.url("/js/css")
);
let responses = format!(r#""responses":[{}]"#, response);
let responses = format!(r#""responses":[{response}]"#);
// not scanned because /js is not complete, and /js/stuff response is not known
let not_scanned_yet = srv.mock(|when, then| {
@@ -61,11 +63,13 @@ fn resume_scan_works() {
then.status(200).body("two words");
});
let state_file_contents = format!("{{{},{},{}}}", scans, config, responses);
let state_file_contents = format!("{{{scans},{config},{responses}}}");
let (tmp_dir2, state_file) = setup_tmp_directory(&[state_file_contents], "state-file").unwrap();
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("-vvv")
.arg("--resume-from")
.arg(state_file.as_os_str())
.assert()

View File

@@ -53,17 +53,17 @@ fn scanner_recursive_request_scan() -> Result<(), Box<dyn std::error::Error>> {
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
then.status(301).header("Location", srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod");
then.status(301).header("Location", &srv.url("/js/prod/"));
then.status(301).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
then.status(301).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -116,17 +116,17 @@ fn scanner_recursive_request_scan_using_only_success_responses(
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js/");
then.status(200).header("Location", &srv.url("/js/"));
then.status(200).header("Location", srv.url("/js/"));
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/js/prod/");
then.status(200).header("Location", &srv.url("/js/prod/"));
then.status(200).header("Location", srv.url("/js/prod/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/");
then.status(200).header("Location", &srv.url("/js/dev/"));
then.status(200).header("Location", srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
@@ -454,7 +454,7 @@ fn scanner_single_request_scan_with_debug_logging() {
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
println!("{}", contents);
println!("{contents}");
assert!(contents.starts_with("Configuration {"));
assert!(contents.contains("TRC"));
assert!(contents.contains("DBG"));
@@ -492,7 +492,7 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
.unwrap();
let contents = std::fs::read_to_string(outfile).unwrap();
println!("{}", contents);
println!("{contents}");
assert!(contents.starts_with("{\"type\":\"configuration\""));
assert!(contents.contains("\"level\":\"TRACE\""));
assert!(contents.contains("\"level\":\"DEBUG\""));
@@ -587,9 +587,12 @@ fn scanner_recursion_works_with_403_directories() {
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE")
.count(2)
.and(predicate::str::contains("200").count(2))
.and(predicate::str::contains("403"))
.and(predicate::str::contains("53c"))
.and(predicate::str::contains("200"))
.and(predicate::str::contains("404"))
.and(predicate::str::contains("53c Auto-filtering"))
.and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter;",
))
.and(predicate::str::contains("14c"))
.and(predicate::str::contains("0c"))
.and(predicate::str::contains("ignored").count(2))
@@ -651,7 +654,7 @@ fn add_discovered_extension_updates_bars_and_stats() {
)
.unwrap();
srv.mock(|when, then| {
let mock = srv.mock(|when, then| {
when.method(GET).path("/stuff.php");
then.status(200).body("cool... coolcoolcool");
});
@@ -675,10 +678,11 @@ fn add_discovered_extension_updates_bars_and_stats() {
.assert()
.success();
mock.assert_hits(1);
let contents = std::fs::read_to_string(file_path).unwrap();
println!("{}", contents);
println!("{contents}");
assert!(contents.contains("discovered new extension: php"));
assert!(contents.contains("extensions_collected: 1"));
// assert!(contents.contains("extensions_collected: 1")); // this is racy
assert!(contents.contains("expected_per_scan: 6"));
}
@@ -852,3 +856,66 @@ fn collect_words_makes_appropriate_requests() {
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("--dont-filter")
.arg("--status-codes")
.arg("301")
.arg("200")
.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(())
}

View File

@@ -9,7 +9,7 @@ pub fn setup_tmp_directory(
filename: &str,
) -> Result<(TempDir, PathBuf), Box<dyn std::error::Error>> {
let tmp_dir = TempDir::new()?;
let file = tmp_dir.path().join(&filename);
let file = tmp_dir.path().join(filename);
write(&file, words.join("\n"))?;
Ok((tmp_dir, file))
}