Compare commits

...

242 Commits

Author SHA1 Message Date
epi
f0252bc375 Merge pull request #360 from epi052/329-dont-scan-enhancements
329 dont scan enhancements
2021-10-15 16:44:11 -05:00
epi
1eca023d6e updated libs 2021-10-15 16:34:35 -05:00
epi
3ae3adf11b merged main 2021-10-15 16:29:11 -05:00
epi
b94edc4e57 Merge pull request #355 from cortantief/main
adding support add_slash and extensions + correcting error on double slashes in the start of a word with extension
2021-10-15 16:25:57 -05:00
epi
8116018b8b reverted the revert 2021-10-15 16:16:13 -05:00
epi
d1d37c135e merged upstream main 2021-10-15 15:46:30 -05:00
epi
1cd1d990de Merge pull request #357 from dsaxton/random-agent
Implement random user agent flag
2021-10-15 15:39:53 -05:00
Daniel Saxton
9c50038a25 Nit 2021-10-14 15:15:32 -05:00
Daniel Saxton
1e3cd3a209 Doc 2021-10-13 20:12:43 -05:00
Daniel Saxton
3536587260 Update banner 2021-10-13 20:11:02 -05:00
Daniel Saxton
19cd5c910a Try a test refactor 2021-10-13 20:08:28 -05:00
Daniel Saxton
b8bfbb09f3 Oops 2021-10-13 16:32:39 -05:00
Daniel Saxton
0a3130934c Ignore 2021-10-13 16:31:48 -05:00
Daniel Saxton
7b3201f2f8 Fix test 2021-10-13 16:14:42 -05:00
Daniel Saxton
521d341e36 Remove unused and update make_request call 2021-10-13 16:03:32 -05:00
epi
3f3b24b26f reverted add-slash change in heuristics 2021-10-13 06:38:26 -05:00
epi
2a4a150598 changed counter type 2021-10-12 20:24:05 -05:00
epi
39b2da9735 changed counter type 2021-10-12 20:22:01 -05:00
epi
87aaa84f1e added user-agent logic 2021-10-12 18:21:05 -05:00
epi
2b7d134ede Merge pull request #399 from eltociear/patch-1
fix typo in parser.rs
2021-10-12 05:55:27 -05:00
Ikko Ashimine
4eebacb077 fix typo in parser.rs
initalize -> initialize
2021-10-12 02:53:29 +09:00
epi
1a6ad39b46 Merge pull request #384 from epi052/all-contributors/add-hunter0x8
docs: add hunter0x8 as a contributor for bug
2021-10-10 14:24:20 -05:00
epi
10b473d920 Merge branch 'main' into all-contributors/add-hunter0x8 2021-10-10 14:24:13 -05:00
epi
1d2dc8bd37 Merge pull request #397 from epi052/all-contributors/add-dnaka91
docs: add dnaka91 as a contributor for infra
2021-10-10 14:21:06 -05:00
allcontributors[bot]
2132ceadd5 docs: update .all-contributorsrc [skip ci] 2021-10-10 19:20:45 +00:00
allcontributors[bot]
15bd50dc24 docs: update README.md [skip ci] 2021-10-10 19:20:44 +00:00
epi
48ca8d510a Merge pull request #396 from epi052/add-contrib-badrequest
updated contributor
2021-10-10 06:07:14 -05:00
epi
9ca48fe877 updated contributor 2021-10-10 06:06:51 -05:00
epi
2f09df921d Merge pull request #395 from epi052/add-contrib-SleepiPanda
updated contributor
2021-10-10 06:03:02 -05:00
epi
93686acb48 updated contributor 2021-10-10 06:02:36 -05:00
epi
294159088c Merge pull request #394 from epi052/add-contributors-hoggard
added henry hoggard
2021-10-10 05:52:28 -05:00
epi
da9bec5a67 added henry hoggard 2021-10-10 05:51:43 -05:00
epi
69cf08bf1f Merge pull request #393 from epi052/all-contributors/add-sicks3c
docs: add sicks3c as a contributor for bug
2021-10-10 05:44:12 -05:00
allcontributors[bot]
035a8c75c0 docs: update .all-contributorsrc [skip ci] 2021-10-10 10:44:04 +00:00
allcontributors[bot]
ac56225405 docs: update README.md [skip ci] 2021-10-10 10:44:03 +00:00
epi
82a7aa458c Merge pull request #392 from epi052/all-contributors/add-BitThr3at
docs: add BitThr3at as a contributor for bug
2021-10-10 05:43:03 -05:00
allcontributors[bot]
c29abe4ec4 docs: update .all-contributorsrc [skip ci] 2021-10-10 10:42:51 +00:00
allcontributors[bot]
bb6146c18f docs: update README.md [skip ci] 2021-10-10 10:42:50 +00:00
epi
882aded16c Merge pull request #391 from epi052/all-contributors/add-moscowchill
docs: add moscowchill as a contributor for bug
2021-10-10 05:41:38 -05:00
allcontributors[bot]
86dc8edd3d docs: update .all-contributorsrc [skip ci] 2021-10-10 10:41:27 +00:00
allcontributors[bot]
0281057944 docs: update README.md [skip ci] 2021-10-10 10:41:26 +00:00
epi
96fa07a5e5 Merge pull request #390 from epi052/all-contributors/add-N0ur5
docs: add N0ur5 as a contributor for ideas
2021-10-10 05:40:27 -05:00
allcontributors[bot]
3ee6641a7d docs: update .all-contributorsrc [skip ci] 2021-10-10 10:40:18 +00:00
allcontributors[bot]
90dd18af2e docs: update README.md [skip ci] 2021-10-10 10:40:17 +00:00
epi
b98ab6d691 Merge pull request #388 from epi052/all-contributors/add-dinosn
docs: add dinosn as a contributor for ideas
2021-10-09 16:31:57 -05:00
allcontributors[bot]
1723847672 docs: update .all-contributorsrc [skip ci] 2021-10-09 21:31:48 +00:00
allcontributors[bot]
125a55f72b docs: update README.md [skip ci] 2021-10-09 21:31:47 +00:00
epi
ba58bd942e Merge pull request #387 from epi052/all-contributors/add-black-A
docs: add black-A as a contributor for ideas
2021-10-09 16:29:49 -05:00
allcontributors[bot]
72fc0b026d docs: update .all-contributorsrc [skip ci] 2021-10-09 21:29:16 +00:00
allcontributors[bot]
5350724e5f docs: update README.md [skip ci] 2021-10-09 21:29:15 +00:00
epi
0b208cd011 Merge pull request #386 from epi052/all-contributors/add-sbrun
docs: add sbrun as a contributor for infra
2021-10-09 16:28:39 -05:00
allcontributors[bot]
82f8f687fd docs: update .all-contributorsrc [skip ci] 2021-10-09 21:27:49 +00:00
allcontributors[bot]
b36c3e0318 docs: update README.md [skip ci] 2021-10-09 21:27:49 +00:00
epi
85473916db Merge pull request #385 from epi052/all-contributors/add-secure-77
docs: add secure-77 as a contributor for bug
2021-10-09 16:27:26 -05:00
allcontributors[bot]
7afb261206 docs: update .all-contributorsrc [skip ci] 2021-10-09 21:27:17 +00:00
allcontributors[bot]
0b2d77605e docs: update README.md [skip ci] 2021-10-09 21:27:16 +00:00
allcontributors[bot]
073291360a docs: update .all-contributorsrc [skip ci] 2021-10-09 21:26:29 +00:00
allcontributors[bot]
98d6fdf536 docs: update README.md [skip ci] 2021-10-09 21:26:28 +00:00
epi
2b6de8e7dc Merge pull request #383 from epi052/all-contributors/add-0xdf
docs: add 0xdf as a contributor for bug
2021-10-09 16:25:39 -05:00
allcontributors[bot]
d0cdf5766b docs: update .all-contributorsrc [skip ci] 2021-10-09 21:25:07 +00:00
allcontributors[bot]
46366291f1 docs: update README.md [skip ci] 2021-10-09 21:25:06 +00:00
epi
b1d33f4f7d updated readme 2021-10-09 16:23:16 -05:00
epi
1e4d3802f8 updated readme 2021-10-09 16:21:58 -05:00
epi
a2bc9ecb49 Merge pull request #382 from epi052/all-contributors/add-Tib3rius
docs: add Tib3rius as a contributor for bug
2021-10-09 16:19:23 -05:00
allcontributors[bot]
4b0b26da02 docs: update .all-contributorsrc [skip ci] 2021-10-09 21:18:40 +00:00
allcontributors[bot]
fe5612ce71 docs: update README.md [skip ci] 2021-10-09 21:18:39 +00:00
epi
ea51805552 fixed badge 2021-10-07 07:20:34 -05:00
epi
2ff4dcde8a Merge pull request #376 from epi052/all-contributors/add-wtwver
docs: add wtwver as a contributor for infra
2021-10-07 07:18:28 -05:00
allcontributors[bot]
9f93c2381a docs: update .all-contributorsrc [skip ci] 2021-10-07 12:18:20 +00:00
allcontributors[bot]
ece220263b docs: update README.md [skip ci] 2021-10-07 12:18:19 +00:00
epi
06312f1f09 Merge pull request #375 from epi052/all-contributors/add-EONRaider
docs: add EONRaider as a contributor for infra
2021-10-07 07:18:07 -05:00
allcontributors[bot]
14023f7e05 docs: update .all-contributorsrc [skip ci] 2021-10-07 12:18:00 +00:00
allcontributors[bot]
cc18dfc7d4 docs: update README.md [skip ci] 2021-10-07 12:17:59 +00:00
epi
99bb0200e5 Merge pull request #374 from epi052/all-contributors/add-craig
docs: add craig as a contributor for infra
2021-10-07 07:17:43 -05:00
allcontributors[bot]
542db19180 docs: update .all-contributorsrc [skip ci] 2021-10-07 12:17:35 +00:00
allcontributors[bot]
d9718d0d6a docs: update README.md [skip ci] 2021-10-07 12:17:34 +00:00
epi
491821f0b2 Merge pull request #373 from epi052/all-contributors/add-noraj
docs: add noraj as a contributor for infra, doc
2021-10-07 07:17:15 -05:00
allcontributors[bot]
6d5235ab0a docs: update .all-contributorsrc [skip ci] 2021-10-07 12:17:08 +00:00
allcontributors[bot]
74b23141e0 docs: update README.md [skip ci] 2021-10-07 12:17:07 +00:00
epi
8f4ffc8e22 Merge pull request #372 from epi052/all-contributors/add-bpsizemore
docs: add bpsizemore as a contributor for code
2021-10-07 07:16:53 -05:00
allcontributors[bot]
7d314c7bac docs: update .all-contributorsrc [skip ci] 2021-10-07 12:16:47 +00:00
allcontributors[bot]
b6c41ae2d3 docs: update README.md [skip ci] 2021-10-07 12:16:46 +00:00
epi
b80c58a073 Merge pull request #371 from epi052/all-contributors/add-bsysop
docs: add bsysop as a contributor for doc
2021-10-07 07:16:36 -05:00
allcontributors[bot]
a035f0eeaf docs: update .all-contributorsrc [skip ci] 2021-10-07 12:16:28 +00:00
allcontributors[bot]
672d17ec27 docs: update README.md [skip ci] 2021-10-07 12:16:28 +00:00
epi
f7d4a3e7b4 Merge pull request #370 from epi052/all-contributors/add-tomtastic
docs: add tomtastic as a contributor for doc
2021-10-07 07:16:13 -05:00
allcontributors[bot]
00b0c3c62d docs: update .all-contributorsrc [skip ci] 2021-10-07 12:16:05 +00:00
allcontributors[bot]
f260a981ca docs: update README.md [skip ci] 2021-10-07 12:16:04 +00:00
epi
f254fe172c Merge pull request #369 from epi052/all-contributors/add-n-thumann
docs: add n-thumann as a contributor for code, doc
2021-10-07 07:15:47 -05:00
allcontributors[bot]
cc1dc94459 docs: update .all-contributorsrc [skip ci] 2021-10-07 12:15:35 +00:00
allcontributors[bot]
e39f6cf16d docs: update README.md [skip ci] 2021-10-07 12:15:34 +00:00
epi
2a406960c4 Merge pull request #368 from epi052/all-contributors/add-mzpqnxow
docs: add mzpqnxow as a contributor for ideas, doc
2021-10-07 07:15:16 -05:00
allcontributors[bot]
c65e2f02b3 docs: update .all-contributorsrc [skip ci] 2021-10-07 12:15:06 +00:00
allcontributors[bot]
831ae011e2 docs: update README.md [skip ci] 2021-10-07 12:15:06 +00:00
epi
32a4db4b46 Merge pull request #367 from epi052/all-contributors/add-evanrichter
docs: add evanrichter as a contributor for code, doc
2021-10-07 07:14:51 -05:00
allcontributors[bot]
a439be0305 docs: update .all-contributorsrc [skip ci] 2021-10-07 12:14:42 +00:00
allcontributors[bot]
36994d208d docs: update README.md [skip ci] 2021-10-07 12:14:41 +00:00
epi
b72c42e1d1 Merge pull request #366 from epi052/all-contributors/add-spikecodes
docs: add spikecodes as a contributor for infra, doc
2021-10-07 07:14:26 -05:00
allcontributors[bot]
b508dcce8d docs: update .all-contributorsrc [skip ci] 2021-10-07 12:14:02 +00:00
allcontributors[bot]
c33d397360 docs: update README.md [skip ci] 2021-10-07 12:14:01 +00:00
epi
449f6bda32 Merge pull request #365 from epi052/all-contributors/add-TGotwig
docs: add TGotwig as a contributor for infra, doc
2021-10-07 07:13:43 -05:00
allcontributors[bot]
092515cf3a docs: update .all-contributorsrc [skip ci] 2021-10-07 12:13:30 +00:00
allcontributors[bot]
e5fe9bb360 docs: update README.md [skip ci] 2021-10-07 12:13:29 +00:00
epi
2e42e3efac Merge pull request #364 from epi052/all-contributors/add-jsav0
docs: add jsav0 as a contributor for infra, doc
2021-10-07 07:13:10 -05:00
allcontributors[bot]
0d1cb25b69 docs: update .all-contributorsrc [skip ci] 2021-10-07 12:12:29 +00:00
allcontributors[bot]
653117bda6 docs: update README.md [skip ci] 2021-10-07 12:12:28 +00:00
epi
5c32fab4cb fixed badge 2021-10-07 06:54:26 -05:00
epi
904c70281a Merge pull request #363 from epi052/all-contributors/add-joohoi
docs: add joohoi as a contributor for doc
2021-10-07 06:48:11 -05:00
allcontributors[bot]
2d5825556f docs: create .all-contributorsrc [skip ci] 2021-10-07 11:45:27 +00:00
allcontributors[bot]
ef7fc7a8a3 docs: update README.md [skip ci] 2021-10-07 11:45:26 +00:00
epi
e48a462471 added confirmed tag to stale-bot 2021-10-07 06:40:40 -05:00
epi
f6047e9819 updated multiple items from new-feature checklist 2021-10-04 06:34:04 -05:00
epi
534cbe8fe1 added a few tests 2021-10-03 14:17:28 -05:00
epi
adb5cd75cc fixed existing tests 2021-10-03 12:16:04 -05:00
epi
3469e2c306 fixed existing tests 2021-10-03 11:32:43 -05:00
epi
6de087ae79 Update cicd-to-dockerhub.yml 2021-10-03 11:26:48 -05:00
epi
07a9fdee41 moved parsing of urls/regexes to config
this means that each url / regex passed to --dont-scan is only parsed once
2021-10-03 09:04:31 -05:00
epi
7b9767107f added regex specific banner title to --dont-scan 2021-10-01 07:16:22 -05:00
epi
5388d40c03 added regex support to --dont-scan 2021-10-01 06:54:30 -05:00
epi
28769b5028 broke absolute path denial into its own function; tests pass 2021-10-01 06:23:44 -05:00
epi
28fa90b093 Merge pull request #359 from wtwver/main
Fix docker build error
2021-09-30 06:12:48 -05:00
wtwver
0b16f368a4 . 2021-09-30 17:25:24 +08:00
Daniel Saxton
c5e59b70f7 Hard code 2021-09-28 15:44:11 -05:00
Daniel Saxton
6756a1da74 Merge remote-tracking branch 'upstream/main' into random-agent 2021-09-28 15:23:29 -05:00
epi
d14de76f9a added redirect 2021-09-27 06:29:40 -05:00
Daniel Saxton
ef3cc05ee3 Merge remote-tracking branch 'upstream/main' into random-agent 2021-09-26 10:19:00 -05:00
Daniel Saxton
efd706cb9b cargo fmt 2021-09-26 10:09:52 -05:00
epi
63baa3ec57 removed github pages workflow 2021-09-26 06:52:46 -05:00
epi
51defffd3b cleaned up docs stuff 2021-09-26 06:52:16 -05:00
epi
439afd2e2a updated to reflect new documentation site 2021-09-26 06:50:56 -05:00
epi
10ae4ee524 updated url for submodule 2021-09-25 18:40:53 -05:00
epi
4af448d7b1 updated url for submodule 2021-09-25 18:35:21 -05:00
epi
adc536bf4b added nojekyll 2021-09-25 18:28:24 -05:00
epi
fde52e95e1 added npm install 2021-09-25 18:24:37 -05:00
epi
e4ae5759ff added node 2021-09-25 17:33:07 -05:00
epi
89eda0e62b added node 2021-09-25 17:31:14 -05:00
epi
00330b053f postcss install 2021-09-25 17:28:15 -05:00
epi
0df1d34ee1 recursive checkout 2021-09-25 16:53:15 -05:00
epi
eddab0de13 cd into docs dir for ci 2021-09-25 16:51:19 -05:00
epi
f9335a7867 removed jekyll theme 2021-09-25 16:48:46 -05:00
epi
bc9779be2a Merge branch 'main' of github.com:epi052/feroxbuster 2021-09-25 16:48:01 -05:00
epi
c4b6fed6ef added new documentation site 2021-09-25 16:47:52 -05:00
epi
3818276c7e Set theme jekyll-theme-midnight 2021-09-24 06:52:17 -05:00
epi
6596759132 Set theme jekyll-theme-dinky 2021-09-24 06:48:23 -05:00
epi
5235208aa8 dynamically generate user agents at build time for inclusion into lib.rs 2021-09-23 21:22:52 -05:00
Daniel Saxton
bce55e77f3 Add arg 2021-09-22 19:41:52 -05:00
Daniel Saxton
8d11bb1800 README 2021-09-22 19:21:07 -05:00
Daniel Saxton
40fccb9761 Bump minor version 2021-09-22 19:14:07 -05:00
Daniel Saxton
28f63aae94 Test 2021-09-22 17:09:11 -05:00
Daniel Saxton
1eaf6fc232 Merge remote-tracking branch 'upstream/main' into random-agent 2021-09-22 17:01:19 -05:00
epi
dd1c824d98 updated shell completions that care about mutual exclusion 2021-09-22 07:30:38 -05:00
epi
42c06c87cc bumped version to 2.3.4 2021-09-22 07:29:14 -05:00
epi
d36379ba1b nitpickery stuff; removed the test i added cuz it's a duplicate 2021-09-22 07:28:57 -05:00
epi
83ba49a486 added test for formatted_urls when both extension and slash are set 2021-09-21 20:49:31 -05:00
Daniel Saxton
0b75e1a548 Implement random user agent flag 2021-09-21 13:35:10 -05:00
epi
867e297284 Set theme jekyll-theme-midnight 2021-09-21 07:25:12 -05:00
epi
63bd89ddc3 Set theme jekyll-theme-leap-day 2021-09-21 07:14:31 -05:00
epi
9f39ee3491 Set theme jekyll-theme-cayman 2021-09-21 07:13:00 -05:00
epi
eabf97b776 Set theme jekyll-theme-merlot 2021-09-21 07:05:39 -05:00
epi
3c3b976a71 Set theme jekyll-theme-hacker 2021-09-21 07:02:59 -05:00
epi
81709b5009 Set theme jekyll-theme-midnight 2021-09-21 07:01:26 -05:00
epi
ffa0c6b390 Set theme jekyll-theme-midnight 2021-09-21 07:00:42 -05:00
epi
141fe74129 removed nojekyll 2021-09-21 06:57:32 -05:00
cortantief
d4a69fa2ec clippy plus cargo fmt 2021-09-20 10:58:53 +02:00
cortantief
9a3754a31d Cargo fmt formating 2021-09-20 10:56:19 +02:00
cortantief
c89453c5c3 Adding support for slash and extensions at the same time.
Support for the extensions and slash where added by using a special condition inside the format function allowing us to treat slash as an extension with a particular format.
We had to modify other place to propagate the changes like in make_wildcard_request for example.
Ofcourse we had to delete the limitation inside the arg parser.
A test was added to ensure the validity of the implementation.
2021-09-20 10:01:09 +02:00
cortantief
be57e620f0 Correcting error when extensions are used with word that start with two slashes
The cause of that error was the fact that the check was made inside a chain of if-else-if conditions, since the extension are tested before the slashes
resulting to not check correctly the word.
2021-09-20 07:02:37 +02:00
epi
45efaa7388 new theme options 2021-09-17 06:29:32 -05:00
epi
15cb5e1619 trying dark theme 2021-09-17 06:20:05 -05:00
epi
fc500a5cd5 flatdoc resolve fix 2021-09-17 06:12:03 -05:00
epi
c84612751c github buttons cahnged to https 2021-09-17 06:09:45 -05:00
epi
c8ecbd4ed6 jquery cahnged to https 2021-09-17 06:08:16 -05:00
epi
fbf79ab7c1 pointing docs to readme 2021-09-17 06:06:55 -05:00
epi
a446192b9a pointing docs to readme 2021-09-17 06:06:22 -05:00
epi
26565be18d removed config file 2021-09-17 06:05:18 -05:00
epi
7c4bc213a3 fixed theme bug 2021-09-17 06:04:52 -05:00
epi
a227dcf726 fixed theme bug 2021-09-17 06:01:13 -05:00
epi
b70c92b1e6 added github pages content 2021-09-17 05:46:49 -05:00
epi
033a57a9e9 Merge branch 'main' of github.com:epi052/feroxbuster 2021-09-16 18:58:14 -05:00
epi
dac74ae040 updated dependencies 2021-09-16 18:58:07 -05:00
epi
d4abb84214 updated readme 2021-09-04 17:04:27 -05:00
epi
5201c300e9 update branch name 2021-09-04 16:41:29 -05:00
epi
ec0e5299ed Merge pull request #345 from EONRaider/cicd-to-dockerhub
Optimize Dockerfile and implement CI/CD to DockerHub
2021-09-04 15:55:17 -05:00
EONRaider
242c35c89f Create cicd-to-dockerhub.yml 2021-09-02 08:29:38 -03:00
EONRaider
f717ee534e Optimize Dockerfile 2021-08-28 14:01:26 -03:00
EONRaider
6b66f39122 Update Dockerfile 2021-08-27 18:08:06 -03:00
epi
4b3e9badbb Merge pull request #336 from epi052/335-fix-wildcard-filter-when-response-is-zero
335 fix wildcard filter when response is zero
2021-08-20 20:44:36 -05:00
epi
c680be558a added test for 0-length wildcard response 2021-08-20 20:09:48 -05:00
epi
8cee7ce247 bumped version; updated dependencies 2021-08-20 19:52:06 -05:00
epi
580aa19681 fixed erroneous reporting of total urls expected 2021-08-20 19:46:46 -05:00
epi
cd220fe471 fixed wildcard filtering when wildcard response len is 0 2021-08-20 19:25:34 -05:00
epi
15b4fd04e5 updated status code defaults to include 500 2021-08-02 19:28:09 -05:00
epi
fceba0b68b updated deps 2021-08-02 19:27:55 -05:00
epi
eef4c9b5ed added 500 to status code defaults 2021-08-02 19:23:20 -05:00
epi
24da4e017c adjusted rlimit imports to ignore windows targets 2021-08-02 05:41:50 -05:00
epi
f3cedf01a5 Merge pull request #321 from epi052/319-separate-log-files-for-parallel
Log to separate files when using --parallel
2021-08-02 05:21:16 -05:00
epi
08ee32595f updated documentation for parallel logging change 2021-08-01 21:03:17 -05:00
epi
4c4d1a2a61 updated documentation for parallel logging change 2021-08-01 19:59:28 -05:00
epi
64b54a6308 fixed up a few todo items 2021-08-01 19:57:28 -05:00
epi
e27b3ee8da added coverage for stdin slugifying 2021-08-01 15:17:59 -05:00
epi
129725cedd added test for --parallel with -o 2021-08-01 14:32:14 -05:00
epi
17886da3df handle dir/outfile case to -o 2021-08-01 14:31:48 -05:00
epi
c8a46b7e5a added prefix param to slugify_filename 2021-08-01 09:23:42 -05:00
epi
f97d103fc6 unique file logging with parallel works 2021-08-01 08:07:12 -05:00
epi
aa2fecc5c1 bumped version to 2.3.2 2021-07-31 14:49:15 -05:00
epi
6f2244e1ff put url slug logic into its own function 2021-07-31 14:48:35 -05:00
epi
a1dc90ba06 updated rlimit lib 2021-07-30 20:01:16 -05:00
epi
32f55ddfb7 Merge branch 'main' of github.com:epi052/feroxbuster 2021-07-30 16:14:32 -05:00
epi
9a65c7f1f5 fixed up code for new clippy checks 2021-07-30 16:14:25 -05:00
epi
0f6bc1c160 updated deps 2021-07-30 07:42:25 -05:00
epi
abef7a236b Update README.md 2021-07-12 16:17:08 -05:00
epi
0cff62dbe2 return 0 when -h/--help is used 2021-07-05 06:33:40 -05:00
epi
a590188e44 Merge pull request #293 from epi052/286-url-blacklist
add --dont-scan option for denying urls
2021-06-18 14:40:16 -07:00
epi
dc3aa11966 added tracing to new extractor fns 2021-06-18 16:32:04 -05:00
epi
57714d243a fixed caching for extraction; much better performance now 2021-06-18 16:18:42 -05:00
epi
1d34a5e99f updated readme 2021-06-18 11:05:57 -05:00
epi
9ab3e5515e added short-circuit to deny check 2021-06-18 06:48:41 -05:00
epi
3abef25c8f added integration tests 2021-06-17 20:34:21 -05:00
epi
454f3a4302 satisfied newest version of clippy 2021-06-17 16:13:18 -05:00
epi
acb9c19f4d added should_deny_url function and unit tests 2021-06-17 14:44:33 -05:00
epi
98f06951bd added banner test 2021-06-15 17:31:24 -05:00
epi
c9e1a7adbe added deny list to banner 2021-06-15 16:59:36 -05:00
epi
c57cf82fce added --dont-scan to options parser 2021-06-15 16:45:23 -05:00
epi
a3bcfaf95c added url_denylist to config 2021-06-15 14:31:01 -05:00
epi
c99afec740 bumped version to 2.3.0 2021-06-15 13:52:45 -05:00
epi
fa9fd65c2f bumped version to 2.3.0 2021-06-15 11:47:57 -05:00
epi
2af87971d5 fixed build script when on cd pipeline 2021-06-15 11:46:20 -05:00
epi
e6753d9474 fixed build script when on cd pipeline 2021-06-15 11:45:35 -05:00
epi
d23717dc6c troubleshooting build script 2021-06-15 11:32:00 -05:00
epi
4debe68ed6 updated pipeline build 2021-06-15 11:28:46 -05:00
epi
e6b78e3986 Merge pull request #292 from epi052/287-always-parse-help-parameter
added small check for help param to always print and exit
2021-06-15 09:22:34 -07:00
epi
7b268cf197 bumped version to 2.2.5 2021-06-15 11:22:14 -05:00
epi
34ff884d52 added tests for new help param catcher 2021-06-15 11:02:43 -05:00
epi
7fef23f888 added small check for help param to always print and exit 2021-06-15 10:28:20 -05:00
epi
7a8d6d0d52 fixed build when config copied on cd 2021-06-15 10:14:59 -05:00
epi
6d4f2a7ed9 Update build.yml 2021-06-15 10:14:16 -05:00
epi
329d04252f config file is dropped to disk when installing via cargo 2021-06-15 08:00:21 -05:00
epi
9b4092ea8c added plusdirs to bash completion script and regenerated it 2021-06-15 06:37:03 -05:00
epi
d942a7705a bumped various lib versions 2021-06-14 20:09:22 -05:00
epi
e3365b42a2 Merge branch 'main' of github.com:epi052/feroxbuster 2021-06-14 20:08:11 -05:00
epi
41689bd742 added verify command 2021-06-14 20:07:03 -05:00
epi
bc487475f0 Update pull_request_template.md 2021-06-14 20:00:19 -05:00
54 changed files with 2507 additions and 1659 deletions

