Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
maurosoria
GitHub Repository: maurosoria/dirsearch
Path: blob/master/lib/core/fuzzer.py
896 views
1
# -*- coding: utf-8 -*-
2
# This program is free software; you can redistribute it and/or modify
3
# it under the terms of the GNU General Public License as published by
4
# the Free Software Foundation; either version 2 of the License, or
5
# (at your option) any later version.
6
#
7
# This program is distributed in the hope that it will be useful,
8
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
# GNU General Public License for more details.
11
#
12
# You should have received a copy of the GNU General Public License
13
# along with this program; if not, write to the Free Software
14
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15
# MA 02110-1301, USA.
16
#
17
# Author: Mauro Soria
18
19
from __future__ import annotations
20
21
import asyncio
22
import re
23
import threading
24
import time
25
from typing import Any, Callable, Generator
26
27
from lib.connection.requester import AsyncRequester, BaseRequester, Requester
28
from lib.connection.response import BaseResponse
29
from lib.core.data import blacklists, options
30
from lib.core.dictionary import Dictionary
31
from lib.core.exceptions import RequestException
32
from lib.core.logger import logger
33
from lib.core.scanner import AsyncScanner, BaseScanner, Scanner
34
from lib.core.settings import (
35
DEFAULT_TEST_PREFIXES,
36
DEFAULT_TEST_SUFFIXES,
37
WILDCARD_TEST_POINT_MARKER,
38
)
39
from lib.parse.url import clean_path
40
from lib.utils.common import get_readable_size, lstrip_once
41
42
43
class BaseFuzzer:
44
def __init__(
45
self,
46
requester: BaseRequester,
47
dictionary: Dictionary,
48
*,
49
match_callbacks: tuple[Callable[[BaseResponse], Any], ...],
50
not_found_callbacks: tuple[Callable[[BaseResponse], Any], ...],
51
error_callbacks: tuple[Callable[[RequestException], Any], ...],
52
) -> None:
53
self._requester = requester
54
self._dictionary = dictionary
55
self._base_path: str = ""
56
self._hashes: dict = {}
57
self.match_callbacks = match_callbacks
58
self.not_found_callbacks = not_found_callbacks
59
self.error_callbacks = error_callbacks
60
61
self.scanners: dict[str, dict[str, Scanner]] = {
62
"default": {},
63
"prefixes": {},
64
"suffixes": {},
65
}
66
67
def set_base_path(self, path: str) -> None:
68
self._base_path = path
69
70
def get_scanners_for(self, path: str) -> Generator[BaseScanner, None, None]:
71
# Clean the path, so can check for extensions/suffixes
72
path = clean_path(path)
73
74
for prefix in self.scanners["prefixes"]:
75
if path.startswith(prefix):
76
yield self.scanners["prefixes"][prefix]
77
78
for suffix in self.scanners["suffixes"]:
79
if path.endswith(suffix):
80
yield self.scanners["suffixes"][suffix]
81
82
for scanner in self.scanners["default"].values():
83
yield scanner
84
85
def is_excluded(self, resp: BaseResponse) -> bool:
86
"""Validate the response by different filters"""
87
88
if resp.status in options["exclude_status_codes"]:
89
return True
90
91
if (
92
options["include_status_codes"]
93
and resp.status not in options["include_status_codes"]
94
):
95
return True
96
97
if (
98
resp.status in blacklists
99
and any(
100
resp.path.endswith(lstrip_once(suffix, "/"))
101
for suffix in blacklists.get(resp.status)
102
)
103
):
104
return True
105
106
if get_readable_size(resp.length).rstrip() in options["exclude_sizes"]:
107
return True
108
109
if resp.length < options["minimum_response_size"]:
110
return True
111
112
if resp.length > options["maximum_response_size"] > 0:
113
return True
114
115
if any(text in resp.content for text in options["exclude_texts"]):
116
return True
117
118
if options["exclude_regex"] and re.search(options["exclude_regex"], resp.content):
119
return True
120
121
if (
122
options["exclude_redirect"]
123
and (
124
options["exclude_redirect"] in resp.redirect
125
or re.search(options["exclude_redirect"], resp.redirect)
126
)
127
):
128
return True
129
130
if (
131
options["filter_threshold"]
132
and self._hashes.get(hash(resp), 0) >= options["filter_threshold"]
133
):
134
return True
135
136
return False
137
138
139
class Fuzzer(BaseFuzzer):
140
def __init__(
141
self,
142
requester: Requester,
143
dictionary: Dictionary,
144
*,
145
match_callbacks: tuple[Callable[[BaseResponse], Any], ...],
146
not_found_callbacks: tuple[Callable[[BaseResponse], Any], ...],
147
error_callbacks: tuple[Callable[[RequestException], Any], ...],
148
) -> None:
149
super().__init__(
150
requester,
151
dictionary,
152
match_callbacks=match_callbacks,
153
not_found_callbacks=not_found_callbacks,
154
error_callbacks=error_callbacks,
155
)
156
self._exc: Exception | None = None
157
self._threads = []
158
self._play_event = threading.Event()
159
self._quit_event = threading.Event()
160
self._pause_semaphore = threading.Semaphore(0)
161
162
def setup_scanners(self) -> None:
163
# Default scanners (wildcard testers)
164
self.scanners["default"]["random"] = Scanner(
165
self._requester, path=self._base_path + WILDCARD_TEST_POINT_MARKER
166
)
167
168
if options["exclude_response"]:
169
self.scanners["default"]["custom"] = Scanner(
170
self._requester, tested=self.scanners, path=options["exclude_response"]
171
)
172
173
for prefix in set(options["prefixes"] + DEFAULT_TEST_PREFIXES):
174
self.scanners["prefixes"][prefix] = Scanner(
175
self._requester,
176
tested=self.scanners,
177
path=f"{self._base_path}{prefix}{WILDCARD_TEST_POINT_MARKER}",
178
context=f"/{self._base_path}{prefix}***",
179
)
180
181
for suffix in set(options["suffixes"] + DEFAULT_TEST_SUFFIXES):
182
self.scanners["suffixes"][suffix] = Scanner(
183
self._requester,
184
tested=self.scanners,
185
path=f"{self._base_path}{WILDCARD_TEST_POINT_MARKER}{suffix}",
186
context=f"/{self._base_path}***{suffix}",
187
)
188
189
for extension in options["extensions"]:
190
if "." + extension not in self.scanners["suffixes"]:
191
self.scanners["suffixes"]["." + extension] = Scanner(
192
self._requester,
193
tested=self.scanners,
194
path=f"{self._base_path}{WILDCARD_TEST_POINT_MARKER}.{extension}",
195
context=f"/{self._base_path}***.{extension}",
196
)
197
198
def setup_threads(self) -> None:
199
if self._threads:
200
self._threads = []
201
202
for _ in range(options["thread_count"]):
203
new_thread = threading.Thread(target=self.thread_proc)
204
new_thread.daemon = True
205
self._threads.append(new_thread)
206
207
def start(self) -> None:
208
self.setup_scanners()
209
self.setup_threads()
210
self.play()
211
self._quit_event.clear()
212
213
for thread in self._threads:
214
thread.start()
215
216
def is_finished(self) -> bool:
217
if self._exc:
218
raise self._exc
219
220
for thread in self._threads:
221
if thread.is_alive():
222
return False
223
224
return True
225
226
def play(self) -> None:
227
self._play_event.set()
228
229
def pause(self) -> bool:
230
"""Pause all threads and wait for them to acknowledge.
231
232
Returns True if all threads paused successfully, False if timeout occurred.
233
"""
234
self._play_event.clear()
235
# Wait for all threads to stop (with timeout to avoid deadlock)
236
for thread in self._threads:
237
if thread.is_alive():
238
# Use timeout to prevent deadlock when threads are blocked on I/O
239
if not self._pause_semaphore.acquire(timeout=2):
240
return False
241
return True
242
243
def quit(self) -> None:
244
self._quit_event.set()
245
self.play()
246
247
def scan(self, path: str) -> None:
248
scanners = self.get_scanners_for(path)
249
try:
250
response = self._requester.request(path)
251
except RequestException as e:
252
for callback in self.error_callbacks:
253
callback(e)
254
return
255
256
if self.is_excluded(response):
257
for callback in self.not_found_callbacks:
258
callback(response)
259
return
260
261
for tester in scanners:
262
# Check if the response is unique, not wildcard
263
if not tester.check(path, response):
264
for callback in self.not_found_callbacks:
265
callback(response)
266
return
267
268
if options["filter_threshold"]:
269
hash_ = hash(response)
270
self._hashes.setdefault(hash_, 0)
271
self._hashes[hash_] += 1
272
273
for callback in self.match_callbacks:
274
callback(response)
275
276
def thread_proc(self) -> None:
277
logger.info(f'THREAD-{threading.get_ident()} started"')
278
279
while True:
280
try:
281
path = next(self._dictionary)
282
self.scan(self._base_path + path)
283
284
except StopIteration:
285
break
286
287
except Exception as e:
288
self._exc = e
289
290
finally:
291
time.sleep(options["delay"])
292
293
if not self._play_event.is_set():
294
logger.info(f'THREAD-{threading.get_ident()} paused"')
295
self._pause_semaphore.release()
296
self._play_event.wait()
297
logger.info(f'THREAD-{threading.get_ident()} continued"')
298
299
if self._quit_event.is_set():
300
break
301
302
303
class AsyncFuzzer(BaseFuzzer):
304
def __init__(
305
self,
306
requester: AsyncRequester,
307
dictionary: Dictionary,
308
*,
309
match_callbacks: tuple[Callable[[BaseResponse], Any], ...],
310
not_found_callbacks: tuple[Callable[[BaseResponse], Any], ...],
311
error_callbacks: tuple[Callable[[RequestException], Any], ...],
312
) -> None:
313
super().__init__(
314
requester,
315
dictionary,
316
match_callbacks=match_callbacks,
317
not_found_callbacks=not_found_callbacks,
318
error_callbacks=error_callbacks,
319
)
320
self._play_event = asyncio.Event()
321
self._background_tasks = set()
322
323
async def setup_scanners(self) -> None:
324
# Default scanners (wildcard testers)
325
self.scanners["default"].update(
326
{
327
"index": await AsyncScanner.create(
328
self._requester, path=self._base_path
329
),
330
"random": await AsyncScanner.create(
331
self._requester, path=self._base_path + WILDCARD_TEST_POINT_MARKER
332
),
333
}
334
)
335
336
if options["exclude_response"]:
337
self.scanners["default"]["custom"] = await AsyncScanner.create(
338
self._requester, tested=self.scanners, path=options["exclude_response"]
339
)
340
341
for prefix in options["prefixes"] + DEFAULT_TEST_PREFIXES:
342
self.scanners["prefixes"][prefix] = await AsyncScanner.create(
343
self._requester,
344
tested=self.scanners,
345
path=f"{self._base_path}{prefix}{WILDCARD_TEST_POINT_MARKER}",
346
context=f"/{self._base_path}{prefix}***",
347
)
348
349
for suffix in options["suffixes"] + DEFAULT_TEST_SUFFIXES:
350
self.scanners["suffixes"][suffix] = await AsyncScanner.create(
351
self._requester,
352
tested=self.scanners,
353
path=f"{self._base_path}{WILDCARD_TEST_POINT_MARKER}{suffix}",
354
context=f"/{self._base_path}***{suffix}",
355
)
356
357
for extension in options["extensions"]:
358
if "." + extension not in self.scanners["suffixes"]:
359
self.scanners["suffixes"]["." + extension] = await AsyncScanner.create(
360
self._requester,
361
tested=self.scanners,
362
path=f"{self._base_path}{WILDCARD_TEST_POINT_MARKER}.{extension}",
363
context=f"/{self._base_path}***.{extension}",
364
)
365
366
async def start(self) -> None:
367
# In Python 3.9, initialize the Semaphore within the coroutine
368
# to avoid binding to a different event loop.
369
self.sem = asyncio.Semaphore(options["thread_count"])
370
await self.setup_scanners()
371
self.play()
372
373
for _ in range(len(self._dictionary)):
374
task = asyncio.create_task(self.task_proc())
375
self._background_tasks.add(task)
376
task.add_done_callback(self._background_tasks.discard)
377
378
await asyncio.gather(*self._background_tasks)
379
380
def play(self) -> None:
381
self._play_event.set()
382
383
def pause(self) -> None:
384
self._play_event.clear()
385
386
def quit(self) -> None:
387
for task in self._background_tasks:
388
task.cancel()
389
390
async def scan(self, path: str) -> None:
391
scanners = self.get_scanners_for(path)
392
try:
393
response = await self._requester.request(path)
394
except RequestException as e:
395
for callback in self.error_callbacks:
396
callback(e)
397
return
398
399
if self.is_excluded(response):
400
for callback in self.not_found_callbacks:
401
callback(response)
402
return
403
404
for tester in scanners:
405
# Check if the response is unique, not wildcard
406
if not tester.check(path, response):
407
for callback in self.not_found_callbacks:
408
callback(response)
409
return
410
411
if options["filter_threshold"]:
412
hash_ = hash(response)
413
self._hashes.setdefault(hash_, 0)
414
self._hashes[hash_] += 1
415
416
for callback in self.match_callbacks:
417
callback(response)
418
419
async def task_proc(self) -> None:
420
async with self.sem:
421
await self._play_event.wait()
422
423
try:
424
path = next(self._dictionary)
425
await self.scan(self._base_path + path)
426
except StopIteration:
427
pass
428
finally:
429
await asyncio.sleep(options["delay"])
430
431