283
.all-contributorsrc Normal file
View File

@@ -0,0 +1,283 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "joohoi",
"name": "Joona Hoikkala",
"avatar_url": "https://avatars.githubusercontent.com/u/5235109?v=4",
"profile": "https://io.fi",
"contributions": [
"doc"
]
},
{
"login": "jsav0",
"name": "J Savage",
"avatar_url": "https://avatars.githubusercontent.com/u/20546041?v=4",
"profile": "https://github.com/jsav0",
"contributions": [
"infra",
"doc"
]
},
{
"login": "TGotwig",
"name": "Thomas Gotwig",
"avatar_url": "https://avatars.githubusercontent.com/u/30773779?v=4",
"profile": "http://www.tgotwig.dev",
"contributions": [
"infra",
"doc"
]
},
{
"login": "spikecodes",
"name": "Spike",
"avatar_url": "https://avatars.githubusercontent.com/u/19519553?v=4",
"profile": "https://github.com/spikecodes",
"contributions": [
"infra",
"doc"
]
},
{
"login": "evanrichter",
"name": "Evan Richter",
"avatar_url": "https://avatars.githubusercontent.com/u/330292?v=4",
"profile": "https://github.com/evanrichter",
"contributions": [
"code",
"doc"
]
},
{
"login": "mzpqnxow",
"name": "AG",
"avatar_url": "https://avatars.githubusercontent.com/u/8016228?v=4",
"profile": "https://github.com/mzpqnxow",
"contributions": [
"ideas",
"doc"
]
},
{
"login": "n-thumann",
"name": "Nicolas Thumann",
"avatar_url": "https://avatars.githubusercontent.com/u/46975855?v=4",
"profile": "https://n-thumann.de/",
"contributions": [
"code",
"doc"
]
},
{
"login": "tomtastic",
"name": "Tom Matthews",
"avatar_url": "https://avatars.githubusercontent.com/u/302127?v=4",
"profile": "https://github.com/tomtastic",
"contributions": [
"doc"
]
},
{
"login": "bsysop",
"name": "bsysop",
"avatar_url": "https://avatars.githubusercontent.com/u/9998303?v=4",
"profile": "https://github.com/bsysop",
"contributions": [
"doc"
]
},
{
"login": "bpsizemore",
"name": "Brian Sizemore",
"avatar_url": "https://avatars.githubusercontent.com/u/11645898?v=4",
"profile": "http://bpsizemore.me",
"contributions": [
"code"
]
},
{
"login": "noraj",
"name": "Alexandre ZANNI",
"avatar_url": "https://avatars.githubusercontent.com/u/16578570?v=4",
"profile": "https://pwn.by/noraj",
"contributions": [
"infra",
"doc"
]
},
{
"login": "craig",
"name": "Craig",
"avatar_url": "https://avatars.githubusercontent.com/u/99729?v=4",
"profile": "https://github.com/craig",
"contributions": [
"infra"
]
},
{
"login": "EONRaider",
"name": "EONRaider",
"avatar_url": "https://avatars.githubusercontent.com/u/15611424?v=4",
"profile": "https://www.reddit.com/u/EONRaider",
"contributions": [
"infra"
]
},
{
"login": "wtwver",
"name": "wtwver",
"avatar_url": "https://avatars.githubusercontent.com/u/53866088?v=4",
"profile": "https://github.com/wtwver",
"contributions": [
"infra"
]
},
{
"login": "Tib3rius",
"name": "Tib3rius",
"avatar_url": "https://avatars.githubusercontent.com/u/48113936?v=4",
"profile": "https://tib3rius.com",
"contributions": [
"bug"
]
},
{
"login": "0xdf",
"name": "0xdf",
"avatar_url": "https://avatars.githubusercontent.com/u/1489045?v=4",
"profile": "https://github.com/0xdf",
"contributions": [
"bug"
]
},
{
"login": "secure-77",
"name": "secure-77",
"avatar_url": "https://avatars.githubusercontent.com/u/31564517?v=4",
"profile": "http://secure77.de",
"contributions": [
"bug"
]
},
{
"login": "sbrun",
"name": "Sophie Brun",
"avatar_url": "https://avatars.githubusercontent.com/u/7712154?v=4",
"profile": "https://github.com/sbrun",
"contributions": [
"infra"
]
},
{
"login": "black-A",
"name": "black-A",
"avatar_url": "https://avatars.githubusercontent.com/u/30686803?v=4",
"profile": "https://github.com/black-A",
"contributions": [
"ideas"
]
},
{
"login": "dinosn",
"name": "Nicolas Krassas",
"avatar_url": "https://avatars.githubusercontent.com/u/3851678?v=4",
"profile": "https://github.com/dinosn",
"contributions": [
"ideas"
]
},
{
"login": "N0ur5",
"name": "N0ur5",
"avatar_url": "https://avatars.githubusercontent.com/u/24260009?v=4",
"profile": "https://github.com/N0ur5",
"contributions": [
"ideas"
]
},
{
"login": "moscowchill",
"name": "mchill",
"avatar_url": "https://avatars.githubusercontent.com/u/72578879?v=4",
"profile": "https://github.com/moscowchill",
"contributions": [
"bug"
]
},
{
"login": "BitThr3at",
"name": "Naman",
"avatar_url": "https://avatars.githubusercontent.com/u/45028933?v=4",
"profile": "http://BitThr3at.github.io",
"contributions": [
"bug"
]
},
{
"login": "sicks3c",
"name": "Ayoub Elaich",
"avatar_url": "https://avatars.githubusercontent.com/u/32225186?v=4",
"profile": "https://github.com/Sicks3c",
"contributions": [
"bug"
]
},
{
"login": "HenryHoggard",
"name": "Henry",
"avatar_url": "https://avatars.githubusercontent.com/u/1208121?v=4",
"profile": "https://github.com/HenryHoggard",
"contributions": [
"bug"
]
},
{
"login": "SleepiPanda",
"name": "SleepiPanda",
"avatar_url": "https://avatars.githubusercontent.com/u/6428561?v=4",
"profile": "https://github.com/SleepiPanda",
"contributions": [
"bug"
]
},
{
"login": "uBadRequest",
"name": "Bad Requests",
"avatar_url": "https://avatars.githubusercontent.com/u/47282747?v=4",
"profile": "https://github.com/uBadRequest",
"contributions": [
"bug"
]
},
{
"login": "dnaka91",
"name": "Dominik Nakamura",
"avatar_url": "https://avatars.githubusercontent.com/u/36804488?v=4",
"profile": "https://home.dnaka91.rocks",
"contributions": [
"infra"
]
},
{
"login": "hunter0x8",
"name": "Muhammad Ahsan",
"avatar_url": "https://avatars.githubusercontent.com/u/46222314?v=4",
"profile": "https://github.com/hunter0x8",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
"projectName": "feroxbuster",
"projectOwner": "epi052",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

View File

@@ -11,7 +11,7 @@ Long form explanations of most of the items below can be found in the [CONTRIBUT
## Static analysis checks
- [ ] All rust files are formatted using `cargo fmt`
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic`
- [ ] All `clippy` checks pass when running `cargo clippy --all-targets --all-features -- -D warnings -A clippy::mutex-atomic`
- [ ] All existing tests pass
## Documentation

1
.github/stale.yml vendored
View File

@@ -6,6 +6,7 @@ daysUntilClose: 7
exemptLabels:
- pinned
- security
- confirmed
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable

View File

@@ -4,6 +4,8 @@ on: [push]
jobs:
build-nix:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/main'
strategy:
@@ -38,6 +40,7 @@ jobs:
- uses: actions/checkout@v2
- name: Install System Dependencies
run: |
env
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
- uses: actions-rs/toolchain@v1
@@ -84,6 +87,8 @@ jobs:
path: ./target/x86_64-unknown-linux-musl/debian/*
build-macos:
env:
IN_PIPELINE: true
runs-on: macos-latest
if: github.ref == 'refs/heads/main'
steps:
@@ -114,6 +119,8 @@ jobs:
path: x86_64-macos-feroxbuster.tar.gz
build-windows:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
if: github.ref == 'refs/heads/main'
strategy:
@@ -146,4 +153,3 @@ jobs:
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}

34
.github/workflows/cicd-to-dockerhub.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: ci-to-dockerhub
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/feroxbuster:latest
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

2
.gitignore vendored
View File

@@ -23,7 +23,7 @@ lcov_cobertura.py
.dockerignore
# state file created during tests
ferox-http*
ferox-*.state
# python stuff cuz reasons
Pipfile*

756
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "feroxbuster"
version = "2.2.4"
version = "2.4.0"
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
license = "MIT"
edition = "2018"
@@ -19,37 +19,40 @@ maintenance = { status = "actively-developed" }
clap = "2.33"
regex = "1"
lazy_static = "1.4"
dirs = "4.0"
[dependencies]
futures = { version = "0.3.14"}
tokio = { version = "1.5.0", features = ["full"] }
tokio-util = {version = "0.6.6", features = ["codec"]}
futures = { version = "0.3"}
tokio = { version = "1.11", features = ["full"] }
tokio-util = {version = "0.6", features = ["codec"]}
log = "0.4"
env_logger = "0.8.3"
env_logger = "0.9"
reqwest = { version = "0.11", features = ["socks"] }
url = { version = "2.2", features = ["serde"]} # uses feature unification to add 'serde' to reqwest::Url
serde_regex = "1.1"
clap = "2.33"
lazy_static = "1.4"
toml = "0.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0.64"
serde_json = "1.0"
uuid = { version = "0.8", features = ["v4"] }
indicatif = "0.15"
console = "0.14"
console = "0.15"
openssl = { version = "0.10", features = ["vendored"] }
dirs = "3.0"
dirs = "4.0"
regex = "1"
crossterm = "0.19"
rlimit = "0.5.4"
ctrlc = "3.1.9"
crossterm = "0.20"
rlimit = "0.6"
ctrlc = "3.2"
fuzzyhash = "0.2.1"
anyhow = "1.0"
leaky-bucket = "0.10.0"
[dev-dependencies]
tempfile = "3.1"
httpmock = "0.5.8"
assert_cmd = "1.0.3"
predicates = "1.0.8"
httpmock = "0.6.2"
assert_cmd = "2.0"
predicates = "2.0"
[profile.release]
lto = true

View File

@@ -1,14 +1,28 @@
FROM alpine:latest
# Image: alpine:3.14.2
FROM alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a as build
LABEL maintainer="wfnintr@null.net"
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories && apk upgrade --update-cache --available
RUN sed -i -e 's/v[[:digit:]]\..*\//edge\//g' /etc/apk/repositories \
&& apk upgrade --update-cache --available && apk add --update openssl
# download default wordlists
RUN apk add --no-cache --virtual .depends subversion font-noto-emoji && \
svn export https://github.com/danielmiessler/SecLists/trunk/Discovery/Web-Content /usr/share/seclists/Discovery/Web-Content && \
apk del .depends
# install latest release
RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip -qO feroxbuster.zip && unzip -d /usr/local/bin/ feroxbuster.zip feroxbuster && rm feroxbuster.zip && chmod +x /usr/local/bin/feroxbuster
# Download latest release
RUN wget https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip -qO feroxbuster.zip \
&& unzip -d /tmp/ feroxbuster.zip feroxbuster \
&& 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
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
RUN adduser \
--gecos "" \
--disabled-password \
feroxbuster
USER feroxbuster
ENTRYPOINT ["feroxbuster"]

View File

@@ -27,12 +27,17 @@ endif
TARGET = target/$(RELEASE)
.PHONY: all clean install uninstall
.PHONY: all clean install uninstall test update
all: cli
cli: $(TARGET)/$(BIN) $(TARGET)/$(BIN).1.gz $(SHR_SOURCES)
install: all install-cli
verify:
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings -A clippy::mutex-atomic
cargo test
clean:
cargo clean

1094
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,7 @@
use std::fs::{copy, create_dir_all, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
extern crate clap;
extern crate dirs;
use clap::Shell;
@@ -20,4 +23,64 @@ fn main() {
for shell in &shells {
app.gen_completions("feroxbuster", *shell, outdir);
}
// 0xdf pointed out an oddity when tab-completing options that expect file paths, the fix we
// landed on was to add -o plusdirs to the bash completion script. The following code aims to
// automate that fix and have it present in all future builds
let mut contents = String::new();
let mut bash_file = OpenOptions::new()
.read(true)
.write(true)
.open(format!("{}/feroxbuster.bash", outdir))
.expect("Couldn't open bash completion script");
bash_file
.read_to_string(&mut contents)
.expect("Couldn't read bash completion script");
contents = contents.replace("default feroxbuster", "default -o plusdirs feroxbuster");
bash_file
.seek(SeekFrom::Start(0))
.expect("Couldn't seek to position 0 in bash completion script");
bash_file
.write_all(contents.as_bytes())
.expect("Couldn't write updated bash completion script to disk");
// hunter0x8 let me know that when installing via cargo, it would be nice if we dropped a
// config file during the build process. The following code will place an example config in
// the user's configuration directory
// - linux: $XDG_CONFIG_HOME or $HOME/.config
// - macOS: $HOME/Library/Application Support
// - windows: {FOLDERID_RoamingAppData}
let mut config_dir = dirs::config_dir().expect("Couldn't resolve user's config directory");
config_dir = config_dir.join("feroxbuster"); // $HOME/.config/feroxbuster
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;
}
}
}
// hard-coding config name here to not rely on the crate we're building, if DEFAULT_CONFIG_NAME
// ever changes, this will need to be updated
let config_file = config_dir.join("ferox-config.toml");
if !config_file.exists() {
// config file doesn't exist, add it to the config directory
if copy("ferox-config.toml.example", config_file).is_err() {
eprintln!("Couldn't copy example config into config directory");
}
}
}

0
docs/.nojekyll Normal file
View File

11
docs/index.html Normal file
View File

@@ -0,0 +1,11 @@
<html>
<head>
<meta http-equiv="refresh"
content="0; url=https://epi052.github.io/feroxbuster-docs/">
</head>
<body>
<p>The page has moved to:
<a href="https://epi052.github.io/feroxbuster-docs/">feroxbuster-docs</a></p>
</body>
</html>

View File

@@ -27,9 +27,12 @@
# output = "/targets/ellingson_mineral_company/gibson.txt"
# debug_log = "/var/log/find-the-derp.log"
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
# random_agent = false
# redirects = true
# insecure = true
# extensions = ["php", "html"]
# url_denylist = ["http://dont-scan.me", "https://also-not.me"]
# regex_denylist = ["/deny.*"]
# no_recursion = true
# add_slash = true
# stdin = true

View File

@@ -41,6 +41,7 @@ _feroxbuster() {
'--user-agent=[Sets the User-Agent (default: feroxbuster/VERSION)]' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]' \
'*--dont-scan=[URL(s) or Regex Pattern(s) to exclude from recursion/scans]' \
'*-H+[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*--headers=[Specify HTTP headers (ex: -H Header:val '\''stuff: things'\'')]' \
'*-Q+[Specify URL query parameters (ex: -Q token=stuff -Q secret=key)]' \
@@ -71,14 +72,16 @@ _feroxbuster() {
'--json[Emit JSON logs to --output and --debug-log instead of normal text]' \
'-D[Don'\''t auto-filter wildcard responses]' \
'--dont-filter[Don'\''t auto-filter wildcard responses]' \
'-A[Use a random User-Agent]' \
'--random-agent[Use a random User-Agent]' \
'-r[Follow redirects]' \
'--redirects[Follow redirects]' \
'-k[Disables TLS certificate validation]' \
'--insecure[Disables TLS certificate validation]' \
'-n[Do not scan recursively]' \
'--no-recursion[Do not scan recursively]' \
'(-x --extensions)-f[Append / to each request]' \
'(-x --extensions)--add-slash[Append / to each request]' \
'-f[Append / to each request]' \
'--add-slash[Append / to each request]' \
'(-u --url)--stdin[Read url(s) from STDIN]' \
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)]' \
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)]' \

View File

@@ -46,6 +46,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/VERSION)')
[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('--dont-scan', 'dont-scan', [CompletionResultType]::ParameterName, 'URL(s) or Regex Pattern(s) to exclude from recursion/scans')
[CompletionResult]::new('-H', 'H', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('--headers', 'headers', [CompletionResultType]::ParameterName, 'Specify HTTP headers (ex: -H Header:val ''stuff: things'')')
[CompletionResult]::new('-Q', 'Q', [CompletionResultType]::ParameterName, 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)')
@@ -76,6 +77,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--json', 'json', [CompletionResultType]::ParameterName, 'Emit JSON logs to --output and --debug-log instead of normal text')
[CompletionResult]::new('-D', 'D', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
[CompletionResult]::new('--dont-filter', 'dont-filter', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')
[CompletionResult]::new('-A', 'A', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
[CompletionResult]::new('--random-agent', 'random-agent', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
[CompletionResult]::new('-r', 'r', [CompletionResultType]::ParameterName, 'Follow redirects')
[CompletionResult]::new('--redirects', 'redirects', [CompletionResultType]::ParameterName, 'Follow redirects')
[CompletionResult]::new('-k', 'k', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation')

View File

@@ -20,7 +20,7 @@ _feroxbuster() {
case "${cmd}" in
feroxbuster)
opts=" -v -q -D -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
opts=" -v -q -D -A -r -k -n -f -e -h -V -w -u -t -d -T -p -P -R -s -o -a -x -H -Q -S -X -W -N -C -L --verbosity --silent --quiet --auto-tune --auto-bail --json --dont-filter --random-agent --redirects --insecure --no-recursion --add-slash --stdin --extract-links --help --version --wordlist --url --threads --depth --timeout --proxy --replay-proxy --replay-codes --status-codes --output --resume-from --debug-log --user-agent --extensions --dont-scan --headers --query --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --scan-limit --parallel --rate-limit --time-limit "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@@ -131,6 +131,10 @@ _feroxbuster() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dont-scan)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--headers)
COMPREPLY=($(compgen -f "${cur}"))
return 0
@@ -222,4 +226,4 @@ _feroxbuster() {
esac
}
complete -F _feroxbuster -o bashdefault -o default feroxbuster
complete -F _feroxbuster -o bashdefault -o default -o plusdirs feroxbuster

View File

@@ -12,6 +12,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l resume-from -d 'State file
complete -c feroxbuster -n "__fish_use_subcommand" -l debug-log -d 'Output file to write log entries (use w/ --json for JSON entries)'
complete -c feroxbuster -n "__fish_use_subcommand" -s a -l user-agent -d 'Sets the User-Agent (default: feroxbuster/VERSION)'
complete -c feroxbuster -n "__fish_use_subcommand" -s x -l extensions -d 'File extension(s) to search for (ex: -x php -x pdf js)'
complete -c feroxbuster -n "__fish_use_subcommand" -l dont-scan -d 'URL(s) or Regex Pattern(s) to exclude from recursion/scans'
complete -c feroxbuster -n "__fish_use_subcommand" -s H -l headers -d 'Specify HTTP headers (ex: -H Header:val \'stuff: things\')'
complete -c feroxbuster -n "__fish_use_subcommand" -s Q -l query -d 'Specify URL query parameters (ex: -Q token=stuff -Q secret=key)'
complete -c feroxbuster -n "__fish_use_subcommand" -s S -l filter-size -d 'Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)'
@@ -31,6 +32,7 @@ complete -c feroxbuster -n "__fish_use_subcommand" -l auto-tune -d 'Automaticall
complete -c feroxbuster -n "__fish_use_subcommand" -l auto-bail -d 'Automatically stop scanning when an excessive amount of errors are encountered'
complete -c feroxbuster -n "__fish_use_subcommand" -l json -d 'Emit JSON logs to --output and --debug-log instead of normal text'
complete -c feroxbuster -n "__fish_use_subcommand" -s D -l dont-filter -d 'Don\'t auto-filter wildcard responses'
complete -c feroxbuster -n "__fish_use_subcommand" -s A -l random-agent -d 'Use a random User-Agent'
complete -c feroxbuster -n "__fish_use_subcommand" -s r -l redirects -d 'Follow redirects'
complete -c feroxbuster -n "__fish_use_subcommand" -s k -l insecure -d 'Disables TLS certificate validation'
complete -c feroxbuster -n "__fish_use_subcommand" -s n -l no-recursion -d 'Do not scan recursively'

View File

@@ -50,6 +50,9 @@ pub struct Banner {
/// represents Configuration.user_agent
user_agent: BannerEntry,
/// represents Configuration.random_agent
random_agent: BannerEntry,
/// represents Configuration.config
config: BannerEntry,
@@ -134,6 +137,9 @@ pub struct Banner {
/// represents Configuration.auto_bail
auto_bail: BannerEntry,
/// represents Configuration.url_denylist
url_denylist: Vec<BannerEntry>,
/// current version of feroxbuster
pub(super) version: String,
@@ -146,6 +152,7 @@ impl Banner {
/// Create a new Banner from a Configuration and live targets
pub fn new(tgts: &[String], config: &Configuration) -> Self {
let mut targets = Vec::new();
let mut url_denylist = Vec::new();
let mut code_filters = Vec::new();
let mut replay_codes = Vec::new();
let mut headers = Vec::new();
@@ -160,6 +167,22 @@ impl Banner {
targets.push(BannerEntry::new("🎯", "Target Url", target));
}
for denied_url in &config.url_denylist {
url_denylist.push(BannerEntry::new(
"🚫",
"Don't Scan Url",
denied_url.as_str(),
));
}
for denied_regex in &config.regex_denylist {
url_denylist.push(BannerEntry::new(
"🚫",
"Don't Scan Regex",
denied_regex.as_str(),
));
}
let mut codes = vec![];
for code in &config.status_codes {
codes.push(status_colorizer(&code.to_string()))
@@ -268,6 +291,7 @@ impl Banner {
let wordlist = BannerEntry::new("📖", "Wordlist", &config.wordlist);
let timeout = BannerEntry::new("💥", "Timeout (secs)", &config.timeout.to_string());
let user_agent = BannerEntry::new("🦡", "User-Agent", &config.user_agent);
let random_agent = BannerEntry::new("🦡", "User-Agent", "Random");
let extract_links =
BannerEntry::new("🔎", "Extract Links", &config.extract_links.to_string());
let json = BannerEntry::new("🧔", "JSON Output", &config.json.to_string());
@@ -296,6 +320,7 @@ impl Banner {
filter_status,
timeout,
user_agent,
random_agent,
auto_bail,
auto_tune,
proxy,
@@ -323,6 +348,7 @@ impl Banner {
rate_limit,
scan_limit,
time_limit,
url_denylist,
config: cfg,
version: VERSION.to_string(),
update_status: UpdateStatus::Unknown,
@@ -413,6 +439,10 @@ by Ben "epi" Risher {} ver: {}"#,
writeln!(&mut writer, "{}", target)?;
}
for denied_url in &self.url_denylist {
writeln!(&mut writer, "{}", denied_url)?;
}
writeln!(&mut writer, "{}", self.threads)?;
writeln!(&mut writer, "{}", self.wordlist)?;
writeln!(&mut writer, "{}", self.status_codes)?;
@@ -424,7 +454,12 @@ by Ben "epi" Risher {} ver: {}"#,
}
writeln!(&mut writer, "{}", self.timeout)?;
writeln!(&mut writer, "{}", self.user_agent)?;
if config.random_agent {
writeln!(&mut writer, "{}", self.random_agent)?;
} else {
writeln!(&mut writer, "{}", self.user_agent)?;
}
// followed by the maybe printed or variably displayed values
if !config.config.is_empty() {

View File

@@ -10,7 +10,8 @@ use crate::{
};
use anyhow::{anyhow, Context, Result};
use clap::{value_t, ArgMatches};
use reqwest::{Client, StatusCode};
use regex::Regex;
use reqwest::{Client, StatusCode, Url};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
@@ -154,6 +155,10 @@ pub struct Configuration {
#[serde(default = "user_agent")]
pub user_agent: String,
/// Use random User-Agent
#[serde(default)]
pub random_agent: bool,
/// Follow redirects
#[serde(default)]
pub redirects: bool,
@@ -248,6 +253,13 @@ pub struct Configuration {
/// Filter out response bodies that meet a certain threshold of similarity
#[serde(default)]
pub filter_similar: Vec<String>,
/// URLs that should never be scanned/recursed into
#[serde(default)]
pub url_denylist: Vec<Url>,
#[serde(with = "serde_regex", default)]
pub regex_denylist: Vec<Regex>,
}
impl Default for Configuration {
@@ -291,6 +303,7 @@ impl Default for Configuration {
redirects: false,
no_recursion: false,
extract_links: false,
random_agent: false,
save_state: true,
proxy: String::new(),
config: String::new(),
@@ -304,6 +317,8 @@ impl Default for Configuration {
extensions: Vec::new(),
filter_size: Vec::new(),
filter_regex: Vec::new(),
url_denylist: Vec::new(),
regex_denylist: Vec::new(),
filter_line_count: Vec::new(),
filter_word_count: Vec::new(),
filter_status: Vec::new(),
@@ -339,8 +354,11 @@ impl Configuration {
/// - **auto_bail**: `false`
/// - **save_state**: `true`
/// - **user_agent**: `feroxbuster/VERSION`
/// - **random_agent**: `false`
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
/// - **extensions**: `None`
/// - **url_denylist**: `None`
/// - **regex_denylist**: `None`
/// - **filter_size**: `None`
/// - **filter_similar**: `None`
/// - **filter_regex**: `None`
@@ -538,6 +556,56 @@ impl Configuration {
config.extensions = arg.map(|val| val.to_string()).collect();
}
if args.is_present("stdin") {
config.stdin = true;
} else if let Some(url) = args.value_of("url") {
config.target_url = String::from(url);
}
if let Some(arg) = args.values_of("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
// url to be scanned. With the addition of regex support, I want to move parsing
// out of should_deny_url and into here, so it's performed once instead of thousands
// of times
for denier in arg.into_iter() {
// could be an absolute url or a regex, need to determine which and populate the
// appropriate vector
match Url::parse(denier.trim_end_matches('/')) {
Ok(absolute) => {
// denier is an absolute url and can be parsed as such
config.url_denylist.push(absolute);
}
Err(err) => {
// there are some expected errors that happen when we try to parse a url
// ex: Url::parse("/login") -> Err("relative URL without a base")
// ex: Url::parse("http:") -> Err("empty host")
//
// these are known errors and are used to determine a valid value to
// --dont-scan, when it's not an absolute url
//
// when expected errors are encountered, we're going to assume
// that the input is a regular expression to be parsed. The possibility
// exists that the user rolled their face across the keyboard and we're
// dealing with the results, in which case we'll report it as an error and
// give up
if err.to_string().contains("relative URL without a base")
|| err.to_string().contains("empty host")
{
let regex = Regex::new(denier)
.unwrap_or_else(|e| report_and_exit(&e.to_string()));
config.regex_denylist.push(regex);
} else {
// unexpected error has occurred; bail
report_and_exit(&err.to_string());
}
}
}
}
}
if let Some(arg) = args.values_of("filter_regex") {
config.filter_regex = arg.map(|val| val.to_string()).collect();
}
@@ -623,12 +691,6 @@ impl Configuration {
config.json = true;
}
if args.is_present("stdin") {
config.stdin = true;
} else if let Some(url) = args.value_of("url") {
config.target_url = String::from(url);
}
////
// organizational breakpoint; all options below alter the Client configuration
////
@@ -637,6 +699,10 @@ impl Configuration {
update_config_if_present!(&mut config.user_agent, args, "user_agent", String);
update_config_if_present!(&mut config.timeout, args, "timeout", u64);
if args.is_present("random_agent") {
config.random_agent = true;
}
if args.is_present("redirects") {
config.redirects = true;
}
@@ -767,6 +833,15 @@ impl Configuration {
update_if_not_default!(&mut conf.insecure, new.insecure, 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.url_denylist, new.url_denylist, Vec::<Url>::new());
if !new.regex_denylist.is_empty() {
// cant use the update_if_not_default macro due to the following error
//
// binary operation `!=` cannot be applied to type `Vec<regex::Regex>`
//
// if we get a non-empty list of regex in the new config, override the old
conf.regex_denylist = new.regex_denylist;
}
update_if_not_default!(&mut conf.headers, new.headers, HashMap::new());
update_if_not_default!(&mut conf.queries, new.queries, Vec::new());
update_if_not_default!(&mut conf.no_recursion, new.no_recursion, false);
@@ -809,6 +884,7 @@ impl Configuration {
update_if_not_default!(&mut conf.timeout, new.timeout, timeout());
update_if_not_default!(&mut conf.user_agent, new.user_agent, user_agent());
update_if_not_default!(&mut conf.random_agent, new.random_agent, false);
update_if_not_default!(&mut conf.threads, new.threads, threads());
update_if_not_default!(&mut conf.depth, new.depth, depth());
update_if_not_default!(&mut conf.wordlist, new.wordlist, wordlist());

View File

@@ -1,6 +1,8 @@
use super::utils::*;
use super::*;
use crate::{traits::FeroxSerialize, DEFAULT_CONFIG_NAME};
use regex::Regex;
use reqwest::Url;
use std::{collections::HashMap, fs::write};
use tempfile::TempDir;
@@ -29,6 +31,8 @@ fn setup_config_test() -> Configuration {
redirects = true
insecure = true
extensions = ["html", "php", "js"]
url_denylist = ["http://dont-scan.me", "https://also-not.me"]
regex_denylist = ["/deny.*"]
headers = {stuff = "things", mostuff = "mothings"}
queries = [["name","value"], ["rick", "astley"]]
no_recursion = true
@@ -72,24 +76,27 @@ fn default_configuration() {
assert_eq!(config.timeout, timeout());
assert_eq!(config.verbosity, 0);
assert_eq!(config.scan_limit, 0);
assert_eq!(config.silent, false);
assert_eq!(config.quiet, false);
assert!(!config.silent);
assert!(!config.quiet);
assert_eq!(config.output_level, OutputLevel::Default);
assert_eq!(config.dont_filter, false);
assert_eq!(config.auto_tune, false);
assert_eq!(config.auto_bail, false);
assert!(!config.dont_filter);
assert!(!config.auto_tune);
assert!(!config.auto_bail);
assert_eq!(config.requester_policy, RequesterPolicy::Default);
assert_eq!(config.no_recursion, false);
assert_eq!(config.json, false);
assert_eq!(config.save_state, true);
assert_eq!(config.stdin, false);
assert_eq!(config.add_slash, false);
assert_eq!(config.redirects, false);
assert_eq!(config.extract_links, false);
assert_eq!(config.insecure, false);
assert!(!config.no_recursion);
assert!(!config.random_agent);
assert!(!config.json);
assert!(config.save_state);
assert!(!config.stdin);
assert!(!config.add_slash);
assert!(!config.redirects);
assert!(!config.extract_links);
assert!(!config.insecure);
assert!(config.regex_denylist.is_empty());
assert_eq!(config.queries, Vec::new());
assert_eq!(config.extensions, Vec::<String>::new());
assert_eq!(config.filter_size, Vec::<u64>::new());
assert_eq!(config.extensions, Vec::<String>::new());
assert_eq!(config.url_denylist, Vec::<Url>::new());
assert_eq!(config.filter_regex, Vec::<String>::new());
assert_eq!(config.filter_similar, Vec::<String>::new());
assert_eq!(config.filter_word_count, Vec::<usize>::new());
@@ -186,35 +193,35 @@ fn config_reads_replay_proxy() {
/// parse the test config and see that the value parsed is correct
fn config_reads_silent() {
let config = setup_config_test();
assert_eq!(config.silent, true);
assert!(config.silent);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_quiet() {
let config = setup_config_test();
assert_eq!(config.quiet, true);
assert!(config.quiet);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_json() {
let config = setup_config_test();
assert_eq!(config.json, true);
assert!(config.json);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_auto_bail() {
let config = setup_config_test();
assert_eq!(config.auto_bail, true);
assert!(config.auto_bail);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_auto_tune() {
let config = setup_config_test();
assert_eq!(config.auto_tune, true);
assert!(config.auto_tune);
}
#[test]
@@ -235,49 +242,49 @@ fn config_reads_output() {
/// parse the test config and see that the value parsed is correct
fn config_reads_redirects() {
let config = setup_config_test();
assert_eq!(config.redirects, true);
assert!(config.redirects);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_insecure() {
let config = setup_config_test();
assert_eq!(config.insecure, true);
assert!(config.insecure);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_no_recursion() {
let config = setup_config_test();
assert_eq!(config.no_recursion, true);
assert!(config.no_recursion);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_stdin() {
let config = setup_config_test();
assert_eq!(config.stdin, true);
assert!(config.stdin);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_dont_filter() {
let config = setup_config_test();
assert_eq!(config.dont_filter, true);
assert!(config.dont_filter);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_add_slash() {
let config = setup_config_test();
assert_eq!(config.add_slash, true);
assert!(config.add_slash);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_extract_links() {
let config = setup_config_test();
assert_eq!(config.extract_links, true);
assert!(config.extract_links);
}
#[test]
@@ -287,6 +294,29 @@ fn config_reads_extensions() {
assert_eq!(config.extensions, vec!["html", "php", "js"]);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_regex_denylist() {
let config = setup_config_test();
assert_eq!(
config.regex_denylist[0].as_str(),
Regex::new("/deny.*").unwrap().as_str()
);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_url_denylist() {
let config = setup_config_test();
assert_eq!(
config.url_denylist,
vec![
Url::parse("http://dont-scan.me").unwrap(),
Url::parse("https://also-not.me").unwrap(),
]
);
}
#[test]
/// parse the test config and see that the value parsed is correct
fn config_reads_filter_regex() {
@@ -333,7 +363,7 @@ fn config_reads_filter_status() {
/// parse the test config and see that the value parsed is correct
fn config_reads_save_state() {
let config = setup_config_test();
assert_eq!(config.save_state, false);
assert!(!config.save_state);
}
#[test]
@@ -371,6 +401,12 @@ fn config_reads_queries() {
assert_eq!(config.queries, queries);
}
#[test]
fn config_default_not_random_agent() {
let config = setup_config_test();
assert!(!config.random_agent);
}
#[test]
#[should_panic]
/// test that an error message is printed and panic is called when report_and_exit is called

View File

@@ -4,6 +4,7 @@ use crate::{
scan_manager::{FeroxState, PAUSE_SCAN},
scanner::RESPONSES,
statistics::StatError,
utils::slugify_filename,
utils::{open_file, write_to},
SLEEP_DURATION,
};
@@ -17,7 +18,6 @@ use std::{
},
thread::sleep,
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
@@ -77,22 +77,14 @@ impl TermInputHandler {
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
log::trace!("enter: sigint_handler({:?})", handles);
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let slug = if !handles.config.target_url.is_empty() {
let filename = if !handles.config.target_url.is_empty() {
// target url populated
handles
.config
.target_url
.replace("://", "_")
.replace("/", "_")
.replace(".", "_")
slugify_filename(&handles.config.target_url, "ferox", "state")
} else {
// stdin used
"stdin".to_string()
slugify_filename("stdin", "ferox", "state")
};
let filename = format!("ferox-{}-{}.state", slug, ts);
let warning = format!(
"🚨 Caught {} 🚨 saving scan state to {} ...",
style("ctrl+c").yellow(),

View File

@@ -213,8 +213,9 @@ impl TermOutHandler {
// should be replayed; not using logged_request due to replay proxy client
make_request(
self.config.replay_client.as_ref().unwrap(),
&resp.url(),
resp.url(),
self.config.output_level,
&self.config,
tx_stats.clone(),
)
.await

View File

@@ -3,17 +3,19 @@ use std::sync::Arc;
use anyhow::{bail, Result};
use tokio::sync::{mpsc, Semaphore};
use crate::response::FeroxResponse;
use crate::url::FeroxUrl;
use crate::{
response::FeroxResponse,
scan_manager::{FeroxScan, FeroxScans, ScanOrder},
scanner::FeroxScanner,
statistics::StatField::TotalScans,
url::FeroxUrl,
utils::should_deny_url,
CommandReceiver, CommandSender, FeroxChannel, Joiner, SLEEP_DURATION,
};
use super::command::Command::AddToUsizeField;
use super::*;
use reqwest::Url;
use tokio::time::Duration;
#[derive(Debug)]
@@ -187,6 +189,8 @@ impl ScanHandler {
/// wrapper around scanning a url to stay DRY
async fn ordered_scan_url(&mut self, targets: Vec<String>, order: ScanOrder) -> Result<()> {
log::trace!("enter: ordered_scan_url({:?}, {:?})", targets, order);
let should_test_deny = !self.handles.config.url_denylist.is_empty()
|| !self.handles.config.regex_denylist.is_empty();
for target in targets {
if self.data.contains(&target) && matches!(order, ScanOrder::Latest) {
@@ -203,6 +207,13 @@ impl ScanHandler {
self.data.add_directory_scan(&target, order).1 // add the new target; return FeroxScan
};
if should_test_deny && should_deny_url(&Url::parse(&target)?, self.handles.clone())? {
// response was caught by a user-provided deny list
// checking this last, since it's most susceptible to longer runtimes due to what
// input is received
continue;
}
let list = self.get_wordlist()?;
log::info!("scan handler received {} - beginning scan", target);
@@ -243,6 +254,11 @@ 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
return Ok(());
}
let mut base_depth = 1_usize;
for (base_url, base_url_depth) in &self.depths {
@@ -256,11 +272,6 @@ impl ScanHandler {
return Ok(());
}
if !response.is_directory() {
// not a directory
return Ok(());
}
let targets = vec![response.url().to_string()];
self.ordered_scan_url(targets, ScanOrder::Latest).await?;

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::utils::should_deny_url;
use crate::{
client,
event_handlers::{
@@ -53,13 +54,19 @@ pub struct Extractor<'a> {
/// Extractor implementation
impl<'a> Extractor<'a> {
/// business logic that handles getting links from a normal http body response
pub async fn extract(&self) -> Result<()> {
let links = match self.target {
ExtractionTarget::ResponseBody => self.extract_from_body().await?,
ExtractionTarget::RobotsTxt => self.extract_from_robots().await?,
};
/// perform extraction from the given target and return any links found
pub async fn extract(&self) -> Result<HashSet<String>> {
log::trace!("enter: extract (this fn has associated trace exit msg)");
match self.target {
ExtractionTarget::ResponseBody => Ok(self.extract_from_body().await?),
ExtractionTarget::RobotsTxt => Ok(self.extract_from_robots().await?),
}
}
/// given a set of links from a normal http body response, task the request handler to make
/// the requests
pub async fn request_links(&self, links: HashSet<String>) -> Result<()> {
log::trace!("enter: request_links({:?})", links);
let recursive = if self.handles.config.no_recursion {
RecursionStatus::NotRecursive
} else {
@@ -121,6 +128,7 @@ impl<'a> Extractor<'a> {
rx.await?;
}
}
log::trace!("exit: request_links");
Ok(())
}
@@ -141,7 +149,7 @@ impl<'a> Extractor<'a> {
let body = self.response.unwrap().text();
for capture in self.links_regex.captures_iter(&body) {
for capture in self.links_regex.captures_iter(body) {
// remove single & double quotes from both ends of the capture
// capture[0] is the entire match, additional capture groups start at [1]
let link = capture[0].trim_matches(|c| c == '\'' || c == '"');
@@ -267,7 +275,7 @@ impl<'a> Extractor<'a> {
};
let new_url = old_url
.join(&link)
.join(link)
.with_context(|| format!("Could not join {} with {}", old_url, link))?;
links.insert(new_url.to_string());
@@ -289,10 +297,10 @@ impl<'a> Extractor<'a> {
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
log::trace!("enter: request_link({})", url);
let ferox_url = FeroxUrl::from_string(&url, self.handles.clone());
let ferox_url = FeroxUrl::from_string(url, self.handles.clone());
// create a url based on the given command line options
let new_url = ferox_url.format(&"", None)?;
let new_url = ferox_url.format("", None)?;
let scanned_urls = self.handles.ferox_scans()?;
@@ -302,6 +310,19 @@ impl<'a> Extractor<'a> {
bail!("previously seen url");
}
if (!self.handles.config.url_denylist.is_empty()
|| !self.handles.config.regex_denylist.is_empty())
&& should_deny_url(&new_url, self.handles.clone())?
{
// can't allow a denied url to be requested
bail!(
"prevented request to {} due to {:?} || {:?}",
url,
self.handles.config.url_denylist,
self.handles.config.regex_denylist,
);
}
// make the request and store the response
let new_response = logged_request(&new_url, self.handles.clone()).await?;
@@ -332,7 +353,7 @@ impl<'a> Extractor<'a> {
if let Some(new_path) = capture.name("url_path") {
let mut new_url = Url::parse(&self.url)?;
new_url.set_path(new_path.as_str());
if self.add_all_sub_paths(&new_url.path(), &mut links).is_err() {
if self.add_all_sub_paths(new_url.path(), &mut links).is_err() {
log::warn!("could not add sub-paths from {} to {:?}", new_url, links);
}
}
@@ -383,6 +404,7 @@ impl<'a> Extractor<'a> {
&client,
&url,
self.handles.config.output_level,
&self.handles.config,
self.handles.stats.tx.clone(),
)
.await?;
@@ -391,7 +413,7 @@ impl<'a> Extractor<'a> {
FeroxResponse::from(response, true, self.handles.config.output_level).await;
log::trace!("exit: get_robots_file -> {}", ferox_response);
return Ok(ferox_response);
Ok(ferox_response)
}
/// update total number of links extracted and expected responses

View File

@@ -54,8 +54,8 @@ fn setup_extractor(target: ExtractionTarget, scanned_urls: Arc<FeroxScans>) -> E
/// in the expected array
fn extractor_get_sub_paths_from_path_with_multiple_paths() {
let path = "homepage/assets/img/icons/handshake.svg";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec![
"homepage/",
"homepage/assets/",
@@ -67,8 +67,8 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -78,15 +78,15 @@ fn extractor_get_sub_paths_from_path_with_multiple_paths() {
/// returned
fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
let path = "/homepage/assets/";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec!["homepage/", "homepage/assets"];
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -95,15 +95,15 @@ fn extractor_get_sub_paths_from_path_with_enclosing_slashes() {
/// included
fn extractor_get_sub_paths_from_path_with_only_a_word() {
let path = "homepage";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec!["homepage"];
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -111,15 +111,15 @@ fn extractor_get_sub_paths_from_path_with_only_a_word() {
/// extract sub paths from the given url fragment; expect 1 sub path, forward slash removed
fn extractor_get_sub_paths_from_path_with_an_absolute_word() {
let path = "/homepage";
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(&path);
let b_paths = BODY_EXT.get_sub_paths_from_path(&path);
let r_paths = ROBOTS_EXT.get_sub_paths_from_path(path);
let b_paths = BODY_EXT.get_sub_paths_from_path(path);
let expected = vec!["homepage"];
assert_eq!(r_paths.len(), expected.len());
assert_eq!(b_paths.len(), expected.len());
for expected_path in expected {
assert_eq!(r_paths.contains(&expected_path.to_string()), true);
assert_eq!(b_paths.contains(&expected_path.to_string()), true);
assert!(r_paths.contains(&expected_path.to_string()));
assert!(b_paths.contains(&expected_path.to_string()));
}
}
@@ -211,16 +211,23 @@ async fn extractor_get_links_with_absolute_url_that_differs_from_target_domain()
let mock = srv.mock(|when, then| {
when.method(GET).path("/some-path");
then.status(200).body(
"\"http://defintely.not.a.thing.probably.com/homepage/assets/img/icons/handshake.svg\"",
"\"http://definitely.not.a.thing.probably.com/homepage/assets/img/icons/handshake.svg\"",
);
});
let client = Client::new();
let url = Url::parse(&srv.url("/some-path")).unwrap();
let config = Configuration::new().unwrap();
let response = make_request(&client, &url, OutputLevel::Default, tx_stats.clone())
.await
.unwrap();
let response = make_request(
&client,
&url,
OutputLevel::Default,
&config,
tx_stats.clone(),
)
.await
.unwrap();
let (handles, _rx) = Handles::for_testing(None, None);
let handles = Arc::new(handles);

View File

@@ -41,7 +41,7 @@ impl FeroxFilters {
if let Ok(filters) = self.filters.lock() {
for filter in filters.iter() {
// wildcard.should_filter goes here
if filter.should_filter_response(&response) {
if filter.should_filter_response(response) {
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
tx_stats
.send(AddToUsizeField(WildcardsFiltered, 1))

View File

@@ -56,7 +56,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
// add any regex filters to filters handler's FeroxFilters (-X|--filter-regex)
for regex_filter in &handles.config.filter_regex {
let raw = regex_filter;
let compiled = skip_fail!(Regex::new(&raw));
let compiled = skip_fail!(Regex::new(raw));
let filter = RegexFilter {
raw_string: raw.to_owned(),
@@ -69,7 +69,7 @@ pub async fn initialize(handles: Arc<Handles>) -> Result<()> {
// add any similarity filters to filters handler's FeroxFilters (--filter-similar-to)
for similarity_filter in &handles.config.filter_similar {
// url as-is based on input, ignores user-specified url manipulation options (add-slash etc)
let url = skip_fail!(Url::parse(&similarity_filter));
let url = skip_fail!(Url::parse(similarity_filter));
// attempt to request the given url
let resp = skip_fail!(logged_request(&url, handles.clone()).await);

View File

@@ -127,6 +127,19 @@ fn wildcard_should_filter_when_static_wildcard_found() {
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on WilcardFilter where static logic matches but response length is 0
fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
let mut resp = FeroxResponse::default();
resp.set_wildcard(true);
resp.set_url("http://localhost");
// default WildcardFilter is used in the code that executes when response.content_length() == 0
let filter = WildcardFilter::new(false);
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on WilcardFilter where dynamic logic matches
fn wildcard_should_filter_when_dynamic_wildcard_found() {

View File

@@ -68,6 +68,14 @@ impl FeroxFilter for WildcardFilter {
return true;
}
if self.size == u64::MAX && response.content_length() == 0 {
// 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.dynamic != u64::MAX {
// dynamic wildcard offset found during testing
@@ -76,7 +84,7 @@ impl FeroxFilter for WildcardFilter {
// 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());
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());

View File

@@ -156,7 +156,15 @@ impl HeuristicTests {
log::trace!("enter: make_wildcard_request({}, {})", target, length);
let unique_str = self.unique_string(length);
let nonexistent_url = target.format(&unique_str, None)?;
// 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(), self.handles.clone()).await?;
@@ -207,7 +215,7 @@ impl HeuristicTests {
let mut good_urls = vec![];
for target_url in target_urls {
let url = FeroxUrl::from_string(&target_url, self.handles.clone());
let url = FeroxUrl::from_string(target_url, self.handles.clone());
let request = skip_fail!(url.format("", None));
let result = logged_request(&request, self.handles.clone()).await;

View File

@@ -43,7 +43,7 @@ pub(crate) type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
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: usize = 8192;
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;
@@ -73,7 +73,8 @@ pub const HIGH_ERROR_RATIO: f64 = 0.90;
/// * 401 Unauthorized
/// * 403 Forbidden
/// * 405 Method Not Allowed
pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
/// * 500 Internal Server Error
pub const DEFAULT_STATUS_CODES: [StatusCode; 10] = [
StatusCode::OK,
StatusCode::NO_CONTENT,
StatusCode::MOVED_PERMANENTLY,
@@ -83,12 +84,28 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
StatusCode::UNAUTHORIZED,
StatusCode::FORBIDDEN,
StatusCode::METHOD_NOT_ALLOWED,
StatusCode::INTERNAL_SERVER_ERROR,
];
/// Default filename for config file settings
///
/// Expected location is in the same directory as the feroxbuster binary.
pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml";
/// User agents to select from when random agent is being used
pub const USER_AGENTS: [&str; 12] = [
"Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; RM-1152) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15254",
"Mozilla/5.0 (Linux; Android 7.0; Pixel C Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/52.0.2743.98 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
"Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1",
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
"Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)",
];
#[cfg(test)]
mod tests {

View File

@@ -1,8 +1,9 @@
use std::{
env::args,
fs::File,
fs::{create_dir, remove_file, File},
io::{stderr, BufRead, BufReader},
ops::Index,
path::Path,
process::Command,
sync::{atomic::Ordering, Arc},
};
@@ -27,7 +28,7 @@ use feroxbuster::{
progress::{PROGRESS_BAR, PROGRESS_PRINTER},
scan_manager::{self},
scanner,
utils::fmt_err,
utils::{fmt_err, slugify_filename},
};
#[cfg(not(target_os = "windows"))]
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
@@ -153,6 +154,28 @@ async fn get_targets(handles: Arc<Handles>) -> Result<Vec<String>> {
targets.push(handles.config.target_url.clone());
}
// remove footgun that arises if a --dont-scan value matches on a base url
for target in &targets {
for denier in &handles.config.regex_denylist {
if denier.is_match(target) {
bail!(
"The regex '{}' matches {}; the scan will never start",
denier,
target
);
}
}
for denier in &handles.config.url_denylist {
if denier.as_str().trim_end_matches('/') == target.trim_end_matches('/') {
bail!(
"The url '{}' matches {}; the scan will never start",
denier,
target
);
}
}
}
log::trace!("exit: get_targets -> {:?}", targets);
Ok(targets)
@@ -230,7 +253,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
Err(e) => {
// should only happen in the event that there was an error reading from stdin
clean_up(handles, tasks).await?;
bail!("Could not get determine initial targets: {}", e);
bail!("Could not determine initial targets: {}", e);
}
};
@@ -267,10 +290,56 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// from removing --parallel)
original.remove(parallel_index);
// to log unique files to a shared folder, we need to first check for the presence
// of -o|--output.
let out_dir = if !config.output.is_empty() {
// -o|--output was used, so we'll attempt to create a directory to store the files
let output_path = Path::new(&handles.config.output);
// this only returns None if the path terminates in `..`. Since I don't want to
// hand-hold to that degree, we'll unwrap and fail if the output path ends in `..`
let base_name = output_path.file_name().unwrap();
let new_folder = slugify_filename(&base_name.to_string_lossy(), "", "logs");
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
create_dir(&final_path).unwrap_or(());
final_path.to_string_lossy().to_string()
} else {
String::new()
};
// unvalidated targets fresh from stdin, just spawn children and let them do all checks
for target in targets {
// add the current target to the provided command
let mut cloned = original.clone();
if !out_dir.is_empty() {
// output directory value is not empty, need to join output directory with
// unique scan filename
// unwrap is ok, we already know -o was used
let out_idx = original
.iter()
.position(|s| *s == "--output" || *s == "-o")
.unwrap();
let filename = slugify_filename(&target, "ferox", "log");
let full_path = Path::new(&out_dir)
.join(filename)
.to_string_lossy()
.to_string();
// a +1 to the index is fine here, as clap has already validated that
// -o|--output has a value associated with it
cloned[out_idx + 1] = full_path;
}
cloned.push("-u".to_string());
cloned.push(target);
@@ -294,8 +363,22 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
});
}
// the output handler creates an empty file to which it will try to write, because
// this happens before we enter the --parallel branch, we need to remove that file
// if it's empty
let output = handles.config.output.to_owned();
clean_up(handles, tasks).await?;
let file = Path::new(&output);
if file.exists() {
// expectation is that this is always true for the first ferox process
if file.metadata()?.len() == 0 {
// empty file, attempt to remove it
remove_file(file)?;
}
}
log::trace!("exit: parallel branch && wrapped main");
return Ok(());
}

View File

@@ -1,6 +1,8 @@
use clap::{App, Arg, ArgGroup};
use lazy_static::lazy_static;
use regex::Regex;
use std::env;
use std::process;
lazy_static! {
/// Regex used to validate values passed to --time-limit
@@ -16,7 +18,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() -> App<'static, 'static> {
App::new("feroxbuster")
let mut app = App::new("feroxbuster")
.version(env!("CARGO_PKG_VERSION"))
.author("Ben 'epi' Risher (@epi052)")
.about("A fast, simple, recursive content discovery tool written in Rust")
@@ -190,6 +192,15 @@ pub fn initialize() -> App<'static, 'static> {
"Sets the User-Agent (default: feroxbuster/VERSION)"
),
)
.arg(
Arg::with_name("random_agent")
.short("A")
.long("random-agent")
.takes_value(false)
.help(
"Use a random User-Agent"
),
)
.arg(
Arg::with_name("redirects")
.short("r")
@@ -216,6 +227,17 @@ pub fn initialize() -> App<'static, 'static> {
"File extension(s) to search for (ex: -x php -x pdf js)",
),
)
.arg(
Arg::with_name("url_denylist")
.long("dont-scan")
.value_name("URL")
.takes_value(true)
.multiple(true)
.use_delimiter(true)
.help(
"URL(s) or Regex Pattern(s) to exclude from recursion/scans",
),
)
.arg(
Arg::with_name("headers")
.short("H")
@@ -252,7 +274,6 @@ pub fn initialize() -> App<'static, 'static> {
.short("f")
.long("add-slash")
.takes_value(false)
.conflicts_with("extensions")
.help("Append / to each request")
)
.arg(
@@ -410,7 +431,20 @@ EXAMPLES:
Ludicrous speed... go!
./feroxbuster -u http://127.1 -t 200
"#)
"#);
for arg in env::args() {
// secure-77 noticed that when an incorrect flag/option is used, the short help message is printed
// which is fine, but if you add -h|--help, it still errors out on the bad flag/option,
// never showing the full help message. This code addresses that behavior
if arg == "--help" || arg == "-h" {
app.print_long_help().unwrap();
println!(); // just a newline to mirror original --help output
process::exit(0);
}
}
app
}
/// Validate that a string is formatted as a number followed by s, m, h, or d (10d, 30s, etc...)
@@ -432,7 +466,7 @@ mod tests {
use super::*;
#[test]
/// initalize parser, expect a clap::App returned
/// initialize parser, expect a clap::App returned
fn parser_initialize_gives_defaults() {
let app = initialize();
assert_eq!(app.get_name(), "feroxbuster");

View File

@@ -51,7 +51,7 @@ pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar {
progress_bar.set_style(style);
progress_bar.set_prefix(&prefix);
progress_bar.set_prefix(prefix);
progress_bar
}

View File

@@ -121,7 +121,7 @@ impl FeroxResponse {
/// Set `FeroxResponse`'s `url` attribute, has no affect if an error occurs
pub fn set_url(&mut self, url: &str) {
match Url::parse(&url) {
match Url::parse(url) {
Ok(url) => {
self.url = url;
}
@@ -339,7 +339,7 @@ impl FeroxSerialize for FeroxResponse {
lines,
words,
chars,
status_colorizer(&status),
status_colorizer(status),
self.url(),
FeroxUrl::path_length_of_url(&self.url)
);

View File

@@ -36,7 +36,7 @@ pub struct FeroxScan {
pub(super) scan_type: ScanType,
/// The order in which the scan was received
pub(super) scan_order: ScanOrder,
pub(crate) scan_order: ScanOrder,
/// Number of requests to populate the progress bar with
pub(super) num_requests: u64,

View File

@@ -431,7 +431,7 @@ impl FeroxScans {
OutputLevel::Silent => BarType::Hidden,
};
let progress_bar = add_bar(&url, bar_length, bar_type);
let progress_bar = add_bar(url, bar_length, bar_type);
progress_bar.reset_elapsed();
@@ -441,7 +441,7 @@ impl FeroxScans {
};
let ferox_scan = FeroxScan::new(
&url,
url,
scan_type,
scan_order,
bar_length,
@@ -462,7 +462,7 @@ 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)
self.add_scan(url, ScanType::Directory, scan_order)
}
/// Given a url, create a new `FeroxScan` and add it to `FeroxScans` as a File Scan
@@ -471,7 +471,7 @@ impl FeroxScans {
///
/// Also return a reference to the new `FeroxScan`
pub fn add_file_scan(&self, url: &str, scan_order: ScanOrder) -> (bool, Arc<FeroxScan>) {
self.add_scan(&url, ScanType::File, scan_order)
self.add_scan(url, ScanType::File, scan_order)
}
/// small helper to determine whether any scans are active or not

View File

@@ -52,7 +52,7 @@ fn add_url_to_list_of_scanned_urls_with_unknown_url() {
let urls = FeroxScans::default();
let url = "http://unknown_url";
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
assert_eq!(result, true);
assert!(result);
}
#[test]
@@ -71,11 +71,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url() {
Some(pb),
);
assert_eq!(urls.insert(scan), true);
assert!(urls.insert(scan));
let (result, _scan) = urls.add_scan(url, ScanType::Directory, ScanOrder::Latest);
assert_eq!(result, false);
assert!(!result);
}
#[test]
@@ -93,27 +93,23 @@ fn stop_progress_bar_stops_bar() {
Some(pb),
);
assert_eq!(
scan.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished(),
false
);
assert!(!scan
.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished());
scan.stop_progress_bar();
assert_eq!(
scan.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished(),
true
);
assert!(scan
.progress_bar
.lock()
.unwrap()
.as_ref()
.unwrap()
.is_finished());
}
#[test]
@@ -131,11 +127,11 @@ fn add_url_to_list_of_scanned_urls_with_known_url_without_slash() {
None,
);
assert_eq!(urls.insert(scan), true);
assert!(urls.insert(scan));
let (result, _scan) = urls.add_scan(url, ScanType::File, ScanOrder::Latest);
assert_eq!(result, false);
assert!(!result);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -171,8 +167,8 @@ async fn call_display_scans() {
.await
.unwrap();
assert_eq!(urls.insert(scan), true);
assert_eq!(urls.insert(scan_two), true);
assert!(urls.insert(scan));
assert!(urls.insert(scan_two));
urls.display_scans().await;
}
@@ -330,7 +326,7 @@ fn ferox_response_serialize_and_deserialize() {
assert_eq!(response.url().as_str(), "https://nerdcore.com/css");
assert_eq!(response.url().path(), "/css");
assert_eq!(response.wildcard(), true);
assert!(response.wildcard());
assert_eq!(response.status().as_u16(), 301);
assert_eq!(response.content_length(), 173);
assert_eq!(response.line_count(), 10);
@@ -382,12 +378,79 @@ fn feroxstates_feroxserialize_implementation() {
assert!(expected_strs.eval(&ferox_state.as_str()));
let json_state = ferox_state.as_json().unwrap();
let expected = format!(
r#"{{"scans":[{{"id":"{}","url":"https://spiritanimal.com","scan_type":"Directory","status":"NotStarted","num_requests":0}}],"config":{{"type":"configuration","wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt","config":"","proxy":"","replay_proxy":"","target_url":"","status_codes":[200,204,301,302,307,308,401,403,405],"replay_codes":[200,204,301,302,307,308,401,403,405],"filter_status":[],"threads":50,"timeout":7,"verbosity":0,"silent":false,"quiet":false,"auto_bail":false,"auto_tune":false,"json":false,"output":"","debug_log":"","user_agent":"feroxbuster/{}","redirects":false,"insecure":false,"extensions":[],"headers":{{}},"queries":[],"no_recursion":false,"extract_links":false,"add_slash":false,"stdin":false,"depth":4,"scan_limit":0,"parallel":0,"rate_limit":0,"filter_size":[],"filter_line_count":[],"filter_word_count":[],"filter_regex":[],"dont_filter":false,"resumed":false,"resume_from":"","save_state":false,"time_limit":"","filter_similar":[]}},"responses":[{{"type":"response","url":"https://nerdcore.com/css","path":"/css","wildcard":true,"status":301,"content_length":173,"line_count":10,"word_count":16,"headers":{{"server":"nginx/1.16.1"}}}}]"#,
saved_id, VERSION
);
println!("{}\n{}", expected, json_state);
assert!(predicates::str::contains(expected).eval(&json_state));
for expected in [
r#""scans""#,
&format!(r#""id":"{}""#, saved_id),
r#""url":"https://spiritanimal.com""#,
r#""scan_type":"Directory""#,
r#""status":"NotStarted""#,
r#""num_requests":0"#,
r#""config""#,
r#""type":"configuration""#,
r#""wordlist":"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt""#,
r#""config""#,
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#""filter_status":[]"#,
r#""threads":50"#,
r#""timeout":7"#,
r#""verbosity":0"#,
r#""silent":false"#,
r#""quiet":false"#,
r#""auto_bail":false"#,
r#""auto_tune":false"#,
r#""json":false"#,
r#""output":"""#,
r#""debug_log":"""#,
&format!(r#""user_agent":"feroxbuster/{}""#, VERSION),
r#""random_agent":false"#,
r#""redirects":false"#,
r#""insecure":false"#,
r#""extensions":[]"#,
r#""headers""#,
r#""queries":[]"#,
r#""no_recursion":false"#,
r#""extract_links":false"#,
r#""add_slash":false"#,
r#""stdin":false"#,
r#""depth":4"#,
r#""scan_limit":0"#,
r#""parallel":0"#,
r#""rate_limit":0"#,
r#""filter_size":[]"#,
r#""filter_line_count":[]"#,
r#""filter_word_count":[]"#,
r#""filter_regex":[]"#,
r#""dont_filter":false"#,
r#""resumed":false"#,
r#""resume_from":"""#,
r#""save_state":false"#,
r#""time_limit":"""#,
r#""filter_similar":[]"#,
r#""url_denylist":[]"#,
r#""responses""#,
r#""type":"response""#,
r#""url":"https://nerdcore.com/css""#,
r#""path":"/css""#,
r#""wildcard":true"#,
r#""status":301"#,
r#""content_length":173"#,
r#""line_count":10"#,
r#""word_count":16"#,
r#""headers""#,
r#""server":"nginx/1.16.1"#,
]
.iter()
{
assert!(
predicates::str::contains(*expected).eval(&json_state),
"{}",
expected
)
}
}
#[should_panic]

View File

@@ -83,7 +83,8 @@ impl FeroxScanner {
.target(RobotsTxt)
.build()?;
let _ = extractor.extract().await;
let links = extractor.extract().await?;
extractor.request_links(links).await?;
}
let scanned_urls = self.handles.ferox_scans()?;

View File

@@ -90,11 +90,9 @@ impl PolicyData {
atomic_store!(self.remove_limit, true);
}
}
self.set_limit(heap.value() as usize);
} else if heap.has_children() {
// streak not at 3, just check that we can move down, and do so
heap.move_left();
self.set_limit(heap.value() as usize);
} else {
// tree bottomed out, need to move back up the tree a bit
let current = heap.value();
@@ -104,9 +102,8 @@ impl PolicyData {
if current > heap.value() {
heap.move_up();
}
self.set_limit(heap.value() as usize);
}
self.set_limit(heap.value() as usize);
}
}
@@ -200,7 +197,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 300);
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -217,7 +214,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 200);
assert_eq!(pd.limit.load(Ordering::Relaxed), 200);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), true);
assert!(pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -234,7 +231,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 350);
assert_eq!(pd.limit.load(Ordering::Relaxed), 350);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -251,7 +248,7 @@ mod tests {
pd.adjust_up(&3);
assert_eq!(pd.heap.read().unwrap().value(), 300);
assert_eq!(pd.limit.load(Ordering::Relaxed), 300);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -269,7 +266,7 @@ mod tests {
pd.adjust_up(&0);
assert_eq!(pd.heap.read().unwrap().value(), 43);
assert_eq!(pd.limit.load(Ordering::Relaxed), 43);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]
@@ -287,7 +284,7 @@ mod tests {
pd.adjust_up(&0);
assert_eq!(pd.heap.read().unwrap().value(), 37);
assert_eq!(pd.limit.load(Ordering::Relaxed), 37);
assert_eq!(pd.remove_limit.load(Ordering::Relaxed), false);
assert!(!pd.remove_limit.load(Ordering::Relaxed));
}
#[test]

View File

@@ -27,6 +27,8 @@ use crate::{
};
use super::{policy_data::PolicyData, FeroxScanner, PolicyTrigger};
use crate::utils::should_deny_url;
use std::collections::HashSet;
/// Makes multiple requests based on the presence of extensions
pub(super) struct Requester {
@@ -45,11 +47,16 @@ pub(super) struct Requester {
/// FeroxScan associated with the creation of this Requester
ferox_scan: Arc<FeroxScan>,
/// cache of previously seen links gotten via link extraction. since the requester is passed
/// around as an arc, and seen_links needs to be mutable, putting it behind a lock for
/// interior mutability, similar to the tuning_lock below
seen_links: RwLock<HashSet<String>>,
/// simple lock to control access to tuning to a single thread (per-scan)
///
/// need a usize to determine the number of consecutive non-error calls that a requester has
/// seen; this will satisfy the non-mut self constraint (due to us being behind an Arc, and
/// the need for a counter
/// the need for a counter)
tuning_lock: Mutex<usize>,
}
@@ -73,6 +80,7 @@ impl Requester {
Ok(Self {
ferox_scan,
policy_data,
seen_links: RwLock::new(HashSet::<String>::new()),
rate_limiter: RwLock::new(rate_limiter),
handles: scanner.handles.clone(),
target_url: scanner.target_url.to_owned(),
@@ -298,6 +306,9 @@ impl Requester {
let urls =
FeroxUrl::from_string(&self.target_url, self.handles.clone()).formatted_urls(word)?;
let should_test_deny = !self.handles.config.url_denylist.is_empty()
|| !self.handles.config.regex_denylist.is_empty();
for url in urls {
// auto_tune is true, or rate_limit was set (mutually exclusive to user)
// and a rate_limiter has been created
@@ -313,6 +324,11 @@ impl Requester {
}
}
if should_test_deny && should_deny_url(&url, self.handles.clone())? {
// can't allow a denied url to be requested
continue;
}
let response = logged_request(&url, self.handles.clone()).await?;
if (should_tune || self.handles.config.auto_bail)
@@ -367,7 +383,26 @@ impl Requester {
.handles(self.handles.clone())
.build()?;
extractor.extract().await?;
let new_links: HashSet<_>;
let extracted = extractor.extract().await?;
{
// gain and quickly drop the read lock on seen_links, using it while unlocked
// to determine if there are any new links to process
let read_links = self.seen_links.read().await;
new_links = extracted.difference(&read_links).cloned().collect();
}
if !new_links.is_empty() {
// using is_empty instead of direct iteration to acquire the write lock behind
// some kind of less expensive gate (and not in a loop, obv)
let mut write_links = self.seen_links.write().await;
for new_link in &new_links {
write_links.insert(new_link.to_owned());
}
}
extractor.request_links(new_links).await?;
}
// everything else should be reported
@@ -536,6 +571,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -563,6 +599,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
@@ -587,6 +624,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
@@ -626,6 +664,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: ferox_scan.clone(),
target_url: "http://localhost".to_string(),
@@ -680,6 +719,7 @@ mod tests {
let req_clone = scan_two.clone();
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: req_clone,
target_url: "http://one/one/stuff.php".to_string(),
@@ -713,6 +753,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://one/one/stuff.php".to_string(),
@@ -734,6 +775,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -756,6 +798,7 @@ mod tests {
let requester = Arc::new(Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -772,7 +815,7 @@ mod tests {
requester.cool_down().await;
assert_eq!(resp.await.unwrap(), true);
assert!(resp.await.unwrap());
println!("{}", start.elapsed().as_millis());
assert!(start.elapsed().as_millis() >= 3500);
}
@@ -785,6 +828,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -822,6 +866,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(scan),
target_url: "http://localhost".to_string(),
@@ -857,6 +902,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(scan),
target_url: "http://localhost".to_string(),
@@ -884,6 +930,7 @@ mod tests {
let mut requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -891,28 +938,16 @@ mod tests {
policy_data: PolicyData::new(RequesterPolicy::AutoBail, 7),
};
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Errors),
false
);
assert!(!requester.too_many_status_errors(PolicyTrigger::Errors));
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status429),
false
);
assert!(!requester.too_many_status_errors(PolicyTrigger::Status429));
requester.ferox_scan.progress_bar().set_position(10);
requester.ferox_scan.add_429();
requester.ferox_scan.add_429();
requester.ferox_scan.add_429();
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status429),
true
);
assert!(requester.too_many_status_errors(PolicyTrigger::Status429));
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status403),
false
);
assert!(!requester.too_many_status_errors(PolicyTrigger::Status403));
requester.ferox_scan = Arc::new(FeroxScan::default());
requester.ferox_scan.progress_bar().set_position(10);
requester.ferox_scan.add_403();
@@ -924,10 +959,7 @@ mod tests {
requester.ferox_scan.add_403();
requester.ferox_scan.add_403();
requester.ferox_scan.add_403();
assert_eq!(
requester.too_many_status_errors(PolicyTrigger::Status403),
true
);
assert!(requester.too_many_status_errors(PolicyTrigger::Status403));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -941,6 +973,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: Arc::new(FeroxScan::default()),
target_url: "http://localhost".to_string(),
@@ -983,6 +1016,7 @@ mod tests {
let requester = Requester {
handles,
seen_links: RwLock::new(HashSet::<String>::new()),
tuning_lock: Mutex::new(0),
ferox_scan: scan.clone(),
target_url: "http://localhost".to_string(),

View File

@@ -620,12 +620,10 @@ impl Stats {
atomic_increment!(self.expected_per_scan, value);
}
StatField::TotalScans => {
let multiplier = self.num_extensions.max(1);
atomic_increment!(self.total_scans, value);
atomic_increment!(
self.total_expected,
value * self.expected_per_scan.load(Ordering::Relaxed) * multiplier
value * self.expected_per_scan.load(Ordering::Relaxed)
);
}
StatField::TotalExpected => {

View File

@@ -55,10 +55,7 @@ fn save_writes_stats_object_to_disk() {
stats.add_status_code(StatusCode::OK);
stats.add_status_code(StatusCode::OK);
let outfile = NamedTempFile::new().unwrap();
if stats
.save(174.33, &outfile.path().to_str().unwrap())
.is_ok()
{}
if stats.save(174.33, outfile.path().to_str().unwrap()).is_ok() {}
assert!(stats.as_json().unwrap().contains("statistics"));
assert!(stats.as_json().unwrap().contains("11")); // requests made

View File

@@ -42,7 +42,13 @@ impl FeroxUrl {
let mut urls = vec![];
match self.format(word, None) {
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
match self.format(word, slash) {
// default request, i.e. no extension
Ok(url) => urls.push(url),
Err(_) => self.handles.stats.send(AddError(UrlFormat))?,
@@ -55,7 +61,6 @@ impl FeroxUrl {
Err(_) => self.handles.stats.send(AddError(UrlFormat))?,
}
}
log::trace!("exit: formatted_urls -> {:?}", urls);
Ok(urls)
}
@@ -66,7 +71,7 @@ impl FeroxUrl {
pub fn format(&self, word: &str, extension: Option<&str>) -> Result<Url> {
log::trace!("enter: format({}, {:?})", word, extension);
if Url::parse(&word).is_ok() {
if Url::parse(word).is_ok() {
// when a full url is passed in as a word to be joined to a base url using
// reqwest::Url::join, the result is that the word (url) completely overwrites the base
// url, potentially resulting in requests to places that aren't actually the target
@@ -97,13 +102,25 @@ impl FeroxUrl {
self.target.to_string()
};
// extensions and slashes are mutually exclusive cases
let word = if extension.is_some() {
format!("{}.{}", word, extension.unwrap())
} else if self.handles.config.add_slash && !word.ends_with('/') {
// -f used, and word doesn't already end with a /
format!("{}/", word)
} else if word.starts_with("//") {
// As of version 2.3.4, extensions and trailing slashes are no longer mutually exclusive.
// Trailing slashes are now treated as just another extension, which is pretty clever.
//
// In addition to the change above, @cortantief ID'd a bug here that incorrectly handled
// 2 leading forward slashes when extensions were used. This block addresses the bugfix.
let mut word = if let Some(ext) = extension {
// 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)
} else {
format!("{}.{}", word, ext)
}
} else {
String::from(word)
};
// We check separately if the current word begins with 2 forward slashes
if word.starts_with("//") {
// bug ID'd by @Sicks3c, when a wordlist contains words that begin with 2 forward slashes
// i.e. //1_40_0/static/js, it gets joined onto the base url in a surprising way
// ex: https://localhost/ + //1_40_0/static/js -> https://1_40_0/static/js
@@ -111,9 +128,7 @@ impl FeroxUrl {
// and simply removes prefixed forward slashes if there are two of them. Additionally,
// trim_start_matches will trim the pattern until it's gone, so even if there are more than
// 2 /'s, they'll still be trimmed
word.trim_start_matches('/').to_string()
} else {
String::from(word)
word = word.trim_start_matches('/').to_string();
};
let base_url = Url::parse(&url)?;
@@ -451,6 +466,20 @@ mod tests {
);
}
#[test]
/// word with two prepended slashes and extensions doesn't discard the entire domain
fn format_url_word_with_two_prepended_slashes_and_extensions() {
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);
assert_eq!(
url.format("//upload/ferox", Some(ext)).unwrap(),
reqwest::Url::parse(&to_check[..]).unwrap()
);
}
}
#[test]
/// word that is a fully formed url, should return an error
fn format_url_word_that_is_a_url() {
@@ -460,4 +489,33 @@ mod tests {
assert!(formatted.is_err());
}
#[test]
/// sending url + word with both an extension and add-slash should get back
/// two urls, one with '/' appended to the word, and the other with the extension
/// appended
fn formatted_urls_with_postslash_and_extensions() {
let config = Configuration {
add_slash: true,
extensions: vec!["rocks".to_string(), "fun".to_string()],
..Default::default()
};
let handles = Arc::new(Handles::for_testing(None, Some(Arc::new(config))).0);
let url = FeroxUrl::from_string("http://localhost", handles);
match url.formatted_urls("ferox") {
Ok(urls) => {
// 3 = One for the main word + slash and for the two extensions
assert_eq!(urls.len(), 3);
assert_eq!(
urls,
[
Url::parse("http://localhost/ferox/").unwrap(),
Url::parse("http://localhost/ferox.rocks").unwrap(),
Url::parse("http://localhost/ferox.fun").unwrap(),
]
)
}
Err(err) => panic!("{}", err.to_string()),
}
}
}

View File

@@ -1,16 +1,20 @@
use anyhow::{bail, Context, Result};
use console::{strip_ansi_codes, style, user_attended};
use indicatif::ProgressBar;
use regex::Regex;
use reqwest::{Client, Response, StatusCode, Url};
#[cfg(not(target_os = "windows"))]
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
use rlimit::{getrlimit, setrlimit, Resource};
use std::{
fs,
io::{self, BufWriter, Write},
sync::Arc,
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::sync::mpsc::UnboundedSender;
use crate::config::Configuration;
use crate::{
config::OutputLevel,
event_handlers::{
@@ -21,8 +25,12 @@ use crate::{
send_command,
statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
traits::FeroxSerialize,
USER_AGENTS,
};
/// simple counter for grabbing 'random' user agents
static mut USER_AGENT_CTR: usize = 0;
/// Given the path to a file, open the file in append mode (create it if it doesn't exist) and
/// return a reference to the buffered file
pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
@@ -92,7 +100,7 @@ pub async fn logged_request(url: &Url, handles: Arc<Handles>) -> Result<Response
let level = handles.config.output_level;
let tx_stats = handles.stats.tx.clone();
let response = make_request(client, url, level, tx_stats).await;
let response = make_request(client, url, level, &handles.config, tx_stats).await;
let scans = handles.ferox_scans()?;
@@ -119,6 +127,7 @@ pub async fn make_request(
client: &Client,
url: &Url,
output_level: OutputLevel,
config: &Configuration,
tx_stats: UnboundedSender<Command>,
) -> Result<Response> {
log::trace!(
@@ -128,7 +137,20 @@ pub async fn make_request(
tx_stats
);
match client.get(url.to_owned()).send().await {
let mut request = client.get(url.to_owned());
if config.random_agent {
let index = unsafe {
USER_AGENT_CTR += 1;
USER_AGENT_CTR % USER_AGENTS.len()
};
let user_agent = USER_AGENTS[index];
request = request.header("User-Agent", user_agent);
}
match request.send().await {
Err(e) => {
log::trace!("exit: make_request -> {}", e);
@@ -217,16 +239,15 @@ pub fn create_report_string(
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
/// there are none).
#[cfg(not(target_os = "windows"))]
pub fn set_open_file_limit(limit: usize) -> bool {
pub fn set_open_file_limit(limit: u64) -> bool {
log::trace!("enter: set_open_file_limit");
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
if hard.as_usize() > limit {
if hard > limit {
// our default open file limit is less than the current hard limit, this means we can
// set the soft limit to our default
let new_soft_limit = Rlim::from_usize(limit);
if setrlimit(Resource::NOFILE, new_soft_limit, hard).is_ok() {
if setrlimit(Resource::NOFILE, limit, hard).is_ok() {
log::debug!("set open file descriptor limit to {}", limit);
log::trace!("exit: set_open_file_limit -> {}", true);
@@ -287,15 +308,183 @@ where
Ok(())
}
/// determine if a url should be denied based on the given absolute url
fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>) -> Result<bool> {
log::trace!(
"enter: should_deny_absolute({}, {:?})",
url_to_test.as_str(),
denier.as_str(),
);
// simplest case is an exact match, check for it first
if url_to_test == denier {
log::trace!("exit: should_deny_absolute -> true");
return Ok(true);
}
match (url_to_test.host(), denier.host()) {
// .host() will return an enum with ipv4|6 or domain and is comparable
// whereas .domain() returns None for ip addresses
(Some(normed_host), Some(denier_host)) => {
if normed_host != denier_host {
// domains don't even match
return Ok(false);
}
}
_ => {
// one or the other couldn't determine the host value, which probably means
// it's not suitable for further comparison
return Ok(false);
}
}
let tested_host = url_to_test.host().unwrap(); // match above will catch errors
// at this point, we have a matching set of ips or domain names. now we can process the
// url path. The goal is to determine whether the given url's path is a subpath of any
// url in the deny list, for example
// GIVEN URL URL DENY LIST USER-SPECIFIED URLS TO SCAN
// http://some.domain/stuff/things, [http://some.domain/stuff], [http://some.domain] => true
// http://some.domain/stuff/things, [http://some.domain/stuff/things], [http://some.domain] => true
// http://some.domain/stuff/things, [http://some.domain/api], [http://some.domain] => false
// the examples above are all pretty obvious, the kicker comes when the blocking url's
// path is a parent to a scanned url
// http://some.domain/stuff/things, [http://some.domain/], [http://some.domain/stuff] => false
// http://some.domain/api, [http://some.domain/], [http://some.domain/stuff] => true
// we want to deny all children of the parent, unless that child is a child of a scan
// we specified through -u(s) or --stdin
let deny_path = denier.path();
let tested_path = url_to_test.path();
if tested_path.starts_with(deny_path) {
// at this point, we know that the given normalized path is a sub-path of the
// current deny-url, now we just need to check to see if this deny-url is a parent
// 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))?;
if let Some(scan_host) = scanner.host() {
// same domain/ip check we perform on the denier above
if tested_host != scan_host {
// domains don't even match, keep on keepin' on...
continue;
}
} else {
// couldn't process .host from scanner
continue;
};
let scan_path = scanner.path();
if scan_path.starts_with(deny_path) && tested_path.starts_with(scan_path) {
// user-specified scan url is a sub-path of the deny-urls's path AND the
// url to check is a sub-path of the user-specified scan url
//
// the assumption is the user knew what they wanted and we're going to give
// the scanned url precedence, even though it's a sub-path
log::trace!("exit: should_deny_absolute -> false");
return Ok(false);
}
}
log::trace!("exit: should_deny_absolute -> true");
return Ok(true);
}
log::trace!("exit: should_deny_absolute -> false");
Ok(false)
}
/// determine if a url should be denied based on the given regular expression
///
/// the regex ONLY matches against the PATH of the url (not the scheme, host, port, etc)
fn should_deny_regex(url_to_test: &Url, denier: &Regex) -> bool {
log::trace!(
"enter: should_deny_regex({}, {})",
url_to_test.as_str(),
denier,
);
let result = denier.is_match(url_to_test.as_str());
log::trace!("exit: should_deny_regex -> {}", result);
result
}
/// determines whether or not a given url should be denied based on the user-supplied --dont-scan
/// flag
pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
log::trace!(
"enter: should_deny_url({}, {:?}, {:?})",
url.as_str(),
handles.config.url_denylist,
handles.ferox_scans()?
);
// normalization for comparison is to remove the trailing / if one exists, this is done for
// the given url and any url to which it's compared
let normed_url = Url::parse(url.to_string().trim_end_matches('/'))?;
for denier in &handles.config.url_denylist {
// note to self: it may seem as though we can use regex only for --dont-scan, however, in
// doing so, we lose the ability to block a parent directory while scanning a child
if let Ok(should_deny) = should_deny_absolute(&normed_url, denier, handles.clone()) {
if should_deny {
return Ok(true);
}
}
}
for denier in &handles.config.regex_denylist {
if should_deny_regex(&normed_url, denier) {
return Ok(true);
}
}
// made it to the end of the deny lists unscathed, return false, indicating we should not deny
// this particular url
log::trace!("exit: should_deny_url -> false");
Ok(false)
}
/// given a url and filename-suffix, return a unique filename comprised of the slugified url,
/// current unix timestamp and suffix
///
/// ex: ferox-http_telsa_com-1606947491.state
pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
log::trace!("enter: slugify({:?}, {:?}, {:?})", url, prefix, suffix);
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs();
let altered_prefix = if !prefix.is_empty() {
format!("{}-", prefix)
} else {
String::new()
};
let slug = url.replace("://", "_").replace("/", "_").replace(".", "_");
let filename = format!("{}{}-{}.{}", altered_prefix, slug, ts, suffix);
log::trace!("exit: slugify -> {}", filename);
filename
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Configuration;
use crate::scan_manager::{FeroxScans, ScanOrder};
#[test]
/// set_open_file_limit with a low requested limit succeeds
fn utils_set_open_file_limit_with_low_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let lower_limit = hard.as_usize() - 1;
let lower_limit = hard - 1;
assert!(set_open_file_limit(lower_limit));
}
@@ -303,9 +492,9 @@ mod tests {
/// set_open_file_limit with a high requested limit succeeds
fn utils_set_open_file_limit_with_high_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let higher_limit = hard.as_usize() + 1;
let higher_limit = hard + 1;
// calculate a new soft to ensure soft != hard and hit that logic branch
let new_soft = Rlim::from_usize(hard.as_usize() - 1);
let new_soft = hard - 1;
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
assert!(set_open_file_limit(higher_limit));
}
@@ -316,7 +505,7 @@ mod tests {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
// calculate a new soft to ensure soft == hard and hit the failure logic branch
setrlimit(Resource::NOFILE, hard, hard).unwrap();
assert!(!set_open_file_limit(hard.as_usize())); // returns false
assert!(!set_open_file_limit(hard)); // returns false
}
#[test]
@@ -366,4 +555,222 @@ mod tests {
fn status_colorizer_returns_as_is() {
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
}
#[test]
/// provide a url that should be blocked where the denier is an exact match for the tested url
/// expect true
fn should_deny_url_blocks_when_denier_is_exact_match() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a url that has a different host than the denier but the same path, expect false
fn should_deny_url_doesnt_compare_mismatched_domains() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://dev.testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier from which we can't check a host, which results in no comparison, expect false
fn should_deny_url_doesnt_compare_non_domains() {
let scan_url = "https://testdomain.com/";
let deny_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a url that has a different host than the denier but the same path, expect false
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
/// still returns true
fn should_deny_url_doesnt_compare_mismatched_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "https://dev.testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier from which we can't check a host, which results in no comparison, expect false
/// because the denier is a parent to the tested, even tho the scanned doesn't compare, it
/// still returns true
fn should_deny_url_doesnt_compare_non_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
fn should_deny_url_blocks_child() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/api";
let tested_url = Url::parse("https://testdomain.com/api/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
fn should_deny_url_doesnt_block_non_child() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/api";
let tested_url = Url::parse("https://testdomain.com/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is a sub-path and the scanned url is not, expect true
fn should_deny_url_blocks_child_when_scan_url_isnt_parent() {
let scan_url = "https://testdomain.com/api";
let deny_url = "https://testdomain.com/";
let tested_url = Url::parse("https://testdomain.com/stuff/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is not a sub-path and the scanned url is not, expect false
fn should_deny_url_doesnt_block_child_when_scan_url_is_parent() {
let scan_url = "https://testdomain.com/api";
let deny_url = "https://testdomain.com/";
let tested_url = Url::parse("https://testdomain.com/api/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is matched against a regular expression in the path
/// of the url
fn should_deny_url_blocks_urls_based_on_regex_in_path() {
let scan_url = "https://testdomain.com/";
let deny_pattern = "/deni.*";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
/// provide a denier where the tested url is matched against a regular expression in the scheme
/// of the url
fn should_deny_url_blocks_urls_based_on_regex_in_scheme() {
let scan_url = "https://testdomain.com/";
let deny_pattern = "http:";
let tested_http_url = Url::parse("http://testdomain.com/denied/").unwrap();
let tested_https_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(scan_url, ScanOrder::Initial);
let mut config = Configuration::new().unwrap();
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_https_url, handles.clone()).unwrap());
assert!(should_deny_url(&tested_http_url, handles).unwrap());
}
}

View File

@@ -113,6 +113,65 @@ fn banner_prints_headers() {
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple dont scan url & regex entries
fn banner_prints_denied_urls() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--dont-scan")
.arg("http://dont-scan.me")
.arg("https://also-not.me")
.arg("https:")
.arg("/deny.*")
.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("Don't Scan Url"))
.and(predicate::str::contains("Don't Scan Regex"))
.and(predicate::str::contains("http://dont-scan.me"))
.and(predicate::str::contains("https://also-not.me"))
.and(predicate::str::contains("https:"))
.and(predicate::str::contains("/deny.*"))
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple headers
fn banner_prints_random_agent() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg("http://localhost")
.arg("--random-agent")
.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("Random"))
.and(predicate::str::contains("─┴─")),
);
}
#[test]
/// test allows non-existent wordlist to trigger the banner printing to stderr
/// expect to see all mandatory prints + multiple size filters

282
tests/test_deny_list.rs Normal file
View File

@@ -0,0 +1,282 @@
mod utils;
use assert_cmd::prelude::*;
use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::MockServer;
use predicates::prelude::*;
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
/// test that the deny list prevents a request if the requested url is a match
fn deny_list_works_during_with_a_normal_scan() {
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("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--dont-scan")
.arg(srv.url("/LICENSE"))
.unwrap();
teardown_tmp_directory(tmp_dir);
cmd.assert()
.success()
.stdout(predicate::str::contains(srv.url("/LICENSE")).not());
assert_eq!(mock.hits(), 0);
}
#[test]
/// test that the deny list prevents requests of urls found during extraction
fn deny_list_works_during_extraction() {
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("/LICENSE");
then.status(200)
.body(&srv.url("'/homepage/assets/img/icons/handshake.svg'"));
});
let mock_two = srv.mock(|when, then| {
when.method(GET)
.path("/homepage/assets/img/icons/handshake.svg");
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--extract-links")
.arg("--dont-scan")
.arg(srv.url("/homepage/"))
.unwrap();
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE")
.and(predicate::str::contains("200"))
.and(predicate::str::contains("/homepage/assets/img/icons/handshake.svg").not()),
);
assert_eq!(mock.hits(), 1);
assert_eq!(mock_two.hits(), 0);
teardown_tmp_directory(tmp_dir);
}
#[test]
/// test that the deny list prevents requests of urls found during recursion
fn deny_list_works_during_recursion() {
let srv = MockServer::start();
let urls = [
"js".to_string(),
"prod".to_string(),
"dev".to_string(),
"file.js".to_string(),
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/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/"));
});
let js_dev_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev");
then.status(301).header("Location", &srv.url("/js/dev/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/file.js");
then.status(200)
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-t")
.arg("1")
.arg("--dont-scan")
.arg(srv.url("/js/dev"))
.unwrap();
cmd.assert().success().stdout(
predicate::str::is_match("301.*js")
.unwrap()
.and(predicate::str::is_match("301.*js/prod").unwrap())
.and(predicate::str::is_match("301.*js/dev").unwrap())
.not()
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
.not(),
);
assert_eq!(js_mock.hits(), 1);
assert_eq!(js_prod_mock.hits(), 1);
assert_eq!(js_dev_mock.hits(), 0);
assert_eq!(js_dev_file_mock.hits(), 0);
teardown_tmp_directory(tmp_dir);
}
#[test]
/// test that the deny list prevents requests of urls found during recursion when the denier is a
/// parent of a user-specified scan
fn deny_list_works_during_recursion_with_inverted_parents() {
let srv = MockServer::start();
let urls = [
"js".to_string(),
"prod".to_string(),
"dev".to_string(),
"api".to_string(),
"file.js".to_string(),
];
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist").unwrap();
let js_mock = srv.mock(|when, then| {
when.method(GET).path("/js");
then.status(301).header("Location", &srv.url("/js/"));
});
let api_mock = srv.mock(|when, then| {
when.method(GET).path("/api");
then.status(200);
});
let js_prod_mock = srv.mock(|when, then| {
when.method(GET).path("/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/"));
});
let js_dev_file_mock = srv.mock(|when, then| {
when.method(GET).path("/js/dev/file.js");
then.status(200)
.body("this is a test and is more bytes than other ones");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/js"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("-t")
.arg("1")
.arg("-vvvv")
.arg("--dont-scan")
.arg(srv.url("/"))
.unwrap();
cmd.assert().success().stdout(
predicate::str::is_match("301.*js")
.unwrap()
.and(predicate::str::is_match("301.*js/prod").unwrap())
.and(predicate::str::is_match("301.*js/dev").unwrap())
.and(predicate::str::is_match("200.*js/dev/file.js").unwrap())
.and(predicate::str::is_match("200.*api").unwrap())
.not(),
);
assert_eq!(js_mock.hits(), 1);
assert_eq!(js_prod_mock.hits(), 1);
assert_eq!(js_dev_mock.hits(), 1);
assert_eq!(js_dev_file_mock.hits(), 1);
assert_eq!(api_mock.hits(), 0);
teardown_tmp_directory(tmp_dir);
}
#[test]
/// test that a regex that prevents the base url from being scanned results in an early exit
fn deny_list_prevents_regex_that_denies_base_url() {
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("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--dont-scan")
.arg("/")
.unwrap();
teardown_tmp_directory(tmp_dir);
let err_msg = format!(
"Could not determine initial targets: The regex '/' matches {}/; the scan will never start",
srv.base_url()
);
cmd.assert()
.success()
.stderr(predicate::str::contains(err_msg));
assert_eq!(mock.hits(), 0);
}
#[test]
/// test that a url that prevents the base url from being scanned results in an early exit
fn deny_list_prevents_url_that_denies_base_url() {
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("/LICENSE");
then.status(200).body("this is a test");
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--dont-scan")
.arg(srv.base_url())
.unwrap();
teardown_tmp_directory(tmp_dir);
let err_msg = format!(
"Could not determine initial targets: The url '{}/' matches {}/; the scan will never start",
srv.base_url(),
srv.base_url()
);
cmd.assert()
.success()
.stderr(predicate::str::contains(err_msg));
assert_eq!(mock.hits(), 0);
}

View File

@@ -224,11 +224,11 @@ fn test_dynamic_wildcard_request_found() {
teardown_tmp_directory(tmp_dir);
assert_eq!(contents.contains("WLD"), true);
assert_eq!(contents.contains("Got"), true);
assert_eq!(contents.contains("200"), true);
assert_eq!(contents.contains("(url length: 32)"), true);
assert_eq!(contents.contains("(url length: 96)"), true);
assert!(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")
@@ -391,11 +391,11 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {
teardown_tmp_directory(tmp_dir);
assert_eq!(contents.contains("WLD"), true);
assert_eq!(contents.contains("Got"), true);
assert_eq!(contents.contains("200"), true);
assert_eq!(contents.contains("(url length: 32)"), true);
assert_eq!(contents.contains("(url length: 96)"), true);
assert!(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")
@@ -451,12 +451,12 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
teardown_tmp_directory(tmp_dir);
assert_eq!(contents.contains("WLD"), true);
assert_eq!(contents.contains("301"), true);
assert_eq!(contents.contains("/some-redirect"), true);
assert_eq!(contents.contains("redirects to => "), true);
assert_eq!(contents.contains(&srv.url("/")), true);
assert_eq!(contents.contains("(url length: 32)"), true);
assert!(contents.contains("WLD"));
assert!(contents.contains("301"));
assert!(contents.contains("/some-redirect"));
assert!(contents.contains("redirects to => "));
assert!(contents.contains(&srv.url("/")));
assert!(contents.contains("(url length: 32)"));
cmd.assert().success().stdout(
predicate::str::contains("redirects to => ")

View File

@@ -3,7 +3,7 @@ use assert_cmd::Command;
use httpmock::Method::GET;
use httpmock::{MockServer, Regex};
use predicates::prelude::*;
use std::fs::read_to_string;
use std::fs::{read_dir, read_to_string};
use utils::{setup_tmp_directory, teardown_tmp_directory};
#[test]
@@ -153,3 +153,73 @@ fn main_parallel_spawns_children() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[test]
/// send three targets over stdin with --output enabled, expect parallel to create a new directory
/// and the log files therein
fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Error>> {
let t1 = MockServer::start();
let t2 = MockServer::start();
let t3 = MockServer::start();
let words = [
String::from("LICENSE"),
String::from("stuff"),
String::from("things"),
String::from("mostuff"),
String::from("mothings"),
];
let (word_tmp_dir, wordlist) = setup_tmp_directory(&words, "wordlist")?;
let (output_dir, outfile) = setup_tmp_directory(&[], "output-file")?;
let (tgt_tmp_dir, targets) =
setup_tmp_directory(&[t1.url("/"), t2.url("/"), t3.url("/")], "targets")?;
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--stdin")
.arg("--parallel")
.arg("2")
.arg("--output")
.arg(outfile.as_os_str())
.arg("--wordlist")
.arg(wordlist.as_os_str())
.pipe_stdin(targets)
.unwrap()
.assert()
.success()
.stderr(
predicate::str::contains("Could not connect to any target provided")
.and(predicate::str::contains("Target Url"))
.not(), // no target url found
);
// output_dir should return something similar to output-file-1627845244.logs with the
// line below. if it ever fails, can use the regex below to filter out the right directory
let sub_dir = read_dir(&output_dir)?.next().unwrap()?.file_name();
let mut num_logs = 0;
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);
// created directory like output-file-1627845741.logs/
assert!(dir_regex.is_match(&sub_dir.to_string_lossy().to_string()));
for entry in sub_dir.read_dir()? {
let entry = entry?;
// created each file like ferox-https_localhost-1627845741.log
println!("name: {:?}", entry.file_name().to_string_lossy());
assert!(file_regex.is_match(&entry.file_name().to_string_lossy()));
num_logs += 1;
}
// should be 3 log files total
assert_eq!(num_logs, 3);
teardown_tmp_directory(word_tmp_dir);
teardown_tmp_directory(tgt_tmp_dir);
teardown_tmp_directory(output_dir);
Ok(())
}

46
tests/test_parser.rs Normal file
View File

@@ -0,0 +1,46 @@
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
/// specify an incorrect param (-fc) with --help after it on the command line
/// old behavior printed
/// error: Found argument '-c' which wasn't expected, or isn't valid in this context
///
/// USAGE:
/// feroxbuster --add-slash --url <URL>...
///
/// For more information try --help
///
/// the new behavior we expect to see is to print the long form help message, of which
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
fn parser_incorrect_param_with_tack_tack_help() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("-fc")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Ludicrous speed... go!"));
}
#[test]
/// specify an incorrect param (-fc) with --help after it on the command line
/// old behavior printed
/// error: Found argument '-c' which wasn't expected, or isn't valid in this context
///
/// USAGE:
/// feroxbuster --add-slash --url <URL>...
///
/// For more information try --help
///
/// the new behavior we expect to see is to print the long form help message, of which
/// Ludicrous speed... go! is near the bottom of that output, so we can test for that
fn parser_incorrect_param_with_tack_h() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("-fc")
.arg("-h")
.assert()
.success()
.stdout(predicate::str::contains("Ludicrous speed... go!"));
}

View File

@@ -77,7 +77,7 @@ fn auto_bail_cancels_scan_with_timeouts() {
// read debug log to get the number of errors enforced
for line in debug_log.lines() {
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
if let Some(message) = log.get("message") {
let str_msg = message.as_str().unwrap_or_default().to_string();
@@ -156,7 +156,7 @@ fn auto_bail_cancels_scan_with_403s() {
// read debug log to get the number of errors enforced
for line in debug_log.lines() {
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
if let Some(message) = log.get("message") {
let str_msg = message.as_str().unwrap_or_default().to_string();
@@ -238,7 +238,7 @@ fn auto_bail_cancels_scan_with_429s() {
// read debug log to get the number of errors enforced
for line in debug_log.lines() {
let log: serde_json::Value = serde_json::from_str(&line).unwrap_or_default();
let log: serde_json::Value = serde_json::from_str(line).unwrap_or_default();
if let Some(message) = log.get("message") {
let str_msg = message.as_str().unwrap_or_default().to_string();

View File

@@ -93,9 +93,13 @@ fn resume_scan_works() {
#[test]
/// kick off scan with a time limit;
fn time_limit_enforced_when_specified() {
let srv = MockServer::start();
let t1 = MockServer::start();
let t2 = MockServer::start();
let (tmp_dir, file) =
setup_tmp_directory(&["css".to_string(), "stuff".to_string()], "wordlist").unwrap();
let (tgt_tmp_dir, targets) =
setup_tmp_directory(&[t1.url("/"), t2.url("/")], "targets").unwrap();
// ensure the command will run long enough by adding crap to the wordlist
let more_words = read_to_string(Path::new("tests/extra-words")).unwrap();
@@ -109,12 +113,13 @@ fn time_limit_enforced_when_specified() {
Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--stdin")
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--time-limit")
.arg("5s")
.pipe_stdin(targets)
.unwrap()
.assert()
.failure();
@@ -127,4 +132,5 @@ fn time_limit_enforced_when_specified() {
assert!(now.elapsed() > lower_bound && now.elapsed() < upper_bound);
teardown_tmp_directory(tmp_dir);
teardown_tmp_directory(tgt_tmp_dir);
}