diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index b113beba..c0dd5da1 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: Pypi Uploader +name: delivery on: release: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fc30fb2..c2f4754c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ -name: Integration +name: integration on: push jobs: - integration: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -13,7 +13,7 @@ jobs: - name: Linter run: flake8 src/fuzzingtool --extend-ignore=E501,E731 --per-file-ignores="__init__.py:F401,F403,W292" --statistics - - name: Build + - name: Package run: | python3 setup.py sdist cd dist diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 8a317f11..c110ed0c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,4 +1,4 @@ -name: SonarCloud +name: code quality on: push: diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index 1f25a592..fb78771d 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -1,13 +1,9 @@ -name: Bandit-SAST +name: sast -on: - pull_request: - branches: - - develop - - master +on: push jobs: - bandit-sast: + bandit: runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/.github/workflows/sca.yml b/.github/workflows/sca.yml index 17c32065..fe46e19b 100644 --- a/.github/workflows/sca.yml +++ b/.github/workflows/sca.yml @@ -1,13 +1,9 @@ -name: Snyk +name: sca -on: - pull_request: - branches: - - develop - - master +on: push jobs: - security: + snyk: runs-on: ubuntu-latest steps: - uses: actions/checkout@master diff --git a/README.md b/README.md index e2adaa50..0df61459 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ FuzzingTool is a web penetration testing tool, that handles with fuzzing. After We're not responsible for the misuse of this tool. This project was created for educational purposes and should not be used in environments without legal authorization. ## Screenshot -![usage-screenshot](https://user-images.githubusercontent.com/43549176/149956432-7f3912df-59a1-416a-94a6-276df7357ec2.png) +![screenshot](https://user-images.githubusercontent.com/43549176/166966746-b4e8f130-eeb7-4ba4-a7b0-b385a81bb16e.png) ## Getting Started Before we start the *penetration testings*, take a look at the **installing** and **prerequisites**. diff --git a/requirements.txt b/requirements.txt index fa607c12..3f574577 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,5 @@ soupsieve>=2.2.1 # via beautifulsoup4 urllib3>=1.26.5 # via requests +python-Wappalyzer>=0.3.1 + # via FuzzingTool (setup.py) diff --git a/setup.py b/setup.py index 2d1035d0..c329a731 100644 --- a/setup.py +++ b/setup.py @@ -12,10 +12,12 @@ def read(fname): 'requests>=2.25.1', 'beautifulsoup4>=4.9.3', 'dnspython>=2.1.0', + 'python-Wappalyzer>=0.3.1', ] dev_requires = [ - 'pytest' + 'pytest', + 'pytest-cov', ] setup( @@ -33,7 +35,7 @@ def read(fname): package_dir={'fuzzingtool': 'src/fuzzingtool'}, entry_points={ 'console_scripts': [ - 'FuzzingTool = fuzzingtool.fuzzingtool:main_cli' + 'fuzzingtool = fuzzingtool.fuzzingtool:main_cli' ] }, install_requires=install_requires, diff --git a/src/FuzzingTool.py b/src/fuzzingtool.py similarity index 100% rename from src/FuzzingTool.py rename to src/fuzzingtool.py diff --git a/src/fuzzingtool/__init__.py b/src/fuzzingtool/__init__.py index da7818da..dc826e4b 100644 --- a/src/fuzzingtool/__init__.py +++ b/src/fuzzingtool/__init__.py @@ -20,21 +20,13 @@ APP_VERSION = { 'MAJOR_VERSION': 3, - "MINOR_VERSION": 13, + "MINOR_VERSION": 14, "PATCH": 0 } -def version(): - global APP_VERSION - version = (str(APP_VERSION['MAJOR_VERSION']) - + "." + str(APP_VERSION['MINOR_VERSION']) - + "." + str(APP_VERSION['PATCH'])) - return version - - __name__ = "FuzzingTool" -__version__ = version() +__version__ = '.'.join([str(value) for value in APP_VERSION.values()]) __author__ = "Vitor Oriel C N Borges" __license__ = "MIT" __copyright__ = "Copyright 2020 - present Vitor Oriel" diff --git a/src/fuzzingtool/api/api.py b/src/fuzzingtool/api.py similarity index 85% rename from src/fuzzingtool/api/api.py rename to src/fuzzingtool/api.py index 5aa1cb93..338fa6dd 100644 --- a/src/fuzzingtool/api/api.py +++ b/src/fuzzingtool/api.py @@ -18,16 +18,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .fuzz_controller import FuzzController -from ..interfaces.cli.cli_arguments import CliArguments +from .fuzz_lib import FuzzLib +from .interfaces.cli.cli_arguments import CliArguments def fuzz(**kwargs) -> None: - FuzzController(**kwargs).main() + FuzzLib(**kwargs).main() def fuzz_cli(args: str, **kwargs) -> None: - args = ['FuzzingTool'] + args.split(' ') + args = ['fuzzingtool'] + args.split(' ') args = vars(CliArguments(args).get_arguments()) args.update(kwargs) - FuzzController(**args).main() + FuzzLib(**args).main() diff --git a/src/fuzzingtool/conn/requesters/requester.py b/src/fuzzingtool/conn/requesters/requester.py index 47bbba94..def80974 100644 --- a/src/fuzzingtool/conn/requesters/requester.py +++ b/src/fuzzingtool/conn/requesters/requester.py @@ -28,9 +28,8 @@ from ..request_parser import (check_is_url_discovery, check_is_data_fuzzing, request_parser) -from ...utils.consts import (UNKNOWN_FUZZING, HTTP_METHOD_FUZZING, - PATH_FUZZING, SUBDOMAIN_FUZZING, DATA_FUZZING) -from ...utils.http_utils import get_pure_url, get_host, get_url_without_scheme +from ...utils.consts import FuzzType +from ...utils.http_utils import get_parsed_url, get_pure_url, get_url_without_scheme from ...objects.fuzz_word import FuzzWord from ...exceptions.request_exceptions import RequestException @@ -53,15 +52,15 @@ class Requester: def __init__(self, url: str, method: str = 'GET', - methods: List[str] = None, - body: str = '', + body: str = None, headers: Dict[str, str] = None, follow_redirects: bool = True, - proxy: str = '', + proxy: str = None, proxies: List[str] = None, timeout: int = 0, - cookie: str = '', - is_session: bool = False): + cookie: str = None, + is_session: bool = False, + replay_proxy: str = None): """Class constructor @type url: str @@ -86,6 +85,8 @@ def __init__(self, @param cookie: The cookie HTTP header value @type is_session: bool @param is_session: The flag to say if the requests will be made as session request + @type replay_proxy: str + @param replay_proxy: The proxy for replay request on matched responses """ self._url, url_params = self.__setup_url(url) self.__url_params = self.__build_data_dict(url_params) @@ -106,9 +107,9 @@ def __init__(self, if is_session or self.is_path_fuzzing(): self.__session = requests.Session() self._request = self.__session_request - self.methods = methods if methods else [self.__method.word] if cookie: self.__header['Cookie'] = FuzzWord(cookie) + self.__replay_proxy = self.__setup_proxy(replay_proxy) if replay_proxy else {} self._lock = Lock() def get_url(self) -> str: @@ -118,33 +119,40 @@ def get_url(self) -> str: """ return self._url.word + def get_method(self) -> str: + """The request method content getter + + @returns str: The request method + """ + return self.__method.word + def is_method_fuzzing(self) -> bool: """The method fuzzing flag getter @returns bool: The method fuzzing flag """ - return self.__fuzzing_type == HTTP_METHOD_FUZZING + return self.__fuzzing_type == FuzzType.HTTP_METHOD_FUZZING def is_data_fuzzing(self) -> bool: """The data fuzzing flag getter @returns bool: The data fuzzing flag """ - return self.__fuzzing_type == DATA_FUZZING + return self.__fuzzing_type == FuzzType.DATA_FUZZING def is_url_discovery(self) -> bool: """Checks if the fuzzing is for url discovery (path or subdomain) @returns bool: A flag to say if is url discovery fuzzing type """ - return self.__fuzzing_type == PATH_FUZZING or self.__fuzzing_type == SUBDOMAIN_FUZZING + return self.__fuzzing_type == FuzzType.PATH_FUZZING or self.__fuzzing_type == FuzzType.SUBDOMAIN_FUZZING def is_path_fuzzing(self) -> bool: """Checks if the fuzzing will be path discovery @returns bool: A flag to say if is path fuzzing """ - return self.__fuzzing_type == PATH_FUZZING + return self.__fuzzing_type == FuzzType.PATH_FUZZING def get_fuzzing_type(self) -> int: """The fuzzing type getter @@ -172,7 +180,7 @@ def set_body(self, body: str) -> None: def test_connection(self) -> None: """Test the connection with the target, and raise an exception if couldn't connect""" try: - url = get_pure_url(self._url.word) + url = get_pure_url(self.get_url()) requests.get( url, proxies=self.__proxy, @@ -197,22 +205,30 @@ def test_connection(self) -> None: ): raise RequestException(f"Failed to establish a connection to {url}") - def request(self, payload: str = '') -> Tuple[requests.Response, float]: + def request(self, + payload: str = '', + replay_proxy: bool = False) -> Tuple[requests.Response, float]: """Make a request and get the response @type payload: str @param payload: The payload used in the request + @type replay_proxy: bool + @param replay_proxy: The replay proxy flag @returns Tuple[Response, float]: The response object of the request """ - if self.__proxies: - self.__proxy = random.choice(self.__proxies) + if not replay_proxy: + proxy = self.__proxy + if self.__proxies: + proxy = random.choice(self.__proxies) + else: + proxy = self.__replay_proxy method, url, body, url_params, headers = self.__get_request_parameters(payload) try: before = time.time() - response = self._request(method, url, body, url_params, headers) + response = self._request(method, url, body, url_params, headers, proxy) rtt = (time.time() - before) except requests.exceptions.ProxyError: - raise RequestException("Can't connect to the proxy") + raise RequestException(f"Can't connect to the proxy {get_url_without_scheme(proxy['http'])}") except requests.exceptions.TooManyRedirects: raise RequestException(f"Too many redirects on {url}") except requests.exceptions.SSLError: @@ -232,7 +248,7 @@ def request(self, payload: str = '') -> Tuple[requests.Response, float]: UnicodeError, urllib3.exceptions.LocationParseError ): - raise RequestException(f"Invalid hostname {get_host(url)} for HTTP request") + raise RequestException(f"Invalid hostname {get_parsed_url(url).hostname} for HTTP request") except ValueError as e: raise RequestException(str(e)) else: @@ -243,19 +259,22 @@ def _request(self, url: str, body: dict, url_params: dict, - headers: dict) -> requests.Response: + headers: dict, + proxy: dict) -> requests.Response: """Performs a request to the target @type method: str @param method: The request method @type url: str @param url: The target URL - @type headers: dict - @param headers: The http header of the request @type body: dict @param body: The body data to be send with the request @type url_params: dict @param url_params: The URL params to be send with the request + @type headers: dict + @param headers: The http header of the request + @type proxy: str + @param proxy: The proxy used in the request @returns Response: The response object of the request """ return requests.request( @@ -264,7 +283,7 @@ def _request(self, data=body, params=url_params, headers=headers, - proxies=self.__proxy, + proxies=proxy, timeout=self.__timeout, allow_redirects=self.__follow_redirects, ) @@ -275,12 +294,12 @@ def _set_fuzzing_type(self) -> int: @returns int: The fuzzing type int value """ if self.__method.has_fuzzing: - return HTTP_METHOD_FUZZING + return FuzzType.HTTP_METHOD_FUZZING if check_is_url_discovery(self._url): - return PATH_FUZZING + return FuzzType.PATH_FUZZING if check_is_data_fuzzing(self.__url_params, self.__body, self.__header): - return DATA_FUZZING - return UNKNOWN_FUZZING + return FuzzType.DATA_FUZZING + return FuzzType.UNKNOWN_FUZZING def __setup_url(self, url: str) -> Tuple[FuzzWord, str]: """The URL setup @@ -370,19 +389,22 @@ def __session_request(self, url: str, body: dict, url_params: dict, - headers: dict) -> requests.Response: + headers: dict, + proxy: dict) -> requests.Response: """Performs a request to the target using Session object @type method: str @param method: The request method @type url: str @param url: The target URL - @type headers: dict - @param headers: The http header of the request @type body: dict @param body: The body data to be send with the request @type url_params: dict @param url_params: The URL params to be send with the request + @type headers: dict + @param headers: The http header of the request + @type proxy: str + @param proxy: The proxy used in the request @returns Response: The response object of the request """ return self.__session.send( @@ -393,7 +415,7 @@ def __session_request(self, params=url_params, headers=headers, )), - proxies=self.__proxy, + proxies=proxy, timeout=self.__timeout, allow_redirects=self.__follow_redirects, ) diff --git a/src/fuzzingtool/conn/requesters/subdomain_requester.py b/src/fuzzingtool/conn/requesters/subdomain_requester.py index 9b6dbf4d..6632f1c6 100644 --- a/src/fuzzingtool/conn/requesters/subdomain_requester.py +++ b/src/fuzzingtool/conn/requesters/subdomain_requester.py @@ -25,8 +25,8 @@ from .requester import Requester from ..request_parser import request_parser -from ...utils.http_utils import get_host -from ...utils.consts import SUBDOMAIN_FUZZING +from ...utils.http_utils import get_parsed_url +from ...utils.consts import FuzzType from ...exceptions.request_exceptions import InvalidHostname @@ -44,12 +44,14 @@ def resolve_hostname(self, hostname: str) -> str: except socket.gaierror: raise InvalidHostname(f"Can't resolve hostname {hostname}") - def request(self, payload: str = '') -> Tuple[Response, float, Dict[str, str]]: + def request(self, + payload: str = '', + replay_proxy: bool = False) -> Tuple[Response, float, Dict[str, str]]: with self._lock: request_parser.set_payload(payload) - host = get_host(request_parser.get_url(self._url)) + host = get_parsed_url(request_parser.get_url(self._url)).hostname ip = self.resolve_hostname(host) - return (*(super().request(payload)), {'ip': ip}) + return (*(super().request(payload, replay_proxy)), ip) def _set_fuzzing_type(self) -> int: - return SUBDOMAIN_FUZZING + return FuzzType.SUBDOMAIN_FUZZING diff --git a/src/fuzzingtool/core/__init__.py b/src/fuzzingtool/core/__init__.py index bce70a1b..efbd0f0e 100644 --- a/src/fuzzingtool/core/__init__.py +++ b/src/fuzzingtool/core/__init__.py @@ -22,7 +22,10 @@ from .defaults import * from .blacklist_status import BlacklistStatus from .dictionary import Dictionary +from .filter import Filter from .fuzzer import Fuzzer +from .job_manager import JobManager from .matcher import Matcher from .payloader import Payloader +from .recursion_manager import RecursionManager from .summary import Summary diff --git a/src/fuzzingtool/decorators/append_args.py b/src/fuzzingtool/core/bases/base_observer.py similarity index 60% rename from src/fuzzingtool/decorators/append_args.py rename to src/fuzzingtool/core/bases/base_observer.py index 09d0ccab..3e9026f1 100644 --- a/src/fuzzingtool/decorators/append_args.py +++ b/src/fuzzingtool/core/bases/base_observer.py @@ -18,30 +18,20 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import Callable +from abc import ABC, abstractmethod -from ..core.bases.base_scanner import BaseScanner -from ..objects.result import Result +from ...objects.result import Result -def append_args(function: Callable) -> Callable: - """Decorator to append extra data from arguments to the result +class BaseObserver(ABC): + """Base class for the observers""" + @abstractmethod + def update(self, subject_name: str, result: Result) -> None: + """Update the Observer stats - @type function: Callable - @param function: The function to inspect the result - """ - def wrapper(cls: BaseScanner, - result: Result, - *args) -> Callable[[BaseScanner, Result], None]: - """Wrapper function to the decorator - - @type cls: BaseScanner - @param cls: The scanner that are using this decorator function + @type subject_name: str + @param subject_name: The provider name @type result: Result - @param result: The result object + @param result: The FuzzingTool result object """ - if args: - result.custom.update(args[0]) - return function(cls, result) - - return wrapper + pass diff --git a/src/fuzzingtool/core/bases/base_scanner.py b/src/fuzzingtool/core/bases/base_scanner.py index 04a23759..4610ac74 100644 --- a/src/fuzzingtool/core/bases/base_scanner.py +++ b/src/fuzzingtool/core/bases/base_scanner.py @@ -18,21 +18,33 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from abc import ABC, abstractmethod +from abc import abstractmethod +from queue import Queue -from ...objects.result import Result +from .job_provider import JobProvider +from ...objects import Payload, Result, ScannerResult -class BaseScanner(ABC): - """Base scanner""" - @abstractmethod - def inspect_result(self, result: Result) -> None: - """Inspects the FuzingTool result to add new information if needed +class BaseScanner(JobProvider): + """Base scanner (ABC) + + Attributes: + payloads_queue: The payload queue for new requests + """ + def __init__(self): + self.payloads_queue = Queue() + super().__init__() + + def __str__(self) -> str: + return type(self).__name__ + + def notify(self, result: Result) -> None: + """Notify the observer with the new job @type result: Result - @param result: The result object + @param result: The FuzzingTool result object """ - pass + self._observer.update(str(self), result) @abstractmethod def scan(self, result: Result) -> bool: @@ -43,3 +55,45 @@ def scan(self, result: Result) -> bool: @reeturns bool: A match flag """ pass + + def process(self, result: Result) -> None: + """Process the FuzzingTool result from this base scanner. + Do not override this function. If you need so, override _process method instead + + @type result: Result + @param result: The result object + """ + scanner_name = str(self) + result.scanners_res[scanner_name] = ScannerResult(scanner_name) + self._process(result) + + def get_self_res(self, result: Result) -> ScannerResult: + """Get the self Scanner result + + @type result: Result + @param result: The FuzzingTool result object + @returns ScannerResult: The self scanner result object + """ + return result.scanners_res[str(self)] + + def enqueue_payload(self, result: Result, payload: str) -> None: + """Enqueue a payload into the payload queue for the next job + + @type result: Result + @param result: The result of the payload + @type payload: str + @param payload: The payload that'll be enqueued + """ + was_empty = self.payloads_queue.empty() + self.payloads_queue.put(Payload().update(result._payload).with_recursion(payload)) + self.get_self_res(result).enqueued_payloads += 1 + if was_empty: + self.notify(result) + + def _process(self, result: Result) -> None: + """Process the FuzzingTool result through child scanner if needed + + @type result: Result + @param result: The result object + """ + pass diff --git a/src/fuzzingtool/core/bases/base_validator.py b/src/fuzzingtool/core/bases/base_validator.py new file mode 100644 index 00000000..a4458c75 --- /dev/null +++ b/src/fuzzingtool/core/bases/base_validator.py @@ -0,0 +1,53 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from abc import ABC +import re +from typing import Pattern + +from ...exceptions import BadArgumentFormat + + +class BaseValidator(ABC): + """Base validator class for both Matcher and Filter + + Attributes: + regexer: The regex object + """ + def __init__(self, regex: str): + """Class constructor + + @type regex: str + @param regex: The regular expression to be setted + """ + self._regexer = None if not regex else self.__build_regexer(regex) + + def __build_regexer(self, regex: str) -> Pattern[str]: + """Build the regexer object + + @type regex: str + @param regex: The regular expression to be setted + @returns Pattern[str]: The regex object + """ + try: + regexer = re.compile(regex, re.IGNORECASE) + except re.error: + raise BadArgumentFormat(f"Invalid regex format {regex} on {type(self).__name__}") + return regexer diff --git a/src/fuzzingtool/core/bases/job_provider.py b/src/fuzzingtool/core/bases/job_provider.py new file mode 100644 index 00000000..248ef2c0 --- /dev/null +++ b/src/fuzzingtool/core/bases/job_provider.py @@ -0,0 +1,46 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from abc import ABC, abstractmethod + +from .base_observer import BaseObserver + + +class JobProvider(ABC): + """Base class for the job providers + + Attributes: + observer: The observer that'll look for this job provider + """ + def __init__(self): + self._observer = None + + def set_observer(self, observer: BaseObserver) -> None: + """The observer setter + + @type observer: BaseObserver + @param observer: The observer that'll look for this job provider + """ + self._observer = observer + + @abstractmethod + def notify(self) -> None: + """Notify the observer for some action""" + pass diff --git a/src/fuzzingtool/core/blacklist_status.py b/src/fuzzingtool/core/blacklist_status.py index f4312a79..72f10a09 100644 --- a/src/fuzzingtool/core/blacklist_status.py +++ b/src/fuzzingtool/core/blacklist_status.py @@ -21,8 +21,7 @@ from typing import List, Dict, Callable from ..utils.utils import split_str_to_list -from ..exceptions.main_exceptions import (BadArgumentType, MissingParameter, - InvalidArgument) +from ..exceptions import BadArgumentType, MissingParameter, InvalidArgument class BlacklistStatus: diff --git a/src/fuzzingtool/core/defaults/scanners/data_scanner.py b/src/fuzzingtool/core/defaults/scanners/data_scanner.py index c2b096fe..ff470f18 100644 --- a/src/fuzzingtool/core/defaults/scanners/data_scanner.py +++ b/src/fuzzingtool/core/defaults/scanners/data_scanner.py @@ -25,8 +25,8 @@ class DataScanner(BaseScanner): __author__ = ("Vitor Oriel",) - def inspect_result(self, result: Result) -> None: - result.custom['payload_length'] = len(result.payload) - def scan(self, result: Result) -> bool: return True + + def _process(self, result: Result) -> None: + self.get_self_res(result).data['payload_length'] = len(result.payload) diff --git a/src/fuzzingtool/core/defaults/scanners/path_scanner.py b/src/fuzzingtool/core/defaults/scanners/path_scanner.py index 0eacd70b..876c8afc 100644 --- a/src/fuzzingtool/core/defaults/scanners/path_scanner.py +++ b/src/fuzzingtool/core/defaults/scanners/path_scanner.py @@ -25,11 +25,10 @@ class PathScanner(BaseScanner): __author__ = ("Vitor Oriel",) - def inspect_result(self, result: Result) -> None: - result.custom['redirected'] = '' - if result.status > 300 and result.status < 400: - result.custom['redirected'] = (result.get_response() - .headers['Location']) - def scan(self, result: Result) -> bool: return True + + def _process(self, result: Result) -> None: + self.get_self_res(result).data['redirected'] = '' + if result.history.status > 300 and result.history.status < 400: + self.get_self_res(result).data['redirected'] = result.history.response.headers['Location'] diff --git a/src/fuzzingtool/core/defaults/scanners/subdomain_scanner.py b/src/fuzzingtool/core/defaults/scanners/subdomain_scanner.py index 6f0b323c..711f6e4b 100644 --- a/src/fuzzingtool/core/defaults/scanners/subdomain_scanner.py +++ b/src/fuzzingtool/core/defaults/scanners/subdomain_scanner.py @@ -20,16 +20,10 @@ from ...bases.base_scanner import BaseScanner from ....objects.result import Result -from ....decorators.append_args import append_args class SubdomainScanner(BaseScanner): __author__ = ("Vitor Oriel",) - @append_args - def inspect_result(self, result: Result) -> None: - """The decorator append_args will deal with the IP custom result attribute.""" - pass - def scan(self, result: Result) -> bool: return True diff --git a/src/fuzzingtool/core/defaults/wordlists/file_wordlist.py b/src/fuzzingtool/core/defaults/wordlists/file_wordlist.py index c258341a..af5242e7 100644 --- a/src/fuzzingtool/core/defaults/wordlists/file_wordlist.py +++ b/src/fuzzingtool/core/defaults/wordlists/file_wordlist.py @@ -22,7 +22,7 @@ from ...bases.base_wordlist import BaseWordlist from ....utils.file_utils import read_file -from ....exceptions.main_exceptions import BuildWordlistFails +from ....exceptions import BuildWordlistFails class FileWordlist(BaseWordlist): diff --git a/src/fuzzingtool/core/dictionary.py b/src/fuzzingtool/core/dictionary.py index 882cfaec..e48d4bd2 100644 --- a/src/fuzzingtool/core/dictionary.py +++ b/src/fuzzingtool/core/dictionary.py @@ -22,6 +22,7 @@ from typing import List from .payloader import Payloader +from ..objects.payload import Payload class Dictionary: @@ -29,6 +30,7 @@ class Dictionary: Attributes: wordlist: The wordlist that contains the payloads backup + size: The payload queue size payloads: The queue that contains all payloads inside the wordlist """ def __init__(self, wordlist: list): @@ -37,14 +39,16 @@ def __init__(self, wordlist: list): @type wordlist: list @param wordlist: The wordlist with the payloads """ - self.__wordlist = wordlist + self.wordlist = wordlist + self.__size = 0 self.__payloads = Queue() - def __next__(self) -> List[str]: + def __next__(self) -> List[Payload]: """Gets the next payload to be processed @returns list: The payloads used in the request """ + self.__size -= 1 return Payloader.get_customized_payload(self.__payloads.get()) def __len__(self) -> int: @@ -61,7 +65,7 @@ def __len__(self) -> int: length_encoders = len(Payloader.encoder) if length_encoders == 0: length_encoders = 1 - return (len(self.__wordlist) + return (self.__size * length_suffix * length_prefix * length_encoders) @@ -75,6 +79,21 @@ def is_empty(self) -> bool: def reload(self) -> None: """Reloads the payloads queue with the wordlist content""" - self.__payloads = Queue() - for payload in self.__wordlist: + for payload in self.wordlist: self.__payloads.put(payload) + self.__size += 1 + + def fill_from_queue(self, payloads_queue: Queue, clear: bool = False) -> None: + """Fill the payloads queue with another queue + + @type payloads_queue: Queue + @param payloads_queue: The other payload quele that'll enqueue the payloads + @type clear: bool + @param clear: The flag to say if the payload queue needs to be cleared before + """ + if clear: + self.__payloads = Queue() + self.__size = 0 + while not payloads_queue.empty(): + self.__payloads.put(payloads_queue.get()) + self.__size += 1 diff --git a/src/fuzzingtool/core/filter.py b/src/fuzzingtool/core/filter.py new file mode 100644 index 00000000..72519286 --- /dev/null +++ b/src/fuzzingtool/core/filter.py @@ -0,0 +1,73 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from typing import List + +from .bases.base_validator import BaseValidator +from ..objects.result import Result +from ..exceptions import BadArgumentType + + +class Filter(BaseValidator): + """Class responsible to filter the results by exclusion rules + + Attributes: + status_codes: The list with excluded status codes + regexer: The regex object to exclude by regex + """ + def __init__(self, status_code: str = None, regex: str = None): + """Class constructor + + @type status_code: str + @param status_code: The status code string for filtering + @type regex: str + @param regex: The regular expression for filtering + """ + self._status_codes = [] if not status_code else self.__build_status_codes(status_code) + super().__init__(regex) + + def check(self, result: Result) -> bool: + """Checks if the filter configs matches with the result attributes + + @type result: Result + @param result: The FuzzingTool result object + @returns bool: A filter flag + """ + if result.history.status in self._status_codes: + return False + if (self._regexer is not None and + self._regexer.search(result.history.response.text)): + return False + return True + + def __build_status_codes(self, status_code: str) -> List[int]: + """Build the status codes list + + @type status_code: str + @param status_code: The status code string + @returns List[str]: The list with status codes as integers + """ + try: + status_codes = [int(status) for status in status_code.split(',')] + except ValueError: + raise BadArgumentType( + f"The filter status argument ({status_code}) must be integer" + ) + return status_codes diff --git a/src/fuzzingtool/core/fuzzer.py b/src/fuzzingtool/core/fuzzer.py index ab3131db..09763b93 100644 --- a/src/fuzzingtool/core/fuzzer.py +++ b/src/fuzzingtool/core/fuzzer.py @@ -20,17 +20,14 @@ from threading import Thread, Event import time -from typing import Callable, List +from typing import Callable, Union, Any from requests.models import Response -from .blacklist_status import BlacklistStatus from .dictionary import Dictionary -from .matcher import Matcher -from .bases.base_scanner import BaseScanner from ..conn.requesters import Requester -from ..objects import Error, Payload, Result -from ..exceptions.request_exceptions import RequestException, InvalidHostname +from ..objects import Error, Payload +from ..exceptions import RequestException, InvalidHostname class Fuzzer: @@ -39,55 +36,38 @@ class Fuzzer: Attributes: requester: The requester object to deal with the requests dict: The dictionary object to handle with the payloads - matcher: A matcher object, used to match the results - scanner: A scanner object, used to validate the results delay: The delay between each test running: A flag to say if the application is running or not - blacklist_status: The blacklist status object to handle - with the blacklisted status """ def __init__(self, requester: Requester, dictionary: Dictionary, - matcher: Matcher, - scanner: BaseScanner, delay: float, number_of_threads: int, - blacklist_status: BlacklistStatus, - result_callback: Callable[[dict, bool], None], - exception_callbacks: List[Callable[[str, str], None]]): + response_callback: Callable[[dict, bool], None], + exception_callbacks: Callable[[Response, float, Payload, Union[Any, None]], None]): """Class constructor @type requester: Requester @param requester: The requester object to deal with the requests @type dict: Dictionary @param dict: The dicttionary object to deal with the payload dictionary - @type matcher: Matcher - @param matcher: The matcher for the results - @type scanner: BaseScanner - @param scanner: The fuzzing results scanner @type delay: float @param delay: The delay between each request @type number_of_threads: int @param number_of_threads: The number of threads used in the fuzzing tests - @type blacklist_status: blacklist_status - @param blacklist_status: The blacklist status object - to handle with the blacklisted status - @type result_callback: Callable - @param result_callback: The callback function for the results + @type response_callback: Callable + @param response_callback: The callback function for the results @type exception_callbacks: List[Callable] @param exception_callbacks: The list that handles with exception callbacks """ self.__requester = requester self.__dict = dictionary - self.__matcher = matcher - self.__scanner = scanner self.__delay = delay self.__running = True - self.__blacklist_status = blacklist_status - self.result_callback = result_callback + self.response_callback = response_callback self.exception_callbacks = exception_callbacks self.setup_threads(number_of_threads) @@ -110,7 +90,6 @@ def setup_threads(self, number_of_threads: int) -> None: self.__paused_threads = 0 self.__join_timeout = 0.001*float(number_of_threads) self.__player = Event() - self.__player.clear() # Not necessary, but force the blocking of the threads def is_running(self) -> bool: """The running flag getter @@ -131,13 +110,12 @@ def run(self) -> None: while not self.__dict.is_empty(): for payload in next(self.__dict): try: - response, rtt, *args = self.__requester.request(str(payload)) + response, rtt, *ip = self.__requester.request(str(payload)) + self.response_callback(response, rtt, payload, *ip) except InvalidHostname as e: self.exception_callbacks[0](Error(e, payload)) except RequestException as e: self.exception_callbacks[1](Error(e, payload)) - else: - self.__threat_result(response, rtt, payload, *args) finally: time.sleep(self.__delay) if self.is_paused(): @@ -158,7 +136,7 @@ def join(self) -> bool: def start(self) -> None: """Starts the fuzzer application""" - self.__player.set() # Awake threads + self.__player.set() for thread in self.__threads: thread.start() @@ -178,33 +156,8 @@ def resume(self) -> None: self.__player.set() def wait_until_pause(self) -> None: - while self.__paused_threads < (self.__running_threads-1): - """Do nothing until all threads are paused""" + """Blocks until all threads are paused""" + while self.__paused_threads < self.__running_threads: + """Wait until all threads are paused""" pass time.sleep(0.1) - - def __threat_result(self, - response: Response, - rtt: float, - payload: Payload, - *args): - """Threats the result - - @type response: Response - @param response: The response object from the request - @type rtt: float - @param rtt: The elapsed time between request and response - @type payload: Payload - @param payload: The payload used in the request - """ - if (self.__blacklist_status and - response.status_code in self.__blacklist_status.codes): - self.__blacklist_status.do_action(response.status_code) - result = Result(response, rtt, payload, self.__requester.get_fuzzing_type()) - self.__scanner.inspect_result(result, *args) - self.result_callback( - result, - (self.__scanner.scan(result) - if self.__matcher.match(result) - else False) - ) diff --git a/src/fuzzingtool/core/job_manager.py b/src/fuzzingtool/core/job_manager.py new file mode 100644 index 00000000..f9d0e156 --- /dev/null +++ b/src/fuzzingtool/core/job_manager.py @@ -0,0 +1,112 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from queue import Queue +from typing import Dict + +from .bases.base_observer import BaseObserver +from .dictionary import Dictionary +from ..objects import Payload, Result + + +class JobManager(BaseObserver): + """Class responsible to manage the jobs + + Attributes: + current_job: The current job index + current_job_name: The current job that's running + pending_jobs: The pending jobs to run + total_jobs: The total jobs that'll run + total_requests: The total requests that'll be made on fuzzing + dictionary: The payload dictionary + job_providers: The job providers that enqueue new payloads for requests + max_rlevel: The maximum jobs recursion level + """ + def __init__(self, + dictionary: Dictionary, + job_providers: Dict[str, Queue], + max_rlevel: int): + """Class constructor + + @type dictionary: Dictionary + @param dictionary: The dictionary that'll be filled with payloads + @type job_providers: Dict[str, Queue[Payload]] + @param job_providers: The job providers, with name and queue + """ + wordlist_queue = Queue() + for payload in dictionary.wordlist: + wordlist_queue.put(Payload(payload)) + self.current_job = 0 + self.current_job_name = None + self.pending_jobs = Queue() + self.pending_jobs.put(("wordlist", wordlist_queue)) + self.total_jobs = 1 + self.total_requests = 0 + self.dictionary = dictionary + self.job_providers = job_providers + self.max_rlevel = max_rlevel + + def update(self, provider: str, result: Result) -> None: + """Update the total jobs count + + @type provider: str + @param provider: The provider name + @type result: Result + @param result: The FuzzingTool result object + """ + self.total_jobs += 1 + result.job_description = f"Enqueued new job from {provider}" + + def get_job(self) -> None: + """Gets a new job to run""" + self.current_job += 1 + self.current_job_name, job_queue = self.pending_jobs.get() + self.dictionary.fill_from_queue(job_queue, clear=True) + self.total_requests = len(self.dictionary) + + def has_pending_jobs(self) -> bool: + """Checks if has pending jobs to run + + @returns bool: The flag to say if has pending jobs to run + """ + return not self.pending_jobs.empty() + + def has_pending_jobs_from_providers(self) -> bool: + """Checks if has pending jobs from providers + + @returns bool: The flag to say if has pending jobs from providers + """ + for job_queue in self.job_providers.values(): + if not job_queue.empty(): + return True + return False + + def check_for_new_jobs(self) -> None: + """Check for new jobs from providers + If has, fill the dictionary with the payloads and enqueue the job + """ + for job_provider, job_queue in self.job_providers.items(): + new_job = Queue() + while not job_queue.empty(): + payload: Payload = job_queue.get() + if payload.rlevel <= self.max_rlevel: + new_job.put(payload) + if not new_job.empty(): + self.pending_jobs.put((job_provider, new_job)) diff --git a/src/fuzzingtool/core/matcher.py b/src/fuzzingtool/core/matcher.py index 096a2d45..9efa0cfe 100644 --- a/src/fuzzingtool/core/matcher.py +++ b/src/fuzzingtool/core/matcher.py @@ -21,70 +21,79 @@ from typing import List, Dict, Tuple, Callable, Union, Type import operator +from .bases.base_validator import BaseValidator from ..objects.result import Result from ..utils.utils import split_str_to_list -from ..exceptions.main_exceptions import BadArgumentType +from ..exceptions import BadArgumentType -def get_allowed_status(status: str, - allowed_list: List[int], - allowed_range: List[int]) -> None: +def get_status_code(status: str, + status_list: List[int], + status_range: List[int]) -> None: """Get the allowed status code list and range @type status: str @param status: The status cod given in the terminal - @type allowed_list: List[int] - @param allowed_list: The allowed status codes list - @type allowed_range: List[int] - @param allowed_range: The range of allowed status codes + @type status_list: List[int] + @param status_list: The allowed status codes list + @type status_range: List[int] + @param status_range: The range of allowed status codes """ try: if '-' not in status: - allowed_list.append(int(status)) + status_list.append(int(status)) else: code_left, code_right = (int(code) for code in status.split('-', 1)) if code_right < code_left: code_left, code_right = code_right, code_left - allowed_range[:] = [code_left, code_right] + status_range[:] = [code_left, code_right] except ValueError: raise BadArgumentType( f"The match status argument ({status}) must be integer" ) -class Matcher: +class Matcher(BaseValidator): """Class to handle with the match validations Attributes: comparator: The dictionary with the default entries to be compared with the current request - allowed_status: The dictionary with the - allowed status codes (and range) + status_code: The dictionary with the + allowed status codes (and range) """ def __init__(self, - allowed_status: str = None, + status_code: str = None, time: str = None, size: str = None, words: str = None, - lines: str = None): + lines: str = None, + regex: str = None): """Class constructor - @type allowed_status: dict - @param allowed_status: The allowed status dictionary - @type comparator: dict - @param comparator: The dict with comparator data - @type match_functions: Tuple[Callable, Callable] - @param match_functions: The callback functions for the match comparator + @type status_code: str + @param status_code: The allowed status codes string + @type time: str + @param time: The time to be compared with the RTT + @type size: str + @param size: The size to be compared with the response body length + @type words: str + @param words: The number of words to be compared with the response body + @type lines: str + @param lines: The number of lines to be compared with the response body + @type regex: str + @param regex: The regular expression to be compared with the response body """ - self._allowed_status = self.__build_allowed_status(allowed_status) + self._status_code = self.__build_status_code(status_code) self._comparator = self.__build_comparator(time, size, words, lines) + super().__init__(regex) - def allowed_status_is_default(self) -> bool: + def status_code_is_default(self) -> bool: """Check if the allowed status is set as default config @returns bool: If the allowed status is the default or not """ - return self._allowed_status['is_default'] + return self._status_code['is_default'] def comparator_is_set(self) -> bool: """Check if any of the comparators are seted @@ -97,13 +106,13 @@ def comparator_is_set(self) -> bool: return True return False - def set_allowed_status(self, allowed_status: str) -> None: + def set_status_code(self, status_code: str) -> None: """The allowed status setter - @type allowed_status: str - @param allowed_status: The allowed status + @type status_code: str + @param status_code: The allowed status """ - self._allowed_status = self.__build_allowed_status(allowed_status) + self._status_code = self.__build_status_code(status_code) def set_comparator(self, time: str, @@ -112,10 +121,14 @@ def set_comparator(self, lines: str) -> None: """The comparator setter - @type size: str - @param size: The size to be compared with response body @type time: str @param time: The time to be compared with the RTT + @type size: str + @param size: The size to be compared with the response body length + @type words: str + @param words: The number of words to be compared with the response body + @type lines: str + @param lines: The number of lines to be compared with the response body """ self._comparator = self.__build_comparator(time, size, words, lines) @@ -127,17 +140,24 @@ def match(self, result: Result) -> bool: @param result: The actual result object @returns bool: A match flag """ - if self._match_status(result.status): - if self._comparator['time'] is not None: - return self._match_time(result.rtt, self._comparator['time']) - if self._comparator['size'] is not None: - return self._match_size(int(result.body_size), self._comparator['size']) - if self._comparator['words'] is not None: - return self._match_words(result.words, self._comparator['words']) - if self._comparator['lines'] is not None: - return self._match_lines(result.lines, self._comparator['lines']) - return True - return False + if not self._match_status(result.history.status): + return False + if (self._comparator['time'] is not None and + not self._match_time(result.history.rtt, self._comparator['time'])): + return False + if (self._comparator['size'] is not None and + not self._match_size(int(result.history.body_size), self._comparator['size'])): + return False + if (self._comparator['words'] is not None and + not self._match_words(result.words, self._comparator['words'])): + return False + if (self._comparator['lines'] is not None and + not self._match_lines(result.lines, self._comparator['lines'])): + return False + if (self._regexer is not None and + not self._regexer.search(result.history.response.text)): + return False + return True def _match_status(self, status: int) -> bool: """Check if the result status match with the allowed status dict @@ -146,10 +166,10 @@ def _match_status(self, status: int) -> bool: @param status: The result status code @returns bool: if match returns True else False """ - return (status in self._allowed_status['list'] - or (self._allowed_status['range'] - and (self._allowed_status['range'][0] <= status - and status <= self._allowed_status['range'][1]))) + return (status in self._status_code['list'] + or (self._status_code['range'] + and (self._status_code['range'][0] <= status + and status <= self._status_code['range'][1]))) def _match_time(self, time: float, comparator_time: float) -> bool: """Check if the result time match with the comparator dict @@ -195,23 +215,23 @@ def _match_lines(self, lines: float, comparator_lines: float) -> bool: """ pass - def __build_allowed_status(self, allowed_status: str) -> dict: + def __build_status_code(self, status_code: str) -> dict: """Build the matcher attribute for allowed status - @type allowed_status: str - @param allowed_status: The allowed status codes to match results + @type status_code: str + @param status_code: The allowed status codes to match results @returns dict: The allowed status code, list and range, parsed into a dict """ - if not allowed_status: + if not status_code: is_default = True allowed_list = [200] else: is_default = False allowed_list = [] allowed_range = [] - for status in split_str_to_list(allowed_status): - get_allowed_status(status, allowed_list, allowed_range) + for status in split_str_to_list(status_code): + get_status_code(status, allowed_list, allowed_range) return { 'is_default': is_default, 'list': allowed_list, @@ -236,7 +256,7 @@ def set_match( @type comparator: str @param comparator: The value to be compared @returns Tuple[Callable, str]: The callback match function, - and the new comparator value + and the new comparator value """ comparator = str(comparator) for key, value in match.items(): @@ -294,10 +314,14 @@ def __build_comparator(self, lines: str) -> dict: """The comparator setter - @type size: str - @param size: The size to be compared with response body @type time: str @param time: The time to be compared with the RTT + @type size: str + @param size: The size to be compared with response body + @type words: str + @param words: The number of words to be compared with response body + @type lines: str + @param lines: The number of lines to be compared with responde body @returns dict: The data comparator """ comparator = { diff --git a/src/fuzzingtool/core/payloader.py b/src/fuzzingtool/core/payloader.py index 07b3534a..d8f5c686 100644 --- a/src/fuzzingtool/core/payloader.py +++ b/src/fuzzingtool/core/payloader.py @@ -24,7 +24,7 @@ from .bases.base_encoder import BaseEncoder from .defaults.encoders import ChainEncoder from ..objects.payload import Payload -from ..exceptions.main_exceptions import BadArgumentFormat +from ..exceptions import BadArgumentFormat class EncodeManager: @@ -176,7 +176,7 @@ def set_uppercase() -> None: """The uppercase setter""" def case(ajusted_payloads: List[Payload]) -> List[Payload]: return [ - payload.with_case(str.upper, "Upper") + Payload().update(payload).with_case(str.upper, "Upper") for payload in ajusted_payloads ] @@ -187,7 +187,7 @@ def set_lowercase() -> None: """The lowercase setter""" def case(ajusted_payloads: List[Payload]) -> List[Payload]: return [ - payload.with_case(str.lower, "Lower") + Payload().update(payload).with_case(str.lower, "Lower") for payload in ajusted_payloads ] @@ -198,21 +198,21 @@ def set_capitalize() -> None: """The capitalize setter""" def case(ajusted_payloads: List[Payload]) -> List[Payload]: return [ - payload.with_case(str.capitalize, "Capitalize") + Payload().update(payload).with_case(str.capitalize, "Capitalize") for payload in ajusted_payloads ] Payloader.case = case @staticmethod - def get_customized_payload(payload: str) -> List[Payload]: + def get_customized_payload(payload: Payload) -> List[Payload]: """Gets the payload list ajusted with the console options - @type payload: str - @param payload: The string payload gived by the payloads queue + @type payload: Payload + @param payload: The payload object gived by the payloads queue @returns List[Payload]: The payloads used in the request """ - ajusted_payloads = [Payload(payload)] + ajusted_payloads = [payload] if Payloader.prefix: ajusted_payloads = [Payload().update(payload).with_prefix(prefix) for prefix in Payloader.prefix diff --git a/src/fuzzingtool/core/plugins/encoders/base64.py b/src/fuzzingtool/core/plugins/encoders/base64.py index 90979f76..46671a93 100644 --- a/src/fuzzingtool/core/plugins/encoders/base64.py +++ b/src/fuzzingtool/core/plugins/encoders/base64.py @@ -30,7 +30,7 @@ class Base64(BaseEncoder, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Encode payload using Base64 encoder" - __type__ = '' + __type__ = None __version__ = "0.1" def encode(self, payload: str) -> str: diff --git a/src/fuzzingtool/core/plugins/encoders/hex.py b/src/fuzzingtool/core/plugins/encoders/hex.py index 73de57d5..c5c61aeb 100644 --- a/src/fuzzingtool/core/plugins/encoders/hex.py +++ b/src/fuzzingtool/core/plugins/encoders/hex.py @@ -28,7 +28,7 @@ class Hex(BaseEncoder, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Encode payload to hexadecimal" - __type__ = '' + __type__ = None __version__ = "0.2" def encode(self, payload: str) -> str: diff --git a/src/fuzzingtool/core/plugins/encoders/html.py b/src/fuzzingtool/core/plugins/encoders/html.py index 4fcb3acb..874394e5 100644 --- a/src/fuzzingtool/core/plugins/encoders/html.py +++ b/src/fuzzingtool/core/plugins/encoders/html.py @@ -30,7 +30,7 @@ class Html(BaseEncoder, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Encode payload using HTML entities encoder" - __type__ = '' + __type__ = None __version__ = "0.1" def encode(self, payload: str) -> str: diff --git a/src/fuzzingtool/core/plugins/encoders/html_dec.py b/src/fuzzingtool/core/plugins/encoders/html_dec.py index a8bc73eb..485fe9b2 100644 --- a/src/fuzzingtool/core/plugins/encoders/html_dec.py +++ b/src/fuzzingtool/core/plugins/encoders/html_dec.py @@ -28,7 +28,7 @@ class HtmlDec(BaseEncoder, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Encode payload to html decimal format" - __type__ = '' + __type__ = None __version__ = "0.1" def encode(self, payload: str) -> str: diff --git a/src/fuzzingtool/core/plugins/encoders/html_hex.py b/src/fuzzingtool/core/plugins/encoders/html_hex.py index f43af255..7dcf2446 100644 --- a/src/fuzzingtool/core/plugins/encoders/html_hex.py +++ b/src/fuzzingtool/core/plugins/encoders/html_hex.py @@ -28,7 +28,7 @@ class HtmlHex(BaseEncoder, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Encode payload to html hexadecimal format" - __type__ = '' + __type__ = None __version__ = "0.1" def encode(self, payload: str) -> str: diff --git a/src/fuzzingtool/core/plugins/encoders/plain.py b/src/fuzzingtool/core/plugins/encoders/plain.py index b5d295fc..972717fd 100644 --- a/src/fuzzingtool/core/plugins/encoders/plain.py +++ b/src/fuzzingtool/core/plugins/encoders/plain.py @@ -28,7 +28,7 @@ class Plain(BaseEncoder, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Do not encode the payload" - __type__ = "" + __type__ = None __version__ = "0.2" def encode(self, payload: str) -> str: diff --git a/src/fuzzingtool/core/plugins/encoders/url.py b/src/fuzzingtool/core/plugins/encoders/url.py index 0277800b..c5d55a37 100644 --- a/src/fuzzingtool/core/plugins/encoders/url.py +++ b/src/fuzzingtool/core/plugins/encoders/url.py @@ -23,7 +23,7 @@ from ...bases.base_plugin import Plugin from ...bases.base_encoder import BaseEncoder from ....decorators.plugin_meta import plugin_meta -from ....exceptions.main_exceptions import BadArgumentFormat +from ....exceptions import BadArgumentFormat @plugin_meta @@ -34,7 +34,7 @@ class Url(BaseEncoder, Plugin): 'type': str, } __desc__ = "Replace special characters in string using the %xx escape. Letters, digits, and the characters '_.-~' are never quoted." - __type__ = '' + __type__ = None __version__ = "0.2" def __init__(self, encode_level: int): diff --git a/src/fuzzingtool/core/plugins/scanners/__init__.py b/src/fuzzingtool/core/plugins/scanners/__init__.py index 7d1d5a43..b5ba74bd 100644 --- a/src/fuzzingtool/core/plugins/scanners/__init__.py +++ b/src/fuzzingtool/core/plugins/scanners/__init__.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from .backups import Backups from .reflected import Reflected -from .find import Find -from .grep import Grep \ No newline at end of file +from .grep import Grep +from .wappalyzer import Wappalyzer \ No newline at end of file diff --git a/src/fuzzingtool/core/plugins/scanners/find.py b/src/fuzzingtool/core/plugins/scanners/backups.py similarity index 60% rename from src/fuzzingtool/core/plugins/scanners/find.py rename to src/fuzzingtool/core/plugins/scanners/backups.py index d8ce94f5..b8ca94ee 100644 --- a/src/fuzzingtool/core/plugins/scanners/find.py +++ b/src/fuzzingtool/core/plugins/scanners/backups.py @@ -18,46 +18,42 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import re - from ...bases.base_plugin import Plugin from ...bases.base_scanner import BaseScanner from ....objects.result import Result from ....decorators.plugin_meta import plugin_meta -from ....decorators.append_args import append_args -from ....exceptions.main_exceptions import MissingParameter, BadArgumentFormat +from ....utils.consts import FuzzType + +DEFAULT_EXTENSIONS = ".bak,.tgz,.zip,.tar.gz,~,.rar,.old,.swp" @plugin_meta -class Find(BaseScanner, Plugin): +class Backups(BaseScanner, Plugin): __author__ = ("Vitor Oriel",) __params__ = { - 'metavar': "REGEX", - 'type': str, + 'metavar': "EXTENSION", + 'type': list, + 'cli_list_separator': ',', } - __desc__ = "Filter results based on a regex match into the response body" - __type__ = "" + __desc__ = ("Look for backups extension on matched responses. " + f"Default extensions: {DEFAULT_EXTENSIONS}") + __type__ = FuzzType.PATH_FUZZING __version__ = "0.1" """ Attributes: - regexer: The regex object to find the content into the response body + extensions: The extensions to look for """ - def __init__(self, regex: str): - if not regex: - raise MissingParameter("regex") - try: - self.__regexer = re.compile(regex) - except re.error: - raise BadArgumentFormat("invalid regex") - - @append_args - def inspect_result(self, result: Result) -> None: - result.custom['found'] = None + def __init__(self, extensions: list): + self.extensions = extensions if extensions else DEFAULT_EXTENSIONS.split(',') + BaseScanner.__init__(self) def scan(self, result: Result) -> bool: - found = (True - if self.__regexer.search(result.get_response().text) - else False) - result.custom['found'] = found - return found + return True + + def _process(self, result: Result) -> None: + if result.history.parsed_url.file_ext not in self.extensions: + for ext in self.extensions: + parsed_url = result.history.parsed_url + self.enqueue_payload(result, f"{parsed_url.file_name}{ext}") + self.enqueue_payload(result, f"{parsed_url.file}{ext}") diff --git a/src/fuzzingtool/core/plugins/scanners/grep.py b/src/fuzzingtool/core/plugins/scanners/grep.py index c5386332..023c133c 100644 --- a/src/fuzzingtool/core/plugins/scanners/grep.py +++ b/src/fuzzingtool/core/plugins/scanners/grep.py @@ -25,8 +25,7 @@ from ....objects.result import Result from ....utils.utils import stringfy_list from ....decorators.plugin_meta import plugin_meta -from ....decorators.append_args import append_args -from ....exceptions.main_exceptions import MissingParameter, BadArgumentFormat +from ....exceptions import MissingParameter, BadArgumentFormat PREPARED_REGEXES = { @@ -46,7 +45,7 @@ class Grep(BaseScanner, Plugin): __desc__ = ("Grep content based on a regex match into the response body. " "You can use these prepared regexes: " + stringfy_list(list(PREPARED_REGEXES.keys()))) - __type__ = "" + __type__ = None __version__ = "0.2" """ @@ -64,20 +63,16 @@ def __init__(self, regexes: list): self.__regexers.append(re.compile(regex)) except re.error: raise BadArgumentFormat(f"invalid regex: {regex}") - - @append_args - def inspect_result(self, result: Result) -> None: - result.custom['found'] = None - for i in range(len(self.__regexers)): - result.custom[f'greped_regex_{i}'] = [] + BaseScanner.__init__(self) def scan(self, result: Result) -> bool: - total_greped = 0 + return True + + def _process(self, result: Result) -> None: + self.get_self_res(result).data['found'] = 0 for i, regexer in enumerate(self.__regexers): this_greped = list(set([ - r.group() for r in regexer.finditer(result.get_response().text) + r.group() for r in regexer.finditer(result.history.response.text) ])) - total_greped += len(this_greped) - result.custom[f'greped_regex_{i}'] = this_greped - result.custom['found'] = total_greped - return True if total_greped else False + self.get_self_res(result).data['found'] += len(this_greped) + self.get_self_res(result).data[f'greped_regex_{i}'] = this_greped diff --git a/src/fuzzingtool/core/plugins/scanners/reflected.py b/src/fuzzingtool/core/plugins/scanners/reflected.py index 354408c5..8c6c32c9 100644 --- a/src/fuzzingtool/core/plugins/scanners/reflected.py +++ b/src/fuzzingtool/core/plugins/scanners/reflected.py @@ -22,7 +22,6 @@ from ...bases.base_scanner import BaseScanner from ....objects.result import Result from ....decorators.plugin_meta import plugin_meta -from ....decorators.append_args import append_args @plugin_meta @@ -30,14 +29,8 @@ class Reflected(BaseScanner, Plugin): __author__ = ("Vitor Oriel",) __params__ = {} __desc__ = "Lookup if the payload was reflected in the response body" - __type__ = "" + __type__ = None __version__ = "0.1" - @append_args - def inspect_result(self, result: Result) -> None: - result.custom['reflected'] = None - def scan(self, result: Result) -> bool: - reflected = result.payload in result.get_response().text - result.custom['reflected'] = reflected - return reflected + return result.payload in result.history.response.text diff --git a/src/fuzzingtool/core/plugins/scanners/wappalyzer.py b/src/fuzzingtool/core/plugins/scanners/wappalyzer.py new file mode 100644 index 00000000..279b1556 --- /dev/null +++ b/src/fuzzingtool/core/plugins/scanners/wappalyzer.py @@ -0,0 +1,56 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import warnings + +import Wappalyzer as WP + +from ...bases.base_plugin import Plugin +from ...bases.base_scanner import BaseScanner +from ....objects.result import Result +from ....decorators.plugin_meta import plugin_meta + + +@plugin_meta +class Wappalyzer(BaseScanner, Plugin): + __author__ = ("Vitor Oriel",) + __params__ = {} + __desc__ = "Lookup for technologies on a web page during discovery scan" + __type__ = None + __version__ = "0.1" + + def __init__(self): + warnings.filterwarnings("ignore", category=UserWarning) + BaseScanner.__init__(self) + + def scan(self, result: Result) -> bool: + return True + + def _process(self, result: Result) -> None: + wp_technologies_dict = WP.Wappalyzer.latest().analyze_with_versions_and_categories( + webpage=WP.WebPage.new_from_response(result.history.response) + ) + technologies = [] + for key, value in wp_technologies_dict.items(): + version = '' + if value['versions']: + version = ' v' + ' | v'.join(value['versions']) + technologies.append(f"{key}{version}") + self.get_self_res(result).data['technologies'] = technologies diff --git a/src/fuzzingtool/core/plugins/wordlists/crt_sh.py b/src/fuzzingtool/core/plugins/wordlists/crt_sh.py index aaf1daba..93d38630 100644 --- a/src/fuzzingtool/core/plugins/wordlists/crt_sh.py +++ b/src/fuzzingtool/core/plugins/wordlists/crt_sh.py @@ -21,14 +21,13 @@ import re from typing import List -from bs4 import BeautifulSoup as bs - from ...bases.base_plugin import Plugin from ...bases.base_wordlist import BaseWordlist from ....conn.requesters.requester import Requester from ....exceptions.request_exceptions import RequestException from ....decorators.plugin_meta import plugin_meta -from ....exceptions.main_exceptions import MissingParameter, BuildWordlistFails +from ....utils.consts import FuzzType +from ....exceptions import MissingParameter, BuildWordlistFails CRTSH_HTTP_HEADER = { 'Host': "crt.sh", @@ -51,8 +50,8 @@ class CrtSh(BaseWordlist, Plugin): 'type': str, } __desc__ = "Build the wordlist based on the content of the site crt.sh" - __type__ = "SubdomainFuzzing" - __version__ = "0.1" + __type__ = FuzzType.SUBDOMAIN_FUZZING + __version__ = "0.2" def __init__(self, host: str): if not host: @@ -61,9 +60,21 @@ def __init__(self, host: str): BaseWordlist.__init__(self) def _build(self) -> List[str]: + response_json = self.__get_response_json() + if not response_json: + raise BuildWordlistFails(f"No certified domains was found for '{self.host}'") + domain_list = self.__get_domain_list(response_json) + return [domain.split(f'.{self.host}')[0] + for domain in domain_list] + + def __get_response_json(self) -> List[dict]: + """Get the json response from CrtSh API + + @returns List[dict]: The json response + """ global CRTSH_HTTP_HEADER requester = Requester( - url=f"https://crt.sh/?q={self.host}", + url=f"https://crt.sh/?q={self.host}&output=json", method='GET', headers=CRTSH_HTTP_HEADER, ) @@ -71,16 +82,21 @@ def _build(self) -> List[str]: response, *_ = requester.request() except RequestException as e: raise BuildWordlistFails(str(e)) - if 'None found' in response.text: - raise BuildWordlistFails(f"No certified domains was found for '{self.host}'") - content_list = [element.string - for element in bs(response.text, "html.parser")('td')] + return response.json() + + def __get_domain_list(self, response_json: List[dict]) -> List[str]: + """Get the domain list from the CrtSh API json response + + @type response_json: List[dict] + @param response_json: The CrtSh API json response + @returns List[str]: The filtered domain list + """ + content_list = list(set([element['common_name'] + for element in response_json])) regex = r"([a-zA-Z0-9]+\.)*[a-zA-Z0-9]+" for splited in self.host.split('.'): - regex += r"\."+splited + regex += r"\." + splited regexer = re.compile(regex) - domain_list = sorted(set([element - for element in content_list - if regexer.match(str(element))])) - return [domain.split(f'.{self.host}')[0] - for domain in domain_list] + return [element + for element in content_list + if regexer.match(str(element))] diff --git a/src/fuzzingtool/core/plugins/wordlists/dns_dumpster.py b/src/fuzzingtool/core/plugins/wordlists/dns_dumpster.py index 917d338d..80ad128d 100644 --- a/src/fuzzingtool/core/plugins/wordlists/dns_dumpster.py +++ b/src/fuzzingtool/core/plugins/wordlists/dns_dumpster.py @@ -28,7 +28,8 @@ from ....conn.requesters.requester import Requester from ....exceptions.request_exceptions import RequestException from ....decorators.plugin_meta import plugin_meta -from ....exceptions.main_exceptions import MissingParameter, BuildWordlistFails +from ....utils.consts import FuzzType +from ....exceptions import MissingParameter, BuildWordlistFails DNSDUMPSTER_HTTP_HEADER = { 'Host': "dnsdumpster.com", @@ -50,7 +51,7 @@ class DnsDumpster(BaseWordlist, Plugin): 'type': str, } __desc__ = "Build the wordlist based on the content of the site dnsdumpster.com" - __type__ = "SubdomainFuzzing" + __type__ = FuzzType.SUBDOMAIN_FUZZING __version__ = "0.1" def __init__(self, host: str): diff --git a/src/fuzzingtool/core/plugins/wordlists/dns_zone.py b/src/fuzzingtool/core/plugins/wordlists/dns_zone.py index 8e786058..e076dad9 100644 --- a/src/fuzzingtool/core/plugins/wordlists/dns_zone.py +++ b/src/fuzzingtool/core/plugins/wordlists/dns_zone.py @@ -19,13 +19,15 @@ # SOFTWARE. from typing import List +from socket import gethostbyname, gaierror from dns import resolver, query, zone from ...bases.base_plugin import Plugin from ...bases.base_wordlist import BaseWordlist from ....decorators.plugin_meta import plugin_meta -from ....exceptions.main_exceptions import MissingParameter, BuildWordlistFails +from ....utils.consts import FuzzType +from ....exceptions import MissingParameter, BuildWordlistFails @plugin_meta @@ -36,7 +38,7 @@ class DnsZone(BaseWordlist, Plugin): 'type': str, } __desc__ = "Build the wordlist based on a DNS zone transfer request" - __type__ = "SubdomainFuzzing" + __type__ = FuzzType.SUBDOMAIN_FUZZING __version__ = "0.2" def __init__(self, host: str): @@ -46,6 +48,10 @@ def __init__(self, host: str): BaseWordlist.__init__(self) def _build(self) -> List[str]: + try: + gethostbyname(self.host) + except gaierror: + raise BuildWordlistFails(f"Couldn't resolve hostname {self.host}") name_servers = resolver.resolve(self.host, 'NS') name_servers_ips = [] for ns in name_servers: diff --git a/src/fuzzingtool/core/plugins/wordlists/overflow.py b/src/fuzzingtool/core/plugins/wordlists/overflow.py index 338de41d..90563142 100644 --- a/src/fuzzingtool/core/plugins/wordlists/overflow.py +++ b/src/fuzzingtool/core/plugins/wordlists/overflow.py @@ -23,8 +23,7 @@ from ...bases.base_plugin import Plugin from ...bases.base_wordlist import BaseWordlist from ....decorators.plugin_meta import plugin_meta -from ....exceptions.main_exceptions import (MissingParameter, BadArgumentFormat, - BadArgumentType) +from ....exceptions import MissingParameter, BadArgumentFormat, BadArgumentType @plugin_meta @@ -35,7 +34,7 @@ class Overflow(BaseWordlist, Plugin): 'type': str, } __desc__ = "Build the wordlist for stress and buffer overflow purposes" - __type__ = "" + __type__ = None __version__ = "0.1" def __init__(self, source_param: str): diff --git a/src/fuzzingtool/core/plugins/wordlists/robots.py b/src/fuzzingtool/core/plugins/wordlists/robots.py index 18a09e87..75043b73 100644 --- a/src/fuzzingtool/core/plugins/wordlists/robots.py +++ b/src/fuzzingtool/core/plugins/wordlists/robots.py @@ -24,11 +24,12 @@ from ...bases.base_plugin import Plugin from ...bases.base_wordlist import BaseWordlist -from ....utils.http_utils import get_path +from ....utils.http_utils import get_parsed_url from ....conn.requesters.requester import Requester from ....decorators.plugin_meta import plugin_meta +from ....utils.consts import FuzzType from ....exceptions.request_exceptions import RequestException -from ....exceptions.main_exceptions import MissingParameter, BuildWordlistFails +from ....exceptions import MissingParameter, BuildWordlistFails ROBOTS_HTTP_HEADER = { 'User-Agent': "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0", @@ -44,7 +45,7 @@ class Robots(BaseWordlist, Plugin): 'type': str, } __desc__ = "Build the wordlist using the target robots.txt" - __type__ = "PathFuzzing" + __type__ = FuzzType.PATH_FUZZING __version__ = "0.1" def __init__(self, url: str): @@ -77,6 +78,6 @@ def _build(self) -> List[str]: )): _, path = line.split(': ', 1) if '://' in path: - path = get_path(path) + path = get_parsed_url(path).path paths.append(path[1:]) return paths diff --git a/src/fuzzingtool/core/recursion_manager.py b/src/fuzzingtool/core/recursion_manager.py new file mode 100644 index 00000000..6a8317b1 --- /dev/null +++ b/src/fuzzingtool/core/recursion_manager.py @@ -0,0 +1,88 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from queue import Queue +from typing import List + +from .bases.job_provider import JobProvider +from ..objects import Payload, Result + + +class RecursionManager(JobProvider): + """Class responsible to manage the directory recursion + + Attributes: + max_rlevel: The maximum jobs recursion level + wordlist: The wordlist with base payloads + directories_queue: The control queue for directories found + payloads_queue: The jobs queue for the job manager + """ + def __init__(self, max_rlevel: int, wordlist: List[str]): + """Class constructor + + @type max_rlevel: int + @param max_rlevel: The maximum jobs recursion level + @type wordlist: List[str] + @param wordlist: The wordlist with base payloads + """ + self.max_rlevel = max_rlevel + self.wordlist = wordlist + self.directories_queue = Queue() + self.payloads_queue = Queue() + super().__init__() + + def notify(self, result: Result, path: str) -> None: + """Notify the observer with the new job + + @type result: Result + @param result: The FuzzingTool result object + @type path: str + @param path: The path that enqueued the job + """ + self._observer.update(f"directory recursion on path {path}", result) + + def has_recursive_job(self) -> bool: + """Check if has pending recursive job + + @returns bool: A flag to say if has recursive job + """ + return not self.directories_queue.empty() + + def check_for_recursion(self, result: Result) -> None: + """Check if a result is eligible for recursion, and enqueue it into the directories queue + + @type result: Result + @param result: THe FuzzingTool result object + """ + payload = result._payload + if result.history.is_path and payload.rlevel < self.max_rlevel: + path = result.history.parsed_url.path + self.directories_queue.put( + Payload().update(payload).with_recursion(path[1:]) + ) + self.notify(result, path) + + def fill_payloads_queue(self) -> None: + """Fill the payloads queue with recursive directory payloads""" + recursive_directory = self.directories_queue.get() + for wordlist_payload in self.wordlist: + new_payload = Payload().update(recursive_directory) + new_payload.final += wordlist_payload + self.payloads_queue.put(new_payload) diff --git a/src/fuzzingtool/decorators/plugin_meta.py b/src/fuzzingtool/decorators/plugin_meta.py index ded4b857..47af846d 100644 --- a/src/fuzzingtool/decorators/plugin_meta.py +++ b/src/fuzzingtool/decorators/plugin_meta.py @@ -19,7 +19,8 @@ # SOFTWARE. from ..core.bases.base_plugin import Plugin -from ..exceptions.main_exceptions import MetadataException +from ..utils.consts import FuzzType +from ..exceptions import MetadataException def plugin_meta(cls: Plugin) -> Plugin: @@ -37,6 +38,12 @@ def plugin_meta(cls: Plugin) -> Plugin: raise MetadataException( f"Description cannot be blank on plugin {cls.__name__}" ) + if cls.__type__ is not None and cls.__type__ not in [ + value for key, value in vars(FuzzType).items() if not key.startswith("__") + ]: + raise MetadataException( + f"Plugin type should be None or a valid FuzzType on plugin {cls.__name__}" + ) if not cls.__version__: raise MetadataException(f"Version cannot be blank on plugin {cls.__name__}") return cls @@ -64,7 +71,7 @@ def _check_params_meta(cls: Plugin) -> None: @type cls: Plugin @param cls: The class with the plugin metadata """ - if not (type(cls.__params__) is dict): + if (type(cls.__params__) is not dict): raise MetadataException("The parameters must be a " f"dictionary on plugin {cls.__name__}") param_dict_keys = cls.__params__.keys() diff --git a/src/fuzzingtool/decorators/report_meta.py b/src/fuzzingtool/decorators/report_meta.py index fac26878..cdd44853 100644 --- a/src/fuzzingtool/decorators/report_meta.py +++ b/src/fuzzingtool/decorators/report_meta.py @@ -18,8 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from ..reports.base_report import BaseReport -from ..exceptions.main_exceptions import MetadataException +from ..persistence.base_report import BaseReport +from ..exceptions import MetadataException def report_meta(cls: BaseReport) -> BaseReport: diff --git a/src/fuzzingtool/exceptions/__init__.py b/src/fuzzingtool/exceptions/__init__.py index bf4cbffc..b6139f36 100644 --- a/src/fuzzingtool/exceptions/__init__.py +++ b/src/fuzzingtool/exceptions/__init__.py @@ -16,4 +16,8 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. \ No newline at end of file +# SOFTWARE. + +from .main_exceptions import * +from .param_exceptions import * +from .request_exceptions import * diff --git a/src/fuzzingtool/exceptions/main_exceptions.py b/src/fuzzingtool/exceptions/main_exceptions.py index 067c008c..10fbe7f1 100644 --- a/src/fuzzingtool/exceptions/main_exceptions.py +++ b/src/fuzzingtool/exceptions/main_exceptions.py @@ -21,7 +21,7 @@ from .base_exceptions import FuzzingToolException -class FuzzControllerException(FuzzingToolException): +class FuzzLibException(FuzzingToolException): pass @@ -29,22 +29,6 @@ class StopActionInterrupt(FuzzingToolException): pass -class MissingParameter(FuzzingToolException): - pass - - -class BadArgumentFormat(FuzzingToolException): - pass - - -class BadArgumentType(FuzzingToolException): - pass - - -class InvalidArgument(FuzzingToolException): - pass - - class BuildWordlistFails(FuzzingToolException): pass diff --git a/src/fuzzingtool/reports/__init__.py b/src/fuzzingtool/exceptions/param_exceptions.py similarity index 79% rename from src/fuzzingtool/reports/__init__.py rename to src/fuzzingtool/exceptions/param_exceptions.py index bf4cbffc..22a9e3aa 100644 --- a/src/fuzzingtool/reports/__init__.py +++ b/src/fuzzingtool/exceptions/param_exceptions.py @@ -16,4 +16,22 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. \ No newline at end of file +# SOFTWARE. + +from .base_exceptions import FuzzingToolException + + +class MissingParameter(FuzzingToolException): + pass + + +class BadArgumentFormat(FuzzingToolException): + pass + + +class BadArgumentType(FuzzingToolException): + pass + + +class InvalidArgument(FuzzingToolException): + pass diff --git a/src/fuzzingtool/factories/plugin_factory.py b/src/fuzzingtool/factories/plugin_factory.py index 9acec079..21d923ec 100644 --- a/src/fuzzingtool/factories/plugin_factory.py +++ b/src/fuzzingtool/factories/plugin_factory.py @@ -25,7 +25,8 @@ from ..utils.utils import split_str_to_list from ..core.bases.base_plugin import Plugin from ..core.plugins import encoders, scanners, wordlists -from ..exceptions.main_exceptions import MissingParameter, BadArgumentFormat +from ..utils.consts import PluginCategory +from ..exceptions import MissingParameter, BadArgumentFormat from ..exceptions.plugin_exceptions import (InvalidPluginCategory, InvalidPlugin, PluginCreationError) @@ -41,9 +42,9 @@ class PluginFactory(BasePluginFactory): @staticmethod def get_plugins_from_category(category: str) -> List[Type[Plugin]]: plugin_categories = { - 'encoders': encoders, - 'scanners': scanners, - 'wordlists': wordlists, + PluginCategory.encoder: encoders, + PluginCategory.scanner: scanners, + PluginCategory.wordlist: wordlists, } try: plugins = [cls for _, cls in plugin_categories[category].__dict__.items() @@ -53,7 +54,7 @@ def get_plugins_from_category(category: str) -> List[Type[Plugin]]: return plugins @staticmethod - def class_creator(name: str, category: str) -> Type[Plugin]: + def class_creator(category: str, name: str) -> Type[Plugin]: plugin_module = import_module( f"fuzzingtool.core.plugins.{category}", package=name @@ -65,9 +66,9 @@ def class_creator(name: str, category: str) -> Type[Plugin]: return plugin_cls @staticmethod - def object_creator(name: str, category: str, params) -> Plugin: + def object_creator(category: str, name: str, params) -> Plugin: try: - plugin_cls = PluginFactory.class_creator(name, category) + plugin_cls = PluginFactory.class_creator(category, name) except InvalidPlugin as e: raise PluginCreationError(str(e)) if not plugin_cls.__params__: diff --git a/src/fuzzingtool/factories/wordlist_factory.py b/src/fuzzingtool/factories/wordlist_factory.py index 4a14ff6c..75f4d3d8 100644 --- a/src/fuzzingtool/factories/wordlist_factory.py +++ b/src/fuzzingtool/factories/wordlist_factory.py @@ -21,10 +21,11 @@ from .base_factories import BaseWordlistFactory from .plugin_factory import PluginFactory from ..core.bases.base_wordlist import BaseWordlist -from ..utils.http_utils import get_host, get_pure_url +from ..utils.consts import PluginCategory +from ..utils.http_utils import get_parsed_url, get_pure_url from ..conn.requesters.requester import Requester from ..core.defaults.wordlists import ListWordlist, FileWordlist -from ..exceptions.main_exceptions import WordlistCreationError +from ..exceptions import WordlistCreationError from ..exceptions.plugin_exceptions import InvalidPlugin, PluginCreationError @@ -32,7 +33,7 @@ class WordlistFactory(BaseWordlistFactory): @staticmethod def creator(name: str, params: str, requester: Requester) -> BaseWordlist: try: - wordlist_cls = PluginFactory.class_creator(name, 'wordlists') + wordlist_cls = PluginFactory.class_creator(PluginCategory.wordlist, name) except InvalidPlugin: if name.startswith('[') and name.endswith(']'): wordlist_obj = ListWordlist(name) @@ -44,11 +45,13 @@ def creator(name: str, params: str, requester: Requester) -> BaseWordlist: wordlist_cls.__params__['metavar'] in ["TARGET_HOST", "TARGET_URL"]): if "TARGET_HOST" in wordlist_cls.__params__['metavar']: - params = get_host(get_pure_url(requester.get_url())) + params = get_parsed_url(get_pure_url(requester.get_url())).hostname elif "TARGET_URL" in wordlist_cls.__params__['metavar']: params = get_pure_url(requester.get_url()) try: - wordlist_obj = PluginFactory.object_creator(name, 'wordlists', params) + wordlist_obj = PluginFactory.object_creator( + PluginCategory.wordlist, name, params + ) except PluginCreationError as e: raise WordlistCreationError(str(e)) return wordlist_obj diff --git a/src/fuzzingtool/api/fuzz_controller.py b/src/fuzzingtool/fuzz_lib.py similarity index 57% rename from src/fuzzingtool/api/fuzz_controller.py rename to src/fuzzingtool/fuzz_lib.py index 1fee6a59..2934e60b 100644 --- a/src/fuzzingtool/api/fuzz_controller.py +++ b/src/fuzzingtool/fuzz_lib.py @@ -20,29 +20,35 @@ from typing import Tuple, List, Union import time - -from ..interfaces.argument_builder import ArgumentBuilder as AB -from ..utils.utils import split_str_to_list -from ..utils.file_utils import read_file -from ..utils.result_utils import ResultUtils -from ..core import BlacklistStatus, Dictionary, Fuzzer, Matcher, Payloader, Summary -from ..core.defaults.scanners import (DataScanner, - PathScanner, SubdomainScanner) -from ..core.bases import BaseScanner, BaseEncoder -from ..conn.requesters import Requester, SubdomainRequester -from ..conn.request_parser import check_is_subdomain_fuzzing -from ..factories import PluginFactory, WordlistFactory -from ..objects import Error, Result -from ..exceptions.main_exceptions import (FuzzControllerException, StopActionInterrupt, - WordlistCreationError, BuildWordlistFails) - - -class FuzzController: +from threading import Thread + +from requests.models import Response + +from .utils.argument_utils import (build_target_from_args, build_target_from_raw_http, + build_wordlist, build_encoder, build_scanner, + build_blacklist_status) +from .utils.consts import PluginCategory +from .utils.utils import split_str_to_list +from .utils.file_utils import read_file +from .utils.result_utils import ResultUtils +from .core import (BlacklistStatus, Dictionary, Fuzzer, Filter, + JobManager, Matcher, Payloader, RecursionManager, Summary) +from .core.bases import BaseScanner, BaseEncoder +from .core.defaults.scanners import (DataScanner, + PathScanner, SubdomainScanner) +from .conn.requesters import Requester, SubdomainRequester +from .conn.request_parser import check_is_subdomain_fuzzing +from .factories import PluginFactory, WordlistFactory +from .objects import BaseItem, Error, Result, HttpHistory, Payload +from .exceptions import (FuzzLibException, StopActionInterrupt, + WordlistCreationError, BuildWordlistFails) + + +class FuzzLib: def __init__(self, **kwargs): self.args = self.__get_default_args() self.args.update(kwargs) self.requester = None - self.elapsed_time = None self.fuzzer = None self.blacklist_status = None self.summary = Summary() @@ -67,38 +73,22 @@ def init(self) -> None: Set the application variables including plugins requires """ self._init_requester() + self._init_filter() self._init_matcher() - self._init_scanner() - if self.args["blacklist_status"]: - blacklisted_status, action, action_param = AB.build_blacklist_status( - self.args["blacklist_status"] - ) - self.blacklist_status = BlacklistStatus( - status=blacklisted_status, - action=action, - action_param=action_param, - action_callbacks={ - 'stop': self._stop_callback, - 'wait': self._wait_callback, - }, - ) - self.delay = self.args["delay"] - self.number_of_threads = self.args["threads"] + self._init_scanners() + self._init_other_arguments() self._init_dictionary() - self.total_requests = (len(self.dictionary) - * len(self.requester.methods)) + self._init_managers() + self._set_observer() def start(self) -> None: - """Starts the fuzzing application. - The target is fuzzed based on his own methods list - """ - Result.reset_index() + """Starts the fuzzing application""" self.summary.start_timer() try: - for method in self.requester.methods: - self.requester.set_method(method) - self.dictionary.reload() - self.fuzz() + while self.job_manager.has_pending_jobs(): + self._get_job() + self.__fuzz() + self._check_for_new_jobs() except StopActionInterrupt as e: if self.fuzzer and self.fuzzer.is_running(): self.fuzzer.stop() @@ -106,32 +96,6 @@ def start(self) -> None: finally: self.summary.stop_timer() - def fuzz(self) -> None: - """Prepare the fuzzer for the fuzzing tests""" - self.fuzzer = Fuzzer( - requester=self.requester, - dictionary=self.dictionary, - matcher=self.matcher, - scanner=self.scanner, - delay=self.delay, - number_of_threads=self.number_of_threads, - blacklist_status=self.blacklist_status, - result_callback=self._result_callback, - exception_callbacks=[ - self._invalid_hostname_callback, - self._request_exception_callback - ], - ) - self.fuzzer.start() - self.fuzzer_join() - - def fuzzer_join(self): - """Join the fuzzer""" - while self.fuzzer.join(): - if self.stop_action: - raise StopActionInterrupt(self.stop_action) - self.fuzzer.stop() - def _stop_callback(self, status: int) -> None: """The skip target callback for the blacklist_action @@ -152,13 +116,13 @@ def _wait_callback(self, status: int) -> None: time.sleep(self.blacklist_status.action_param) self.fuzzer.resume() - def _result_callback(self, result: Result, validate: bool) -> None: - """Callback function for the results output + def _result_callback(self, result: Result, valid: bool) -> None: + """Callback function for the FuzzingTool results @type result: Result - @param result: The FuzzingTool result - @type validate: bool - @param validate: A validator flag for the result, gived by the scanner + @param result: The FuzzingTool result object + @type valid: bool + @param valid: A validator flag for the result """ pass @@ -182,22 +146,22 @@ def _init_requester(self) -> None: """Initialize the requester""" target = None if self.args["url"]: - target = AB.build_target_from_args( + target = build_target_from_args( self.args["url"], self.args["method"], self.args["data"] ) if self.args["raw_http"]: - target = AB.build_target_from_raw_http( + target = build_target_from_raw_http( self.args["raw_http"], self.args["scheme"] ) if not target: - raise FuzzControllerException("A target is needed to make the fuzzing") + raise FuzzLibException("A target is needed to make the fuzzing") if check_is_subdomain_fuzzing(target['url']): requester_cls = SubdomainRequester else: requester_cls = Requester self.requester = requester_cls( url=target['url'], - methods=target['methods'], + method=target['method'], body=target['body'], headers=target['header'], follow_redirects=self.args["follow_redirects"], @@ -206,6 +170,14 @@ def _init_requester(self) -> None: if self.args["proxies"] else []), timeout=self.args["timeout"], cookie=self.args["cookie"], + replay_proxy=self.args["replay_proxy"], + ) + + def _init_filter(self) -> None: + """Initialize the filter""" + self.filter = Filter( + self.args['filter_status'], + self.args['filter_regex'] ) def _init_matcher(self) -> None: @@ -216,26 +188,51 @@ def _init_matcher(self) -> None: self.args['match_size'], self.args['match_words'], self.args['match_lines'], + self.args['match_regex'], ) if (self.requester.is_url_discovery() and - self.matcher.allowed_status_is_default()): - self.matcher.set_allowed_status("200-399,401,403") + self.matcher.status_code_is_default()): + self.matcher.set_status_code("200-399,401,403") - def _init_scanner(self) -> None: - """Initialize the scanner""" + def _init_scanners(self) -> None: + """Initialize the scanners""" + self.scanners: List[BaseScanner] = [self.__get_default_scanner()] if self.args["scanner"]: - scanner, param = AB.build_scanner(self.args["scanner"]) - self.scanner: BaseScanner = PluginFactory.object_creator( - scanner, 'scanners', param + scanners = (self.args["scanner"] + if isinstance(self.args["scanner"], list) + else [self.args["scanner"]]) + for scanner in scanners: + scanner, param = build_scanner(scanner) + plugin_scanner: BaseScanner = PluginFactory.object_creator( + PluginCategory.scanner, scanner, param + ) + self.scanners.append(plugin_scanner) + + def _init_other_arguments(self) -> None: + """Initialize the uncategorized arguments""" + if self.args["blacklist_status"]: + blacklisted_status, action, action_param = build_blacklist_status( + self.args["blacklist_status"] ) - else: - self.scanner = self.__get_default_scanner() + self.blacklist_status = BlacklistStatus( + status=blacklisted_status, + action=action, + action_param=action_param, + action_callbacks={ + 'stop': self._stop_callback, + 'wait': self._wait_callback, + }, + ) + self.delay = self.args["delay"] + self.number_of_threads = self.args["threads"] + self.has_recursion = self.args["recursive"] + self.replay_proxy = self.args["replay_proxy"] def _init_dictionary(self) -> None: """Initialize the dictionary""" self.__configure_payloader() final_wordlist = self.__build_wordlist( - AB.build_wordlist(self.args["wordlist"]) + build_wordlist(self.args["wordlist"]) ) atual_length = len(final_wordlist) self.dict_metadata = {} @@ -247,6 +244,45 @@ def _init_dictionary(self) -> None: self.dict_metadata['len'] = atual_length self.dictionary = Dictionary(final_wordlist) + def _init_managers(self) -> None: + """Initialize the recursion manager and job manager""" + self.recursion_manager = RecursionManager( + max_rlevel=self.args["max_rlevel"], + wordlist=self.dictionary.wordlist + ) + self.job_manager = JobManager( + dictionary=self.dictionary, + job_providers={ + **{str(scanner): scanner.payloads_queue for scanner in self.scanners[1:]}, + "recursion": self.recursion_manager.payloads_queue, + }, + max_rlevel=self.args["max_rlevel"] + ) + + def _set_observer(self) -> None: + """Set the job manager as observer for the job providers""" + self.recursion_manager.set_observer(self.job_manager) + for scanner in self.scanners[1:]: + scanner.set_observer(self.job_manager) + + def _get_job(self) -> None: + """Get a job from the job queue""" + BaseItem.reset_index() + self.job_manager.get_job() + + def _join(self) -> None: + """Blocks until the fuzzer ends""" + while self.fuzzer.join(): + if self.stop_action: + raise StopActionInterrupt(self.stop_action) + self.fuzzer.stop() + + def _check_for_new_jobs(self) -> None: + """Check for new jobs""" + if self.recursion_manager.has_recursive_job(): + self.recursion_manager.fill_payloads_queue() + self.job_manager.check_for_new_jobs() + def __get_default_args(self) -> dict: """Gets the default arguments for the program @@ -275,12 +311,15 @@ def __get_default_args(self) -> dict: upper=False, lower=False, capitalize=False, - # Match and Scanner options + # Matcher, Filter and Scanner options match_status=None, match_time=None, match_size=None, match_words=None, match_lines=None, + match_regex=None, + filter_status=None, + filter_regex=None, scanner=None, # Display options simple_output=False, @@ -288,22 +327,21 @@ def __get_default_args(self) -> dict: threads=1, delay=0, blacklist_status=None, + recursive=False, + max_rlevel=1, + replay_proxy=None, # Callbacks res_callback=None, req_ex_callback=None, invalid_host_calalback=None, ) - def __build_encoders(self) -> Union[ - Tuple[List[BaseEncoder], List[List[BaseEncoder]]], None - ]: + def __build_encoders(self) -> Tuple[List[BaseEncoder], List[List[BaseEncoder]]]: """Build the encoders - @returns Tuple | None: The encoders used in the program + @returns tuple: The encoders used in the program """ - if not self.args["encoder"]: - return None - encoders_list = AB.build_encoder(self.args["encoder"]) + encoders_list = build_encoder(self.args["encoder"]) if self.args["encode_only"]: Payloader.encoder.set_regex(self.args["encode_only"]) encoders_default = [] @@ -318,7 +356,7 @@ def __build_encoders(self) -> Union[ for encoder in encoders: name, param = encoder encoder = PluginFactory.object_creator( - name, 'encoders', param + PluginCategory.encoder, name, param ) append_to.append(encoder) if is_chain: @@ -337,9 +375,8 @@ def __configure_payloader(self) -> None: Payloader.set_uppercase() elif self.args["capitalize"]: Payloader.set_capitalize() - encoders = self.__build_encoders() - if encoders: - Payloader.encoder.set_encoders(encoders) + if self.args["encoder"]: + Payloader.encoder.set_encoders(self.__build_encoders()) def __build_wordlist(self, wordlists: List[Tuple[str, str]]) -> List[str]: @@ -349,9 +386,13 @@ def __build_wordlist(self, @param wordlists: The wordlists used in the dictionary @returns List[str]: The builded payload wordlist """ - final_wordlist = [] - self.wordlist_errors: List[Union[WordlistCreationError, BuildWordlistFails]] = [] - for wordlist in wordlists: + def run(wordlist: Tuple[str, str]) -> None: + """Run the wordlist thread function + + @type wordlist: Tuple[str, str] + @param wordlist: The wordlist name and parameter + """ + nonlocal final_wordlist try: wordlist_obj = WordlistFactory.creator(*wordlist, self.requester) wordlist_obj.build() @@ -359,8 +400,16 @@ def __build_wordlist(self, self.wordlist_errors.append(e) else: final_wordlist.extend(wordlist_obj.get()) + + final_wordlist = [] + wordlist_threads = [Thread(target=run, args=(wordlist,)) for wordlist in wordlists] + self.wordlist_errors: List[Union[WordlistCreationError, BuildWordlistFails]] = [] + for thread in wordlist_threads: + thread.start() + for thread in wordlist_threads: + thread.join() if not final_wordlist: - raise FuzzControllerException("The wordlist is empty") + raise FuzzLibException("The wordlist is empty") return final_wordlist def __get_default_scanner(self) -> BaseScanner: @@ -373,3 +422,72 @@ def __get_default_scanner(self) -> BaseScanner: return PathScanner() return SubdomainScanner() return DataScanner() + + def __fuzz(self) -> None: + """Prepare the fuzzer for the fuzzing tests""" + self.fuzzer = Fuzzer( + requester=self.requester, + dictionary=self.dictionary, + delay=self.delay, + number_of_threads=self.number_of_threads, + response_callback=self.__handle_response, + exception_callbacks=[ + self._invalid_hostname_callback, + self._request_exception_callback + ], + ) + self.fuzzer.start() + self._join() + + def __handle_response(self, + response: Response, + rtt: float, + payload: Payload, + *ip) -> None: + """Handle the response from the request + + @type response: Response + @param response: The response object from the request + @type rtt: float + @param rtt: The elapsed time between request and response + @type payload: Payload + @param payload: The payload used in the request + """ + if (self.blacklist_status and + response.status_code in self.blacklist_status.codes): + self.blacklist_status.do_action(response.status_code) + result = Result(HttpHistory(response, rtt, *ip), + payload, + self.requester.get_fuzzing_type()) + self.__handle_result(result) + + def __is_valid(self, result: Result) -> bool: + """Checks if the result is valid or not + + @type result: Result + @param result: The FuzzingTool result object + @returns bool: A flag to say if the result is valid or not + """ + if self.filter.check(result) and self.matcher.match(result): + for scanner in self.scanners: + if not scanner.scan(result): + return False + return True + return False + + def __handle_result(self, result: Result) -> bool: + """Process the result + + @type result: Result + @param result: The FuzzingTool result object + """ + if self.__is_valid(result): + for scanner in self.scanners: + scanner.process(result) + if self.has_recursion: + self.recursion_manager.check_for_recursion(result) + self._result_callback(result, True) + if self.replay_proxy: + self.requester.request(result.payload, replay_proxy=True) + else: + self._result_callback(result, False) diff --git a/src/fuzzingtool/fuzzingtool.py b/src/fuzzingtool/fuzzingtool.py index 10ed6ac0..eda9dfba 100644 --- a/src/fuzzingtool/fuzzingtool.py +++ b/src/fuzzingtool/fuzzingtool.py @@ -21,14 +21,14 @@ from .interfaces.cli.cli_arguments import CliArguments from .interfaces.cli.cli_controller import CliController from .interfaces.cli.cli_output import Colors -from .exceptions.main_exceptions import BadArgumentFormat +from .exceptions import BadArgumentFormat def main_cli() -> None: try: arguments = CliArguments().get_arguments() except BadArgumentFormat as e: - exit(str(e).capitalize()) + exit(str(e)) if arguments.disable_colors: Colors.disable() CliController(arguments).main() diff --git a/src/fuzzingtool/interfaces/argument_builder.py b/src/fuzzingtool/interfaces/argument_builder.py deleted file mode 100644 index aaea6993..00000000 --- a/src/fuzzingtool/interfaces/argument_builder.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) 2020 - present Vitor Oriel -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from collections import deque -from typing import List, Tuple, Dict - -from ..utils.consts import FUZZING_MARK -from ..utils.utils import split_str_to_list, parse_option_with_args -from ..utils.file_utils import read_file -from ..exceptions.main_exceptions import BadArgumentFormat - - -class ArgumentBuilder: - @staticmethod - def build_target_from_args( - url: str, - method: str, - body: str - ) -> dict: - """Build the targets from arguments - - @type urls: List[str] - @param urls: The target URLs - @type method: str - @param method: The request methods - @type body: str - @param body: The raw request body data - @returns dict: The targets data builded into a dictionary - """ - methods = split_str_to_list(method) - if not methods: - if body and not ('?' in url or FUZZING_MARK in url): - methods = ['POST'] - else: - methods = ['GET'] - return { - 'url': url, - 'methods': methods, - 'body': body, - 'header': {}, - } - - @staticmethod - def build_target_from_raw_http( - raw_http_filename: str, - scheme: str - ) -> dict: - """Build the targets from raw http files - - @type raw_http_filenames: list - @param raw_http_filenames: The list with the raw http filenames - @type scheme: str - @param scheme: The scheme used in the URL - @returns dict: The target HTTP data builded into a dict - """ - def build_header_from_raw_http( - header_list: deque - ) -> Dict[str, str]: - """Get the HTTP header - - @tyoe header_list: deque - @param header_list: The list with HTTP header - @returns Dict[str, str]: The HTTP header parsed into a dict - """ - headers = {} - i = 0 - this_header = header_list.popleft() - header_length = len(header_list) - while i < header_length and this_header != '': - key, value = this_header.split(': ', 1) - headers[key] = value - this_header = header_list.popleft() - i += 1 - if this_header: - key, value = this_header.split(': ', 1) - headers[key] = value - return headers - - try: - header_list = deque(read_file(raw_http_filename)) - except ValueError: - raise BadArgumentFormat("Invalid header format. E.g. Key: value") - method, path, _ = header_list.popleft().split(' ') - methods = split_str_to_list(method) - headers = build_header_from_raw_http(header_list) - url = f"{scheme}://{headers['Host']}{path}" - if len(header_list) > 0: - body = header_list.popleft() - else: - body = '' - return { - 'url': url, - 'methods': methods, - 'body': body, - 'header': headers, - } - - @staticmethod - def build_wordlist(wordlists: str) -> List[Tuple[str, str]]: - """Build the wordlists - - @type wordlists: str - @param wordlists: The wordlists from command line - @returns List[Tuple[str, str]]: The builded wordlists - """ - return [ - parse_option_with_args(wordlist) - for wordlist in split_str_to_list(wordlists, separator=';') - ] - - @staticmethod - def build_encoder(encoders: str) -> List[List[Tuple[str, str]]]: - """Build the encoders - - @type encoders: str - @param encoders: The encoders from command line - @returns List[List[Tuple[str, str]]]: The builded encoders - """ - return [[ - parse_option_with_args(e) - for e in split_str_to_list(encoder, separator='@')] - for encoder in split_str_to_list(encoders) - ] - - @staticmethod - def build_scanner(scanner: str) -> Tuple[str, str]: - """Build the scanner - - @type scanner: str - @param scanner: The scanner from command line - @returns Tuple[str, str]: The builded scanner - """ - return parse_option_with_args(scanner) - - @staticmethod - def build_verbose_mode(is_common: bool, is_detailed: bool) -> List[bool]: - """Build the verbose mode - - @type is_common: bool - @param is_common: A flag to say if is common verbose mode - @type is_detailed: bool - @param is_detailed: A flag to say if is detailed verbose mode - @returns List[bool]: The builded verbose mode - """ - verbose = [False, False] - if is_common: - verbose = [True, False] - elif is_detailed: - verbose = [True, True] - return verbose - - @staticmethod - def build_blacklist_status(blacklist_status: str) -> Tuple[str, str, str]: - """Build the blacklist_status - - @type blacklist_status: str - @param blacklist_status: The blacklist status from command line - @returns Tuple[str, str, str]: The builded blacklist status - """ - blacklisted_status = blacklist_status - blacklist_action = '' - blacklist_action_param = '' - if ':' in blacklisted_status: - blacklisted_status, blacklist_action = blacklisted_status.split(':', 1) - blacklist_action = blacklist_action.lower() - if '=' in blacklist_action: - blacklist_action, blacklist_action_param = blacklist_action.split('=') - else: - blacklist_action = 'stop' - return (blacklisted_status, blacklist_action, blacklist_action_param) diff --git a/src/fuzzingtool/interfaces/cli/cli_arguments.py b/src/fuzzingtool/interfaces/cli/cli_arguments.py index 12441768..7cd8cca9 100644 --- a/src/fuzzingtool/interfaces/cli/cli_arguments.py +++ b/src/fuzzingtool/interfaces/cli/cli_arguments.py @@ -24,26 +24,29 @@ from .cli_output import CliOutput as CO from ... import __version__ +from ...utils.consts import FUZZ_TYPE_NAME from ...utils.utils import stringfy_list from ...factories.plugin_factory import PluginFactory -from ...reports.report import Report -from ...exceptions.main_exceptions import BadArgumentFormat +from ...persistence.report import Report +from ...exceptions import BadArgumentFormat class CliArguments(argparse.ArgumentParser): """Class to handle with the arguments parsing Overrides the error method from argparse.ArgumentParser, raising an exception instead of exiting - - @type args: List[str] - @param args: The arguments list """ def __init__(self, args: List[str] = None): + """Class constructor + + @type args: List[str] + @param args: The arguments list + """ if args: self.args = args else: self.args = sys.argv - usage = "Usage: FuzzingTool [-u|-r TARGET]+ [-w WORDLIST]+ [options]*" + usage = "Usage: fuzzingtool -u|-r TARGET -w WORDLIST [options]*" examples = ("For usage examples, see: " "https://github.com/NESCAU-UFLA/FuzzingTool/wiki/Usage-Examples") if len(self.args) < 2: @@ -116,10 +119,9 @@ def __show_plugins_help_from_category(self, category: str) -> None: @param category: The package category to search for his plugins """ for plugin_cls in PluginFactory.get_plugins_from_category(category): - if not plugin_cls.__type__: - type_fuzzing = '' - else: - type_fuzzing = f" (Used for {plugin_cls.__type__})" + type_fuzzing = '' + if plugin_cls.__type__ is not None: + type_fuzzing = f" (Fuzz type: {FUZZ_TYPE_NAME[plugin_cls.__type__]})" if not plugin_cls.__params__: params = '' else: @@ -172,7 +174,7 @@ def __build_request_opts(self) -> None: '--scheme', action='store', dest='scheme', - help="Define the scheme used in the URL (default http)", + help="Define the scheme used in the URL (default: http)", metavar='SCHEME', default="http", ) @@ -303,7 +305,8 @@ def __build_match_opts(self) -> None: '-Mc', action='store', dest='match_status', - help="Match responses based on their status codes", + help=("Match responses based on their status codes. " + "Set a list of codes and/or a range, like: 200,300-400,401"), metavar='STATUS', ) match_opts.add_argument( @@ -335,8 +338,29 @@ def __build_match_opts(self) -> None: metavar='QTY_LINES', ) match_opts.add_argument( - '--scanner', + '-Mr', action='store', + dest='match_regex', + help="Match responses based on a regex", + metavar='REGEX', + ) + match_opts.add_argument( + '-Fc', + action='store', + dest='filter_status', + help="Exclude responses based on status code(s), separated by comma if more than one", + metavar='STATUS', + ) + match_opts.add_argument( + '-Fr', + action='store', + dest='filter_regex', + help="Exclude responses based on a regex", + metavar='REGEX', + ) + match_opts.add_argument( + '--scanner', + action='append', dest='scanner', help="Define the custom scanner (--help=scanners for more info)", metavar='SCANNER', @@ -414,7 +438,7 @@ def __build_more_opts(self) -> None: '-t', action='store', dest='threads', - help="Define the number of threads used in the tests", + help="Define the number of threads used in the tests (default: 1)", metavar='NUMBEROFTHREADS', type=int, default=1, @@ -423,7 +447,7 @@ def __build_more_opts(self) -> None: '--delay', action='store', dest='delay', - help="Define delay between each request", + help="Define delay between each request (default: 0)", metavar='DELAY', type=float, default=0, @@ -437,3 +461,26 @@ def __build_more_opts(self) -> None: "wait=SECONDS (to pause the app for some seconds)"), metavar='STATUS:ACTION', ) + more_opts.add_argument( + '--recursive', + action='store_true', + dest='recursive', + help="Set recursive mode for each directory found on path fuzzing", + default=False, + ) + more_opts.add_argument( + '--max-rlevel', + action='store', + dest='max_rlevel', + help="Define the maximum recursion level from jobs (default: 1)", + metavar='RECURSION_LEVEL', + type=int, + default=1, + ) + more_opts.add_argument( + '--replay-proxy', + action='store', + dest='replay_proxy', + help="Define the proxy to replay request when found matched results", + metavar='PROXY', + ) diff --git a/src/fuzzingtool/interfaces/cli/cli_controller.py b/src/fuzzingtool/interfaces/cli/cli_controller.py index 0a103f31..5a9a9a05 100644 --- a/src/fuzzingtool/interfaces/cli/cli_controller.py +++ b/src/fuzzingtool/interfaces/cli/cli_controller.py @@ -22,20 +22,16 @@ import threading from argparse import Namespace -from fuzzingtool.objects.payload import Payload - from .cli_output import CliOutput, Colors -from ..argument_builder import ArgumentBuilder as AB from ... import __version__ -from ...api.fuzz_controller import FuzzController -from ...core.bases.base_plugin import Plugin -from ...utils.http_utils import get_host, get_pure_url -from ...utils.logger import Logger -from ...reports.report import Report -from ...objects import Error, Result +from ...fuzz_lib import FuzzLib +from ...utils.argument_utils import build_verbose_mode +from ...utils.consts import FUZZ_TYPE_NAME +from ...utils.http_utils import get_parsed_url, get_pure_url +from ...persistence import Logger, Report +from ...objects import BaseItem, Error, Payload, Result, HttpHistory +from ...exceptions import FuzzLibException, StopActionInterrupt, RequestException from ...exceptions.base_exceptions import FuzzingToolException -from ...exceptions.main_exceptions import FuzzControllerException, StopActionInterrupt -from ...exceptions.request_exceptions import RequestException def banner() -> str: @@ -54,16 +50,13 @@ def banner() -> str: return banner -class CliController(FuzzController): - """Class that handle with the entire application +class CliController(FuzzLib): + """Class that handle with the CLI application Attributes: - requesters: The requesters list - started_time: The time when start the fuzzing test - fuzzer: The fuzzer object to handle with the fuzzing test lock: A thread locker to prevent overwrites on logfiles - blacklist_status: The blacklist status object logger: The object to handle with the program log + cli_output: The object that handles the terminal output """ def __init__(self, arguments: Namespace): super().__init__(**vars(arguments)) @@ -90,7 +83,6 @@ def main(self) -> None: self.cli_output.error_box(str(e)) if not self.args["simple_output"]: self.print_configs() - self.cli_output.set_verbosity_mode(self.is_verbose_mode()) try: self.check_connection() self.prepare() @@ -107,7 +99,7 @@ def init(self) -> None: """The initialization function. Set the application variables including plugins requires """ - self.verbose = AB.build_verbose_mode( + self.verbose = build_verbose_mode( self.args["common_verbose"], self.args["detailed_verbose"] ) @@ -119,10 +111,10 @@ def print_configs(self) -> None: self.cli_output.print_configs( target={ 'url': self.requester.get_url(), - 'methods': [method for method in self.requester.methods], + 'method': self.requester.get_method(), 'header': 'custom' if self.args["raw_http"] else 'default', 'body': self.args["data"], - 'type_fuzzing': self.__get_target_fuzzing_type(), + 'type_fuzzing': FUZZ_TYPE_NAME[self.requester.get_fuzzing_type()], }, dictionary=self.dict_metadata ) @@ -139,7 +131,7 @@ def check_connection(self) -> None: except RequestException as e: if not self.cli_output.ask_yes_no('warning', f"{str(e)}. Continue anyway?"): - raise FuzzControllerException("No target left for fuzzing") + raise FuzzLibException("No target left for fuzzing") else: if self.is_verbose_mode(): self.cli_output.info_box("Connection status: OK") @@ -149,58 +141,19 @@ def start(self) -> None: Each target is fuzzed based on their own methods list """ self.cli_output.info_box("Start fuzzing on " - + get_host(get_pure_url(self.requester.get_url()))) + + get_parsed_url(get_pure_url(self.requester.get_url())).hostname) try: super().start() except StopActionInterrupt as e: self.cli_output.abort_box(f"{str(e)}. Program stopped.") - else: - if not self.is_verbose_mode(): - CliOutput.print("") - - def fuzzer_join(self): - while self.fuzzer.is_running(): - try: - super().fuzzer_join() - except KeyboardInterrupt: - self.cli_output.warning_box("Ctrl+C detected, pausing threads ...") - self.handle_pause() - - def handle_pause(self): - """Handle with the Ctrl+C pause""" - self.fuzzer.pause() - self.fuzzer.wait_until_pause() - self.summary.pause_timer() - if not self.is_verbose_mode(): - CliOutput.print("") - answer = '' - while answer not in ['q', 'c']: - try: - answer = self.cli_output.ask_data("[c]ontinue | [s]tatus | [q]uit") - except KeyboardInterrupt: - answer = 'q' - if answer == "q": - self.fuzzer.stop() - self.cli_output.abort_box("Test aborted by the user") - elif answer == 's': - str_percentage = self.cli_output.get_percentage( - self.last_index, - self.total_requests - ) - self.cli_output.info_box( - f"Progress: {Colors.LIGHT_YELLOW}{str_percentage}{Colors.RESET} completed" - ) - elif answer == "c": - self.summary.resume_timer() - self.fuzzer.resume() def prepare(self) -> None: """Prepare the application before the fuzzing""" - self.target_host = get_host(get_pure_url(self.requester.get_url())) + self.target_host = get_parsed_url(get_pure_url(self.requester.get_url())).hostname if self.is_verbose_mode(): self.cli_output.info_box(f"Preparing target {self.target_host} ...") self.check_ignore_errors() - if (not isinstance(self.scanner, Plugin) and + if (len(self.scanners) == 1 and (self.requester.is_data_fuzzing() and not self.matcher.comparator_is_set())): self.cli_output.info_box("DataFuzzing detected, checking for a data comparator ...") @@ -243,34 +196,31 @@ def _wait_callback(self, status: int) -> None: f"Status code {str(status)} detected. Pausing threads ..." ) self.fuzzer.wait_until_pause() - if not self.is_verbose_mode(): - CliOutput.print("") self.cli_output.info_box( f"Waiting for {self.blacklist_status.action_param} seconds ..." ) time.sleep(self.blacklist_status.action_param) - self.cli_output.info_box("Resuming target ...") + self.cli_output.info_box("Resuming job ...") self.fuzzer.resume() - def _result_callback(self, result: Result, validate: bool) -> None: + def _result_callback(self, result: Result, valid: bool) -> None: if self.verbose[0]: - if validate: + if valid: self.summary.results.append(result) - self.cli_output.print_result(result, validate) + self.cli_output.print_result(result, valid) else: - if validate: + if valid: self.summary.results.append(result) - self.cli_output.print_result(result, validate) - self.cli_output.progress_status( - result.index, self.total_requests, result.payload + self.cli_output.print_result(result, valid) + self.__print_progress( + result.index, result.payload ) - self.last_index = result.index def _request_exception_callback(self, error: Error) -> None: if self.ignore_errors: if not self.verbose[0]: - self.cli_output.progress_status( - error.index, self.total_requests, error.payload + self.__print_progress( + error.index, error.payload ) else: if self.verbose[1]: @@ -279,17 +229,15 @@ def _request_exception_callback(self, error: Error) -> None: self.logger.write(str(error), error.payload) else: self.stop_action = str(error) - self.last_index = error.index def _invalid_hostname_callback(self, error: Error) -> None: if self.verbose[0]: if self.verbose[1]: self.cli_output.not_worked_box(str(error)) else: - self.cli_output.progress_status( - error.index, self.total_requests, error.payload + self.__print_progress( + error.index, error.payload ) - self.last_index = error.index def _init_dictionary(self) -> None: try: @@ -299,6 +247,65 @@ def _init_dictionary(self) -> None: for e in self.wordlist_errors: self.cli_output.warning_box(str(e)) + def _get_job(self) -> None: + super()._get_job() + self.cli_output.set_new_job(self.job_manager.total_requests) + + def _join(self) -> None: + """Blocks until the fuzzer is running""" + while self.fuzzer.is_running(): + try: + super()._join() + except KeyboardInterrupt: + self.cli_output.warning_box("Ctrl+C detected, pausing threads ...") + self._handle_pause() + + def _handle_pause(self) -> None: + """Handle with the Ctrl+C pause""" + self.fuzzer.pause() + self.fuzzer.wait_until_pause() + self.summary.pause_timer() + options = "[c]ontinue | [p]rogress | [q]uit" + if (self.job_manager.has_pending_jobs() + or self.job_manager.has_pending_jobs_from_providers() + or self.recursion_manager.has_recursive_job()): + options += " | [s]kip" + answer = '' + while answer not in ['q', 'c', 's']: + try: + answer = self.cli_output.ask_data(options) + except KeyboardInterrupt: + answer = 'q' + if answer == 'q': + self._handle_quit() + elif answer == 'p': + self._handle_progress() + elif answer == "c": + self._handle_continue() + elif answer == 's': + self._handle_skip() + + def _handle_quit(self) -> None: + """Handle with the quit option when pause""" + raise StopActionInterrupt("Test aborted") + + def _handle_progress(self) -> None: + """Handle with the progress option when pause""" + str_percentage = self.cli_output.get_percentage(BaseItem.index) + self.cli_output.info_box( + f"Progress: {Colors.LIGHT_YELLOW}{str_percentage}{Colors.RESET} completed" + ) + + def _handle_continue(self) -> None: + """Handle with the continue option when pause""" + self.summary.resume_timer() + self.fuzzer.resume() + + def _handle_skip(self) -> None: + """Handle with the skip option when pause""" + self.fuzzer.stop() + self.cli_output.abort_box(f"Current job ({self.job_manager.current_job}) skipped") + def __init_report(self) -> None: """Initialize the report""" self.report = Report.build(self.args["report_name"]) @@ -306,21 +313,6 @@ def __init_report(self) -> None: Result.save_headers = self.args["save_headers"] Result.save_body = self.args["save_body"] - def __get_target_fuzzing_type(self) -> str: - """Get the target fuzzing type, as a string format - - @return str: The fuzzing type, as a string - """ - if self.requester.is_method_fuzzing(): - return "MethodFuzzing" - if self.requester.is_data_fuzzing(): - return "DataFuzzing" - if self.requester.is_url_discovery(): - if self.requester.is_path_fuzzing(): - return "PathFuzzing" - return "SubdomainFuzzing" - return "Couldn't determine the fuzzing type" - def __get_comparator_value(self, name_value: str, ask_message: str) -> str: @@ -356,7 +348,7 @@ def __get_data_comparator(self) -> tuple: response, rtt = self.requester.request(payload) except RequestException as e: raise StopActionInterrupt(str(e)) - result_to_comparator = Result(response, rtt, Payload(payload)) + result_to_comparator = Result(HttpHistory(response, rtt), Payload(payload)) self.cli_output.print_result(result_to_comparator, False) time = self.__get_comparator_value( name_value="RTT", @@ -376,6 +368,21 @@ def __get_data_comparator(self) -> tuple: ) return (time, length, words, lines) + def __print_progress(self, index: int, payload: str) -> None: + """Print the progress status + + @type index: int + @param index: The actual item index + @type payload: str + @param payload: The payload used in the previous request + """ + self.cli_output.progress_status( + item_index=index, + payload=payload, + current_job=self.job_manager.current_job, + total_jobs=self.job_manager.total_jobs, + ) + def __handle_valid_results(self, host: str, results: list) -> None: diff --git a/src/fuzzingtool/interfaces/cli/cli_output.py b/src/fuzzingtool/interfaces/cli/cli_output.py index 8f780cc5..e9eb475a 100644 --- a/src/fuzzingtool/interfaces/cli/cli_output.py +++ b/src/fuzzingtool/interfaces/cli/cli_output.py @@ -22,11 +22,13 @@ import threading import sys from typing import Tuple +from math import floor, ceil, log10 +from shutil import get_terminal_size from ...objects.result import Result -from ...utils.consts import MAX_PAYLOAD_LENGTH_TO_OUTPUT, PATH_FUZZING, SUBDOMAIN_FUZZING -from ...utils.utils import stringfy_list, fix_payload_to_output -from ...utils.http_utils import get_path, get_host, get_pure_url +from ...utils.consts import MAX_PAYLOAD_LENGTH_TO_OUTPUT, FuzzType +from ...utils.utils import fix_payload_to_output +from ...utils.http_utils import get_parsed_url, get_pure_url from ...utils.result_utils import ResultUtils @@ -47,7 +49,7 @@ class Colors: BOLD = '\033[1m' @staticmethod - def disable(): + def disable() -> None: """Disable the colors of the program""" Colors.RESET = '' Colors.GRAY = '' @@ -125,7 +127,6 @@ def help_content(num_spaces: int, command: str, desc: str) -> None: def __init__(self): self.__lock = threading.Lock() - self.__break_line = '' self.__last_inline = False self.__info = f'{Colors.GRAY}[{Colors.BLUE_GRAY}INFO{Colors.GRAY}]{Colors.RESET} ' self.__warning = f'{Colors.GRAY}[{Colors.YELLOW}WARNING{Colors.GRAY}]{Colors.RESET} ' @@ -147,16 +148,17 @@ def get_blank_time() -> str: self.__worked = f'{Colors.GRAY}[{Colors.GREEN}+{Colors.GRAY}]{Colors.RESET} ' self.__not_worked = f'{Colors.GRAY}[{Colors.RED}-{Colors.GRAY}]{Colors.RESET} ' - def set_verbosity_mode(self, verbose_mode: bool) -> None: - """Set the verbosity mode + def set_new_job(self, total_requests: int) -> None: + """Set the variables from job manager - @type verbose_mode: bool - @param verbose_mode: The verbose mode flag + @type total_requests: int + @param total_requests: The number of requests that'll be made """ - if verbose_mode: - self.__break_line = '' - else: - self.__break_line = '\n' + self.__total_requests = total_requests + self.__request_indent = ceil(log10(total_requests)) + self.__progress_length = (38 # Progress bar, spaces, square brackets and slashes + + MAX_PAYLOAD_LENGTH_TO_OUTPUT + + self.__request_indent * 2) def info_box(self, msg: str) -> None: """Print the message with a info label @@ -164,7 +166,7 @@ def info_box(self, msg: str) -> None: @type msg: str @param msg: The message """ - print(f'{self.__get_time()}{self.__get_info(msg)}') + print(f'{self._get_break()}{self.__get_time()}{self.__get_info(msg)}') def error_box(self, msg: str) -> None: """End the application with error label and a message @@ -172,7 +174,7 @@ def error_box(self, msg: str) -> None: @type msg: str @param msg: The message """ - exit(f'{self.__get_time()}{self.__get_error(msg)}') + exit(f'{self._get_break()}{self.__get_time()}{self.__get_error(msg)}') def warning_box(self, msg: str) -> None: """Print the message with a warning label @@ -182,8 +184,7 @@ def warning_box(self, msg: str) -> None: """ with self.__lock: sys.stdout.flush() - print(f'{self.__break_line}' - f'{self.__get_time()}{self.__get_warning(msg)}') + print(f'{self._get_break()}{self.__get_time()}{self.__get_warning(msg)}') def abort_box(self, msg: str) -> None: """Print the message with abort label and a message @@ -193,8 +194,7 @@ def abort_box(self, msg: str) -> None: """ with self.__lock: sys.stdout.flush() - print(f'{self.__break_line}' - f'{self.__get_time()}{self.__get_abort(msg)}') + print(f'{self._get_break()}{self.__get_time()}{self.__get_abort(msg)}') def worked_box(self, msg: str) -> None: """Print the message with worked label and a message @@ -226,7 +226,7 @@ def ask_yes_no(self, ask_type: str, msg: str) -> bool: get_type = self.__get_warning else: get_type = self.__get_info - print(f"{self.__get_time()}{get_type(msg)} (y/N) ", end='') + print(f"{self._get_break()}{self.__get_time()}{get_type(msg)} (y/N) ", end='') action = input() if action == 'y' or action == 'Y': return True @@ -239,7 +239,7 @@ def ask_data(self, msg: str) -> str: @param msg: The message @returns mixed: The data asked """ - print(self.__get_time()+self.__get_info(msg), end=': ') + print(f"{self._get_break()}{self.__get_time()}{self.__get_info(msg)}", end=': ') return input() def print_config(self, key: str, value: str = '', spaces: int = 0) -> None: @@ -267,10 +267,8 @@ def print_configs(self, """ print("") spaces = 3 - self.print_config("Target", get_host(get_pure_url(target['url']))) - self.print_config("Methods", - stringfy_list(target['methods']), - spaces) + self.print_config("Target", get_parsed_url(get_pure_url(target['url'])).hostname) + self.print_config("Method", target['method'], spaces) self.print_config("HTTP headers", target['header'], spaces) if target['body']: self.print_config("Body data", target['body'], spaces) @@ -283,44 +281,46 @@ def print_configs(self, self.print_config("Dictionary size", dict_size) print("") - def get_percentage(self, item_index: int, total_requests: int) -> str: - """Get the percentage from item_index / total_requests + def get_percentage(self, item_index: int) -> str: + """Get the percentage string from item_index per total_requests @type item_index: int @param item_index: The actual request index - @type total_requests: int - @param total_requests: The total of requests quantity @returns str: The percentage str """ - return f"{str(int((int(item_index)/total_requests)*100))}%" + return f"{self._get_percentage_value(item_index, self.__total_requests)}%" def progress_status(self, item_index: int, - total_requests: int, - payload: str) -> None: + payload: str, + current_job: int, + total_jobs: int) -> None: """Output the progress status of the fuzzing @type item_index: int @param item_index: The actual request index - @type total_requests: int - @param total_requests: The total of requests quantity @type payload: str @param payload: The payload used in the request """ - status = (f"{Colors.GRAY}[{Colors.LIGHT_GRAY}{item_index}" - + f"{Colors.GRAY}/{Colors.LIGHT_GRAY}{total_requests}" - + f"{Colors.GRAY}]{Colors.RESET} {Colors.LIGHT_YELLOW}" - + self.get_percentage(item_index, total_requests) - + f"{Colors.RESET}") - payload = fix_payload_to_output(payload) - while len(payload) < MAX_PAYLOAD_LENGTH_TO_OUTPUT: - payload += ' ' - with self.__lock: - if not self.__last_inline: - self.__last_inline = True - self.__erase_line() - print(f"\r{self.__get_time()}{status}" - f"{Colors.GRAY} :: {Colors.LIGHT_GRAY}{payload}", end='') + jobs_indent = ceil(log10(total_jobs)) + progress_length = self.__progress_length + (2 * jobs_indent) + if progress_length <= get_terminal_size()[0]: + percentage_value = self._get_percentage_value(item_index, self.__total_requests) + status = self._get_progress_bar(percentage_value) + payload = fix_payload_to_output(payload) + status += (f" {Colors.LIGHT_YELLOW}{percentage_value:>3}% {Colors.RESET}" + + f"{Colors.GRAY}[{Colors.LIGHT_GRAY}{item_index:>{self.__request_indent}}" + + f"{Colors.GRAY}/{Colors.LIGHT_GRAY}{self.__total_requests}" + + f"{Colors.GRAY}]{Colors.RESET} " + + f"{Colors.GRAY}[{Colors.LIGHT_GRAY}{current_job:>{jobs_indent}}" + + f"{Colors.GRAY}/{Colors.LIGHT_GRAY}{total_jobs}" + + f"{Colors.GRAY}]{Colors.RESET}") + status += f"{Colors.GRAY} :: {Colors.LIGHT_GRAY}{payload:<{MAX_PAYLOAD_LENGTH_TO_OUTPUT}}" + with self.__lock: + if not self.__last_inline: + self.__last_inline = True + self.__erase_line() + print(f"\r{status}", end='') def print_result(self, result: Result, vuln_validator: bool) -> None: """Custom output print for box mode @@ -340,6 +340,40 @@ def print_result(self, result: Result, vuln_validator: bool) -> None: self.__erase_line() self.worked_box(formatted_result_str) + def _get_break(self) -> str: + """Get a break line if the last message was inline + + @returns str: The break line + """ + if self.__last_inline: + self.__last_inline = False + return '\n' + return '' + + def _get_percentage_value(self, item_index: int, total_requests: int) -> int: + """Get the percentage from item_index per total_requests + + @type item_index: int + @param item_index: The actual request index + @type total_requests: int + @param total_requests: The total of requests quantity + @returns int: The percentage value + """ + return int((item_index/total_requests)*100) + + def _get_progress_bar(self, percentage_value: int) -> str: + """Get a formated progress bar + + @type percentage_value: int + @param percentage_value: The percentage value of progress status + @returns str: The formated progress bar + """ + bar_size = floor(percentage_value/5) + spaces = 20-bar_size + return (f"{Colors.GRAY}[" + f"{Colors.LIGHT_GREEN}{Colors.BOLD}{'#'*bar_size}{Colors.RESET}{' '*spaces}" + f"{Colors.GRAY}]{Colors.RESET}") + def __get_time(self) -> str: """Get a time label @@ -417,14 +451,13 @@ def __get_formatted_payload(self, result: Result) -> str: @param result: The result of the request @returns str: The formatted payload to output """ - if result.fuzz_type == PATH_FUZZING: - try: - formatted_payload = get_path(result.url) - except ValueError: - formatted_payload = result.url + if result.fuzz_type == FuzzType.PATH_FUZZING: + formatted_payload = result.history.parsed_url.path + if not formatted_payload: + return result.history.url return formatted_payload - if result.fuzz_type == SUBDOMAIN_FUZZING: - return get_host(result.url) + if result.fuzz_type == FuzzType.SUBDOMAIN_FUZZING: + return result.history.parsed_url.hostname return result.payload def __get_formatted_status(self, status: int) -> str: @@ -460,10 +493,10 @@ def __get_formatted_result_items(self, result: Result) -> Tuple[ @returns Tuple[str, str, str, str, str, str]: The tuple with the formatted result items """ payload, rtt, length, words, lines = ResultUtils.get_formatted_result( - self.__get_formatted_payload(result), result.rtt, - result.body_size, result.words, result.lines + self.__get_formatted_payload(result), result.history.rtt, + result.history.body_size, result.words, result.lines ) - return (payload, self.__get_formatted_status(result.status), rtt, length, words, lines) + return (payload, self.__get_formatted_status(result.history.status), rtt, length, words, lines) def __get_formatted_result(self, result: Result) -> str: """Format the entire result message @@ -482,11 +515,5 @@ def __get_formatted_result(self, result: Result) -> str: f"{Colors.LIGHT_GRAY}Words{Colors.RESET} {words} | " f"{Colors.LIGHT_GRAY}Lines{Colors.RESET} {lines}{Colors.GRAY}]{Colors.RESET}" ) - if result.custom: - custom_str = '' - for key, value in result.custom.items(): - if (value is not None and isinstance(value, bool)) or value: - custom_str += (f"\n{Colors.LIGHT_YELLOW}|_ {key}: " - f"{ResultUtils.format_custom_field(value)}{Colors.RESET}") - formatted_result_str += custom_str + formatted_result_str += f"{Colors.LIGHT_YELLOW}{result.get_description()}{Colors.RESET}" return formatted_result_str diff --git a/src/fuzzingtool/objects/__init__.py b/src/fuzzingtool/objects/__init__.py index eb8a50c4..9376be71 100644 --- a/src/fuzzingtool/objects/__init__.py +++ b/src/fuzzingtool/objects/__init__.py @@ -18,6 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from .base_objects import BaseItem from .error import Error +from .http_history import HttpHistory from .payload import Payload from .result import Result +from .scanner_result import ScannerResult diff --git a/src/fuzzingtool/objects/base_objects.py b/src/fuzzingtool/objects/base_objects.py index c82bed43..132fccaf 100644 --- a/src/fuzzingtool/objects/base_objects.py +++ b/src/fuzzingtool/objects/base_objects.py @@ -28,12 +28,15 @@ class BaseItem(ABC): Attributes: index: The index of the item """ - index = count(1) + index = 1 + _index = count(1) @staticmethod def reset_index() -> None: """Resets the item index""" - BaseItem.index = count(1) + BaseItem.index = 1 + BaseItem._index = count(1) def __init__(self): - self.index = next(BaseItem.index) + self.index = next(BaseItem._index) + BaseItem.index = self.index diff --git a/src/fuzzingtool/objects/http_history.py b/src/fuzzingtool/objects/http_history.py new file mode 100644 index 00000000..4ce6efc6 --- /dev/null +++ b/src/fuzzingtool/objects/http_history.py @@ -0,0 +1,96 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from requests.models import PreparedRequest, Response + +from ..utils.http_utils import build_raw_response_header, UrlParse, get_parsed_url + + +class HttpHistory: + """Class that stores the HTTP information from the ressult + + Attributes: + url: The requested target URL + method: The method used in the request + rtt: The elapsed time on both request and response + status: The response HTTP status code + body_size: The length of the response body content + ip: The IP from the request, if provided + response: The Response object from python requests + """ + def __init__(self, response: Response, rtt: float = 0.0, *ip): + """Class constructor + + @type response: Response + @param response: The response given in the request + @type rtt: float + @param rtt: The elapsed time on both request and response + """ + self.url = response.url + self.method = response.request.method + self.rtt = float('%.6f' % (rtt)) + self.status = response.status_code + self.body_size = len(response.content) + self.ip = ip[0] if ip else '' + self.__response = response + + @property + def parsed_url(self) -> UrlParse: + return get_parsed_url(self.url) + + @property + def is_path(self) -> bool: + """Checks if the url path is a directory + + @returns bool: A flag to say if it's a directory path + """ + return self.url[-1] == '/' + + @property + def raw_headers(self) -> str: + return build_raw_response_header(self.__response) + + @property + def headers_length(self) -> int: + return len(self.raw_headers) + + @property + def response_time(self) -> float: + return self.__response.elapsed.total_seconds() + + @property + def request_time(self) -> float: + return float('%.6f' % (self.rtt - self.response_time)) + + @property + def request(self) -> PreparedRequest: + """The request getter + + @returns Request: The request object + """ + return self.__response.request + + @property + def response(self) -> Response: + """The response getter + + @returns Response: The response of the request + """ + return self.__response diff --git a/src/fuzzingtool/objects/payload.py b/src/fuzzingtool/objects/payload.py index 527907d8..bd2fc750 100644 --- a/src/fuzzingtool/objects/payload.py +++ b/src/fuzzingtool/objects/payload.py @@ -27,6 +27,7 @@ class Payload: Attributes: raw: The payload before the mutation final: The payloa after the mutation + rlevel: The recursion level of the payload config: The config of the payload mutation """ def __init__(self, payload: str = ''): @@ -35,14 +36,15 @@ def __init__(self, payload: str = ''): @type payload: str @param payload: The payload that'll be mutated """ - self.raw = payload - self.final = payload + self.raw: str = payload + self.final: str = payload + self.rlevel = 0 self.config = {} def __str__(self) -> str: return self.final - def update(self, other: object) -> object: + def update(self, other: 'Payload') -> 'Payload': """Update the base payload from another payload @type other: Payload @@ -51,10 +53,11 @@ def update(self, other: object) -> object: """ self.raw = other.raw self.final = other.final + self.rlevel = other.rlevel self.config = {key: value for key, value in other.config.items()} return self - def with_prefix(self, prefix: str) -> object: + def with_prefix(self, prefix: str) -> 'Payload': """Build the payload with prefix @type prefix: str @@ -65,7 +68,7 @@ def with_prefix(self, prefix: str) -> object: self.final = prefix + self.final return self - def with_suffix(self, suffix: str) -> object: + def with_suffix(self, suffix: str) -> 'Payload': """Build the payload with suffix @type suffix: str @@ -76,7 +79,7 @@ def with_suffix(self, suffix: str) -> object: self.final += suffix return self - def with_case(self, case_callback: Callable, case_method: str) -> object: + def with_case(self, case_callback: Callable, case_method: str) -> 'Payload': """Build the payload with case (upper, lower, cap) @type case_callback: Callable @@ -89,7 +92,7 @@ def with_case(self, case_callback: Callable, case_method: str) -> object: self.final = case_callback(self.final) return self - def with_encoder(self, encoded: str, encoder: str) -> object: + def with_encoder(self, encoded: str, encoder: str) -> 'Payload': """Build the payload with an encoder @type encoded: str @@ -101,3 +104,15 @@ def with_encoder(self, encoded: str, encoder: str) -> object: self.config['encoder'] = encoder self.final = encoded return self + + def with_recursion(self, payload: str) -> 'Payload': + """Build the payload recursively + + @type payload: str + @param payload: The payload that'll be send to recursion + @returns Payload: The updated payload + """ + self.config[f'rlevel_{self.rlevel}'] = self.final + self.rlevel += 1 + self.final = payload + return self diff --git a/src/fuzzingtool/objects/result.py b/src/fuzzingtool/objects/result.py index 6b9dac98..4d31ac3a 100644 --- a/src/fuzzingtool/objects/result.py +++ b/src/fuzzingtool/objects/result.py @@ -20,12 +20,10 @@ from typing import Iterator, Tuple -from requests import Response - from .base_objects import BaseItem +from .http_history import HttpHistory from .payload import Payload -from ..utils.consts import UNKNOWN_FUZZING -from ..utils.http_utils import build_raw_response_header +from ..utils.consts import FuzzType from ..utils.result_utils import ResultUtils @@ -33,106 +31,98 @@ class Result(BaseItem): """The FuzzingTool result object Attributes: + history: The HTTP history of the result payload: The string payload used in the request - url: The requested target URL - method: The method used in the request - rtt: The elapsed time on both request and response - request_time: The elapsed time only for the request - response_time: The elapsed time only for the response - status: The response HTTP status code - headers: The response raw HTTP headers - headers_length: The length of the raw HTTP headers - body_size: The length of the response body content words: The quantitty of words in the response body lines: The quantity of lines in the response body - custom: A dictionary to store custom data from the scanners + scanners_res: The results dict provided from the scanners _payload: The Payload object - response: The Response object from python requests """ save_payload_configs = False save_headers = False save_body = False def __init__(self, - response: Response, - rtt: float = 0.0, + history: HttpHistory, payload: Payload = Payload(), - fuzz_type: int = UNKNOWN_FUZZING): + fuzz_type: int = FuzzType.UNKNOWN_FUZZING): """Class constructor - @type response: Response - @param response: The response given in the request - @type rtt: float - @param rtt: The elapsed time on both request and response + @type history: HttpHistory + @param history: The HTTP history of this result @type payload: Payload @param payload: The payload used in the request + @type fuzz_type: int + @param fuzz_type: The request fuzzing type """ super().__init__() + self.history = history self.payload = payload.final - self.url = response.url - self.method = response.request.method - self.rtt = float('%.6f' % (rtt)) - response_time = response.elapsed.total_seconds() - self.request_time = float('%.6f' % (rtt-response_time)) - self.response_time = response_time - self.status = response.status_code - content = response.content - self.headers = build_raw_response_header(response) - self.headers_length = len(self.headers) - self.body_size = len(content) + content = self.history.response.content self.words = len(content.split()) self.lines = content.count(b'\n') self.fuzz_type = fuzz_type - self.custom = {} + self.job_description = '' + self.scanners_res = {} self._payload = payload - self.__response = response def __str__(self) -> str: payload, rtt, length, words, lines = ResultUtils.get_formatted_result( - self.payload, self.rtt, self.body_size, + self.payload, self.history.rtt, self.history.body_size, self.words, self.lines ) returned_str = ( f"{payload} [" - f"Code {self.status} | " + f"Code {self.history.status} | " f"RTT {rtt} | " f"Size {length} | " f"Words {words} | " f"Lines {lines}]" ) - for key, value in self.custom.items(): - if value is not None: - returned_str += (f"\n|_ {key}: " - f"{ResultUtils.format_custom_field(value)}") + returned_str += self.get_description() return returned_str def __iter__(self) -> Iterator[Tuple]: yield 'index', self.index - yield 'url', self.url - yield 'method', self.method - yield 'rtt', self.rtt - yield 'request_time', self.request_time - yield 'response_time', self.response_time - yield 'status', self.status - yield 'headers_length', self.headers_length - yield 'body_size', self.body_size + yield 'url', self.history.url + yield 'method', self.history.method + yield 'rtt', self.history.rtt + yield 'request_time', self.history.request_time + yield 'response_time', self.history.response_time + yield 'status', self.history.status + yield 'headers_length', self.history.headers_length + yield 'body_size', self.history.body_size yield 'words', self.words yield 'lines', self.lines - for key, value in self.custom.items(): - yield key, ResultUtils.format_custom_field(value, force_detailed=True) + if self.history.ip: + yield 'ip', self.history.ip + for s_res in self.scanners_res.values(): + for key, value in s_res.data.items(): + yield key, ResultUtils.format_custom_field(value, force_detailed=True) yield 'payload', self.payload if Result.save_payload_configs: yield 'payload_raw', self._payload.raw for key, value in self._payload.config.items(): yield f"payload_{key}", value if Result.save_headers: - yield 'headers', self.headers + yield 'headers', self.history.raw_headers if Result.save_body: - yield 'body', self.__response.text + yield 'body', self.history.response.text - def get_response(self) -> Response: - """The response getter + def get_description(self) -> str: + """Get the description from the result - @returns Response: The response of the request + @returns str: The job description and scanners descriptions """ - return self.__response + description = '' + if self.job_description: + description += f"\n|_ {self.job_description}" + for scanner, s_res in self.scanners_res.items(): + for key, value in s_res.data.items(): + if (value is not None and isinstance(value, bool)) or value: + description += (f"\n|_ {key}: " + f"{ResultUtils.format_custom_field(value)}") + if s_res.enqueued_payloads: + description += (f"\n|_ Scanner {scanner} enqueued " + f"{s_res.enqueued_payloads} payloads") + return description diff --git a/src/fuzzingtool/objects/scanner_result.py b/src/fuzzingtool/objects/scanner_result.py new file mode 100644 index 00000000..ef1e94fc --- /dev/null +++ b/src/fuzzingtool/objects/scanner_result.py @@ -0,0 +1,37 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +class ScannerResult: + """Class to store the information provided from scanners + + Attributes: + source: The scanner source name + data: The data provided from the scanner + enqueued_payloads: The quantity of enqueued payloads by the scanner + """ + def __init__(self, scanner_name: str): + """Class constructor + + @type scanner_name: str + @param scanner_name: The name of the scanner that created this result + """ + self.source = scanner_name + self.data = {} + self.enqueued_payloads = 0 diff --git a/src/fuzzingtool/api/__init__.py b/src/fuzzingtool/persistence/__init__.py similarity index 95% rename from src/fuzzingtool/api/__init__.py rename to src/fuzzingtool/persistence/__init__.py index bec22f99..c25c1f87 100644 --- a/src/fuzzingtool/api/__init__.py +++ b/src/fuzzingtool/persistence/__init__.py @@ -18,4 +18,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .api import fuzz, fuzz_cli +from .logger import Logger +from .report import Report diff --git a/src/fuzzingtool/reports/base_report.py b/src/fuzzingtool/persistence/base_report.py similarity index 100% rename from src/fuzzingtool/reports/base_report.py rename to src/fuzzingtool/persistence/base_report.py diff --git a/src/fuzzingtool/utils/logger.py b/src/fuzzingtool/persistence/logger.py similarity index 98% rename from src/fuzzingtool/utils/logger.py rename to src/fuzzingtool/persistence/logger.py index 0da955bd..004afdc8 100644 --- a/src/fuzzingtool/utils/logger.py +++ b/src/fuzzingtool/persistence/logger.py @@ -21,7 +21,7 @@ from datetime import datetime from pathlib import Path -from .consts import OUTPUT_DIRECTORY +from ..utils.consts import OUTPUT_DIRECTORY class Logger: diff --git a/src/fuzzingtool/reports/report.py b/src/fuzzingtool/persistence/report.py similarity index 98% rename from src/fuzzingtool/reports/report.py rename to src/fuzzingtool/persistence/report.py index ed74346f..fb9e7374 100644 --- a/src/fuzzingtool/reports/report.py +++ b/src/fuzzingtool/persistence/report.py @@ -24,7 +24,7 @@ from . import reports from .base_report import BaseReport from ..utils.utils import stringfy_list -from ..exceptions.main_exceptions import InvalidArgument +from ..exceptions import InvalidArgument def get_report_name_and_type(name: str) -> Tuple[str, str]: diff --git a/src/fuzzingtool/reports/reports/__init__.py b/src/fuzzingtool/persistence/reports/__init__.py similarity index 100% rename from src/fuzzingtool/reports/reports/__init__.py rename to src/fuzzingtool/persistence/reports/__init__.py diff --git a/src/fuzzingtool/reports/reports/csv_report.py b/src/fuzzingtool/persistence/reports/csv_report.py similarity index 100% rename from src/fuzzingtool/reports/reports/csv_report.py rename to src/fuzzingtool/persistence/reports/csv_report.py diff --git a/src/fuzzingtool/reports/reports/json_report.py b/src/fuzzingtool/persistence/reports/json_report.py similarity index 100% rename from src/fuzzingtool/reports/reports/json_report.py rename to src/fuzzingtool/persistence/reports/json_report.py diff --git a/src/fuzzingtool/reports/reports/txt_report.py b/src/fuzzingtool/persistence/reports/txt_report.py similarity index 100% rename from src/fuzzingtool/reports/reports/txt_report.py rename to src/fuzzingtool/persistence/reports/txt_report.py diff --git a/src/fuzzingtool/utils/argument_utils.py b/src/fuzzingtool/utils/argument_utils.py new file mode 100644 index 00000000..2c391157 --- /dev/null +++ b/src/fuzzingtool/utils/argument_utils.py @@ -0,0 +1,174 @@ +# Copyright (c) 2020 - present Vitor Oriel +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from collections import deque +from typing import List, Tuple, Dict + +from .consts import FUZZING_MARK +from .utils import split_str_to_list, parse_option_with_args +from .file_utils import read_file +from ..exceptions import BadArgumentFormat + + +def build_target_from_args(url: str, method: str, body: str) -> dict: + """Build the targets from arguments + + @type urls: List[str] + @param urls: The target URLs + @type method: str + @param method: The request method + @type body: str + @param body: The raw request body data + @returns dict: The targets data builded into a dictionary + """ + if not method: + if body and not ('?' in url or FUZZING_MARK in url): + method = 'POST' + else: + method = 'GET' + return { + 'url': url, + 'method': method, + 'body': body, + 'header': {}, + } + + +def build_target_from_raw_http(raw_http_filename: str, scheme: str) -> dict: + """Build the targets from raw http files + + @type raw_http_filenames: list + @param raw_http_filenames: The list with the raw http filenames + @type scheme: str + @param scheme: The scheme used in the URL + @returns dict: The target HTTP data builded into a dict + """ + def build_header_from_raw_http(header_list: deque) -> Dict[str, str]: + """Get the HTTP header + + @tyoe header_list: deque + @param header_list: The list with HTTP header + @returns Dict[str, str]: The HTTP header parsed into a dict + """ + headers = {} + i = 0 + this_header = header_list.popleft() + header_length = len(header_list) + while i < header_length and this_header != '': + key, value = this_header.split(': ', 1) + headers[key] = value + this_header = header_list.popleft() + i += 1 + if this_header: + key, value = this_header.split(': ', 1) + headers[key] = value + return headers + + try: + header_list = deque(read_file(raw_http_filename)) + except ValueError: + raise BadArgumentFormat("Invalid header format. E.g. Key: value") + method, path, _ = header_list.popleft().split(' ') + headers = build_header_from_raw_http(header_list) + url = f"{scheme}://{headers['Host']}{path}" + if len(header_list) > 0: + body = header_list.popleft() + else: + body = '' + return { + 'url': url, + 'method': method, + 'body': body, + 'header': headers, + } + + +def build_wordlist(wordlists: str) -> List[Tuple[str, str]]: + """Build the wordlists + + @type wordlists: str + @param wordlists: The wordlists from command line + @returns List[Tuple[str, str]]: The builded wordlists + """ + return [ + parse_option_with_args(wordlist) + for wordlist in split_str_to_list(wordlists, separator=';') + ] + + +def build_encoder(encoders: str) -> List[List[Tuple[str, str]]]: + """Build the encoders + + @type encoders: str + @param encoders: The encoders from command line + @returns List[List[Tuple[str, str]]]: The builded encoders + """ + return [[ + parse_option_with_args(e) + for e in split_str_to_list(encoder, separator='@')] + for encoder in split_str_to_list(encoders) + ] + + +def build_scanner(scanner: str) -> Tuple[str, str]: + """Build the scanner + + @type scanner: str + @param scanner: The scanner from command line + @returns Tuple[str, str]: The builded scanner + """ + return parse_option_with_args(scanner) + + +def build_verbose_mode(is_common: bool, is_detailed: bool) -> List[bool]: + """Build the verbose mode + + @type is_common: bool + @param is_common: A flag to say if is common verbose mode + @type is_detailed: bool + @param is_detailed: A flag to say if is detailed verbose mode + @returns List[bool]: The builded verbose mode + """ + verbose = [False, False] + if is_common: + verbose = [True, False] + elif is_detailed: + verbose = [True, True] + return verbose + + +def build_blacklist_status(blacklist_status: str) -> Tuple[str, str, str]: + """Build the blacklist_status + + @type blacklist_status: str + @param blacklist_status: The blacklist status from command line + @returns Tuple[str, str, str]: The builded blacklist status + """ + blacklisted_status = blacklist_status + blacklist_action = '' + blacklist_action_param = '' + if ':' in blacklisted_status: + blacklisted_status, blacklist_action = blacklisted_status.split(':', 1) + blacklist_action = blacklist_action.lower() + if '=' in blacklist_action: + blacklist_action, blacklist_action_param = blacklist_action.split('=') + else: + blacklist_action = 'stop' + return (blacklisted_status, blacklist_action, blacklist_action_param) diff --git a/src/fuzzingtool/utils/consts.py b/src/fuzzingtool/utils/consts.py index 18c1237d..85792bb4 100644 --- a/src/fuzzingtool/utils/consts.py +++ b/src/fuzzingtool/utils/consts.py @@ -20,15 +20,32 @@ from pathlib import Path -UNKNOWN_FUZZING = -1 -HTTP_METHOD_FUZZING = 0 -PATH_FUZZING = 1 -SUBDOMAIN_FUZZING = 2 -DATA_FUZZING = 3 + +class PluginCategory: + encoder = "encoders" + scanner = "scanners" + wordlist = "wordlists" + + +class FuzzType: + UNKNOWN_FUZZING = -1 + HTTP_METHOD_FUZZING = 0 + PATH_FUZZING = 1 + SUBDOMAIN_FUZZING = 2 + DATA_FUZZING = 3 + + +FUZZ_TYPE_NAME = { + FuzzType.UNKNOWN_FUZZING: "Unknown Fuzzing", + FuzzType.HTTP_METHOD_FUZZING: "HTTP Method Fuzzing", + FuzzType.PATH_FUZZING: "Path Fuzzing", + FuzzType.SUBDOMAIN_FUZZING: "Subdomain Fuzzing", + FuzzType.DATA_FUZZING: "Data Fuzzing", +} FUZZING_MARK = 'FUZZ' FUZZING_MARK_LEN = len(FUZZING_MARK) OUTPUT_DIRECTORY = f'{Path.home()}/.FuzzingTool' -MAX_PAYLOAD_LENGTH_TO_OUTPUT = 30 +MAX_PAYLOAD_LENGTH_TO_OUTPUT = 25 diff --git a/src/fuzzingtool/utils/http_utils.py b/src/fuzzingtool/utils/http_utils.py index 54fd2e39..7331573c 100644 --- a/src/fuzzingtool/utils/http_utils.py +++ b/src/fuzzingtool/utils/http_utils.py @@ -18,35 +18,26 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from requests import Response +from urllib.parse import ParseResult, urlparse +from os.path import splitext + +from requests.models import Response from .consts import FUZZING_MARK -def get_host(url: str) -> str: - """Get the target host from url +def get_url_without_scheme(url: str) -> str: + """Get the target url without scheme @type url: str @param url: The target URL - @returns str: The payloaded target + @returns str: The url without scheme """ - url = get_url_without_scheme(url) - if '/' in url: - return url[:url.index('/')] + if '://' in url: + return url[(url.index('://')+3):] return url -def get_path(url: str) -> str: - """Get the target path from url - - @type url: str - @param url: The target URL - @returns str: The payloaded path - """ - url = get_url_without_scheme(url) - return url[url.index('/'):] - - def get_pure_url(url: str) -> str: """Gets the URL without the FUZZING_MARK variable @@ -61,18 +52,6 @@ def get_pure_url(url: str) -> str: return url -def get_url_without_scheme(url: str) -> str: - """Get the target url without scheme - - @type url: str - @param url: The target URL - @returns str: The url without scheme - """ - if '://' in url: - return url[(url.index('://')+3):] - return url - - def build_raw_response_header(response: Response) -> str: """Build the raw response header from requests lib Response object @@ -86,3 +65,41 @@ def build_raw_response_header(response: Response) -> str: str_header += f"{key}: {value}\r\n" str_header += "\r\n" return str_header + + +class UrlParse(ParseResult): + """Class that has utils url parsing functions""" + @property + def file(self) -> str: + """Get the file name and extension + + @returns str: The file name and extension + """ + return self.path.split('/')[-1:][0] + + @property + def file_name(self) -> str: + """Get the file name only + + @returns str: The file name only + """ + return splitext(self.file)[0] + + @property + def file_ext(self) -> str: + """Get the file extension only + + @returns str: The file extension only + """ + return splitext(self.file)[1] + + +def get_parsed_url(url: str) -> UrlParse: + """Get the url parser object + Has some common url parts (like scheme, hostname and path) + + @type url: str + @param url: The url that'll be parsed + @returns UrlParse: The url parser object + """ + return UrlParse(*urlparse(url)) diff --git a/src/fuzzingtool/utils/result_utils.py b/src/fuzzingtool/utils/result_utils.py index 0f0d8684..42377062 100644 --- a/src/fuzzingtool/utils/result_utils.py +++ b/src/fuzzingtool/utils/result_utils.py @@ -20,7 +20,9 @@ from typing import Tuple -from ..utils.utils import get_human_length, get_formatted_rtt, fix_payload_to_output + +from .consts import MAX_PAYLOAD_LENGTH_TO_OUTPUT +from .utils import get_human_length, get_formatted_rtt, fix_payload_to_output class ResultUtils: @@ -49,7 +51,7 @@ def get_formatted_result(payload: str, if isinstance(rtt, float): rtt = "%.2f" % rtt return ( - f"{fix_payload_to_output(payload):<30}", + f"{fix_payload_to_output(payload):<{MAX_PAYLOAD_LENGTH_TO_OUTPUT}}", f"{rtt:>5} {time_order}", f"{length:>7} {length_order}", '{:>6}'.format(words), diff --git a/src/fuzzingtool/utils/utils.py b/src/fuzzingtool/utils/utils.py index 2b01d42e..7a091e34 100644 --- a/src/fuzzingtool/utils/utils.py +++ b/src/fuzzingtool/utils/utils.py @@ -50,17 +50,24 @@ def split_str_to_list(string: str, @param ignores: A string to ignores the separator @returns List[str]: The splited string """ + def split_with_ignores() -> List[str]: + """Split the string with ignores and separator + + @returns List[str]: The splited string + """ + final = [] + buffer = '' + for substr in string.split(separator): + if substr and substr[-1] == ignores: + buffer += substr[:-1]+separator + else: + final.extend([buffer+substr]) + buffer = '' + return final + if string: if f'{ignores}{separator}' in string: - final = [] - buffer = '' - for substr in string.split(separator): - if substr[-1] == ignores: - buffer += substr[:-1]+separator - else: - final.extend([buffer+substr]) - buffer = '' - return final + return split_with_ignores() return string.split(separator) return [] diff --git a/tests/api/__init__.py b/tests/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/api/test_fuzz_controller.py b/tests/api/test_fuzz_controller.py deleted file mode 100644 index 573386ad..00000000 --- a/tests/api/test_fuzz_controller.py +++ /dev/null @@ -1,182 +0,0 @@ -import unittest -from unittest.mock import Mock, patch - -from src.fuzzingtool.api.fuzz_controller import FuzzController -from src.fuzzingtool.conn.requesters import Requester, SubdomainRequester -from src.fuzzingtool.utils.consts import FUZZING_MARK -from src.fuzzingtool.exceptions.main_exceptions import FuzzControllerException, WordlistCreationError -from src.fuzzingtool.core.defaults.scanners import DataScanner, PathScanner, SubdomainScanner -from src.fuzzingtool.core.plugins.scanners import Reflected -from src.fuzzingtool.core.plugins.encoders import Html -from ..mock_utils.wordlist_mock import WordlistMock - - -class TestFuzzController(unittest.TestCase): - def test_init_requester_with_common_requester(self): - test_url = "http://test-url.com/" - test_fuzz_controller = FuzzController(url=test_url) - test_fuzz_controller._init_requester() - self.assertIsInstance(test_fuzz_controller.requester, Requester) - - def test_init_requester_with_subdomain_requester(self): - test_url = f"http://{FUZZING_MARK}.test-url.com/" - test_fuzz_controller = FuzzController(url=test_url) - test_fuzz_controller._init_requester() - self.assertIsInstance(test_fuzz_controller.requester, SubdomainRequester) - - @patch("src.fuzzingtool.api.fuzz_controller.AB.build_target_from_raw_http") - def test_init_requester_with_raw_http( - self, - mock_build_target_from_raw_http: Mock - ): - return_target = { - 'url': "http://test-url.com/", - 'methods': ['GET'], - 'body': '', - 'header': { - 'test-key': "test-value" - } - } - test_raw_filename = "/home/test/test_raw.txt" - mock_build_target_from_raw_http.return_value = return_target - test_fuzz_controller = FuzzController(raw_http=test_raw_filename) - test_fuzz_controller._init_requester() - mock_build_target_from_raw_http.assert_called_once_with(test_raw_filename, None) - self.assertIsInstance(test_fuzz_controller.requester, Requester) - - def test_init_requester_with_raise_exception(self): - with self.assertRaises(FuzzControllerException) as e: - FuzzController(wordlist="test")._init_requester() - self.assertEqual(str(e.exception), "A target is needed to make the fuzzing") - - @patch("src.fuzzingtool.api.fuzz_controller.Matcher.set_allowed_status") - def test_init_matcher(self, mock_set_allowed_status: Mock): - test_fuzz_controller = FuzzController(url=f"http://test-url.com/{FUZZING_MARK}") - test_fuzz_controller._init_requester() - test_fuzz_controller._init_matcher() - mock_set_allowed_status.assert_called_once_with("200-399,401,403") - - def test_get_default_scanner_with_path_scanner(self): - test_fuzz_controller = FuzzController(url=f"http://test-url.com/{FUZZING_MARK}") - test_fuzz_controller._init_requester() - returned_scanner = test_fuzz_controller._FuzzController__get_default_scanner() - self.assertIsInstance(returned_scanner, PathScanner) - - def test_get_default_scanner_with_subdomain_scanner(self): - test_fuzz_controller = FuzzController(url=f"http://{FUZZING_MARK}.test-url.com/") - test_fuzz_controller._init_requester() - returned_scanner = test_fuzz_controller._FuzzController__get_default_scanner() - self.assertIsInstance(returned_scanner, SubdomainScanner) - - def test_get_default_scanner_with_data_scanner(self): - test_fuzz_controller = FuzzController(url=f"http://test-url.com/", data=f"a={FUZZING_MARK}") - test_fuzz_controller._init_requester() - returned_scanner = test_fuzz_controller._FuzzController__get_default_scanner() - self.assertIsInstance(returned_scanner, DataScanner) - - @patch("src.fuzzingtool.api.fuzz_controller.PluginFactory.object_creator") - def test_init_scanner_with_plugin_scanner(self, mock_object_creator: Mock): - mock_object_creator.return_value = Reflected() - test_fuzz_controller = FuzzController(scanner="Reflected") - test_fuzz_controller._init_scanner() - mock_object_creator.assert_called_once_with("Reflected", "scanners", '') - - @patch("src.fuzzingtool.api.fuzz_controller.FuzzController._FuzzController__get_default_scanner") - def test_init_scanner_with_default_scanner(self, mock_get_default_scanner: Mock): - FuzzController(url=f"http://test-url.com/{FUZZING_MARK}")._init_scanner() - mock_get_default_scanner.assert_called_once() - - def test_build_encoders_with_no_encoder(self): - return_expected = None - returned_encoder = FuzzController()._FuzzController__build_encoders() - self.assertEqual(returned_encoder, return_expected) - - @patch("src.fuzzingtool.api.fuzz_controller.PluginFactory.object_creator") - def test_build_encoders_with_encoders(self, mock_object_creator: Mock): - expected_encoder = Html() - return_expected = ([expected_encoder], []) - mock_object_creator.return_value = expected_encoder - returned_encoders = FuzzController(encoder="Html")._FuzzController__build_encoders() - mock_object_creator.assert_called_once_with("Html", "encoders", '') - self.assertEqual(returned_encoders, return_expected) - - @patch("src.fuzzingtool.api.fuzz_controller.PluginFactory.object_creator") - def test_build_encoders_with_chain_encoders(self, mock_object_creator: Mock): - expected_encoder = Html() - return_expected = ([], [[expected_encoder, expected_encoder]]) - mock_object_creator.return_value = expected_encoder - returned_encoders = FuzzController(encoder="Html@Html")._FuzzController__build_encoders() - mock_object_creator.assert_called_with("Html", "encoders", '') - self.assertEqual(returned_encoders, return_expected) - - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.encoder.set_regex") - @patch("src.fuzzingtool.api.fuzz_controller.PluginFactory.object_creator") - def test_build_encoders_with_encode_only(self, - mock_object_creator: Mock, - mock_set_regex: Mock): - test_encode_only = "<|>|;" - mock_object_creator.return_value = Html() - FuzzController(encoder="Html", encode_only=test_encode_only)._FuzzController__build_encoders() - mock_set_regex.assert_called_once_with(test_encode_only) - - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.set_prefix") - def test_configure_payloader_with_prefix(self, mock_set_prefix: Mock): - FuzzController(prefix="test,test2")._FuzzController__configure_payloader() - mock_set_prefix.assert_called_once_with(["test", "test2"]) - - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.set_suffix") - def test_configure_payloader_with_suffix(self, mock_set_suffix: Mock): - FuzzController(suffix="test,test2")._FuzzController__configure_payloader() - mock_set_suffix.assert_called_once_with(["test", "test2"]) - - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.set_lowercase") - def test_configure_payloader_with_lowercase(self, mock_set_lowercase: Mock): - FuzzController(lower=True)._FuzzController__configure_payloader() - mock_set_lowercase.assert_called_once() - - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.set_uppercase") - def test_configure_payloader_with_uppercase(self, mock_set_uppercase: Mock): - FuzzController(upper=True)._FuzzController__configure_payloader() - mock_set_uppercase.assert_called_once() - - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.set_capitalize") - def test_configure_payloader_with_capitalize(self, mock_set_capitalize: Mock): - FuzzController(capitalize=True)._FuzzController__configure_payloader() - mock_set_capitalize.assert_called_once() - - @patch("src.fuzzingtool.api.fuzz_controller.FuzzController._FuzzController__build_encoders") - @patch("src.fuzzingtool.api.fuzz_controller.Payloader.encoder.set_encoders") - def test_configure_payloader_with_encoders(self, - mock_set_encoders: Mock, - mock_build_encoders: Mock): - build_encoders_return = ([Html()], []) - mock_build_encoders.return_value = build_encoders_return - FuzzController(encoder="Html")._FuzzController__configure_payloader() - mock_set_encoders.assert_called_once_with(build_encoders_return) - - @patch("src.fuzzingtool.api.fuzz_controller.WordlistFactory.creator") - def test_build_wordlist(self, mock_creator: Mock): - test_wordlist = WordlistMock('1') - mock_creator.return_value = test_wordlist - returned_wordlist = FuzzController( - url="http://test-url.com/", wordlist="test=1" - )._FuzzController__build_wordlist([("test", '1')]) - mock_creator.assert_called_once_with("test", '1', None) - self.assertIsInstance(returned_wordlist, list) - self.assertEqual(returned_wordlist, test_wordlist._build()) - - @patch("src.fuzzingtool.api.fuzz_controller.WordlistFactory.creator") - def test_build_wordlist_with_blank_wordlist(self, mock_creator: Mock): - mock_creator.side_effect = WordlistCreationError() - test_fuzz_controller = FuzzController(url="http://test-url.com/", wordlist="test") - with self.assertRaises(FuzzControllerException) as e: - test_fuzz_controller._FuzzController__build_wordlist([("test", '')]) - self.assertEqual(str(e.exception), "The wordlist is empty") - - @patch("src.fuzzingtool.api.fuzz_controller.FuzzController._FuzzController__build_wordlist") - def test_init_dictionary(self, mock_build_wordlist: Mock): - mock_build_wordlist.return_value = ["test", "test", "test2"] - test_fuzz_controller = FuzzController(wordlist="test", unique=True) - test_fuzz_controller._init_dictionary() - self.assertEqual(test_fuzz_controller.dict_metadata["removed"], 1) - self.assertEqual(test_fuzz_controller.dict_metadata["len"], 2) diff --git a/tests/conn/requesters/test_requester.py b/tests/conn/requesters/test_requester.py index ae0a5acc..260c23fd 100644 --- a/tests/conn/requesters/test_requester.py +++ b/tests/conn/requesters/test_requester.py @@ -5,10 +5,9 @@ from src.fuzzingtool.conn.requesters.requester import Requester from src.fuzzingtool.objects.fuzz_word import FuzzWord -from src.fuzzingtool.utils.consts import (FUZZING_MARK, UNKNOWN_FUZZING, HTTP_METHOD_FUZZING, - PATH_FUZZING, DATA_FUZZING) +from src.fuzzingtool.utils.consts import FUZZING_MARK, FuzzType from src.fuzzingtool.exceptions.request_exceptions import RequestException -from src.fuzzingtool.utils.http_utils import get_host +from src.fuzzingtool.utils.http_utils import get_parsed_url from ...mock_utils.response_mock import ResponseMock @@ -152,43 +151,47 @@ def test_get_request_parameters(self): self.assertIsInstance(returned_header, dict) def test_set_fuzzing_type_for_method_fuzzing(self): - return_expected = HTTP_METHOD_FUZZING + return_expected = FuzzType.HTTP_METHOD_FUZZING test_method = FUZZING_MARK requester = Requester("https://test-url.com/", method=test_method) returned_data = requester._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) + self.assertEqual(returned_data, requester.get_fuzzing_type()) self.assertEqual(requester.is_method_fuzzing(), True) def test_set_fuzzing_type_for_path_fuzzing(self): - return_expected = PATH_FUZZING + return_expected = FuzzType.PATH_FUZZING test_url = f"https://test-url.com/{FUZZING_MARK}" requester = Requester(test_url) returned_data = requester._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) + self.assertEqual(returned_data, requester.get_fuzzing_type()) self.assertEqual(requester.is_path_fuzzing(), True) def test_set_fuzzing_type_for_data_fuzzing_on_url_params(self): - return_expected = DATA_FUZZING + return_expected = FuzzType.DATA_FUZZING test_url = f"https://test-url.com/?q={FUZZING_MARK}" requester = Requester(test_url) returned_data = requester._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) + self.assertEqual(returned_data, requester.get_fuzzing_type()) self.assertEqual(requester.is_data_fuzzing(), True) def test_set_fuzzing_type_for_data_fuzzing_on_body(self): - return_expected = DATA_FUZZING + return_expected = FuzzType.DATA_FUZZING test_body = f"user={FUZZING_MARK}&pass={FUZZING_MARK}" requester = Requester("https://test-url.com/", body=test_body) returned_data = requester._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) + self.assertEqual(returned_data, requester.get_fuzzing_type()) self.assertEqual(requester.is_data_fuzzing(), True) def test_set_fuzzing_type_for_data_fuzzing_on_headers(self): - return_expected = DATA_FUZZING + return_expected = FuzzType.DATA_FUZZING test_header = { 'Cookie': f"TESTSESSID={FUZZING_MARK}" } @@ -196,14 +199,22 @@ def test_set_fuzzing_type_for_data_fuzzing_on_headers(self): returned_data = requester._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) + self.assertEqual(returned_data, requester.get_fuzzing_type()) self.assertEqual(requester.is_data_fuzzing(), True) def test_set_fuzzing_type_for_unknown_fuzzing(self): - return_expected = UNKNOWN_FUZZING + return_expected = FuzzType.UNKNOWN_FUZZING returned_data = Requester("https://test-url.com/")._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) + def test_constructor_with_cookie(self): + test_cookie = "COOKIE=TEST" + requester = Requester("https://test-url.com/", cookie=test_cookie) + returned_cookie: FuzzWord = requester._Requester__header['Cookie'] + self.assertIsInstance(returned_cookie, FuzzWord) + self.assertEqual(returned_cookie.word, test_cookie) + def test_get_url(self): return_expected = "https://test-url.com/" test_url = "https://test-url.com/" @@ -211,6 +222,12 @@ def test_get_url(self): self.assertIsInstance(returned_data, str) self.assertEqual(returned_data, return_expected) + def test_get_method(self): + test_method = "GET" + returned_data = Requester("https://test-url.com/", method=test_method).get_method() + self.assertIsInstance(returned_data, str) + self.assertEqual(returned_data, test_method) + def test_set_method(self): test_method = "GET" requester = Requester("https://test-url.com/") @@ -268,26 +285,46 @@ def test_request(self, mock_request: Mock, mock_time: Mock, mock_get_parameters: return_expected = (expected_response, expected_rtt) test_url = "https://test-url.com/" test_payload = "test_payload" + test_proxy = "test-proxy.com:8001" test_parameters = ("GET", test_url, {}, {}, {}) mock_get_parameters.return_value = test_parameters mock_time.return_value = expected_rtt mock_request.return_value = expected_response - returned_data = Requester(test_url, proxies=["test-proxy.com:8001"]).request(test_payload) + returned_data = Requester(test_url, proxies=[test_proxy]).request(test_payload) mock_get_parameters.assert_called_once_with(test_payload) - mock_request.assert_called_once_with(*test_parameters) + mock_request.assert_called_once_with(*(*test_parameters, { + 'http': f"http://{test_proxy}", + 'https': f"https://{test_proxy}" + })) self.assertIsInstance(returned_data, tuple) self.assertTupleEqual(returned_data, return_expected) + @patch("src.fuzzingtool.conn.requesters.requester.Requester._Requester__get_request_parameters") + @patch("src.fuzzingtool.conn.requesters.requester.Requester._request") + def test_request_with_replay_proxy(self, mock_request: Mock, mock_get_parameters: Mock): + test_url = "https://test-url.com/" + test_payload = "test_payload" + test_proxy = "test-proxy.com:8001" + test_parameters = ("GET", test_url, {}, {}, {}) + mock_get_parameters.return_value = test_parameters + mock_request.return_value = ResponseMock() + Requester(test_url, replay_proxy=test_proxy).request(test_payload, replay_proxy=True) + mock_request.assert_called_once_with(*(*test_parameters, { + 'http': f"http://{test_proxy}", + 'https': f"https://{test_proxy}" + })) + @patch("src.fuzzingtool.conn.requesters.requester.Requester._request") def test_request_with_raise_exception(self, mock_request: Mock): test_url = "https://test-url.com/" test_header_key = "test_key" test_header_value = "test_value" - requester = Requester(test_url, headers={test_header_key: test_header_value}) + test_proxy = "test-proxy.com:8080" + requester = Requester(test_url, headers={test_header_key: test_header_value}, proxy=test_proxy) mock_request.side_effect = requests.exceptions.ProxyError with self.assertRaises(RequestException) as e: requester.request() - self.assertEqual(str(e.exception), "Can't connect to the proxy") + self.assertEqual(str(e.exception), f"Can't connect to the proxy {test_proxy}") mock_request.side_effect = requests.exceptions.TooManyRedirects with self.assertRaises(RequestException) as e: requester.request() @@ -315,7 +352,7 @@ def test_request_with_raise_exception(self, mock_request: Mock): mock_request.side_effect = UnicodeError with self.assertRaises(RequestException) as e: requester.request() - self.assertEqual(str(e.exception), f"Invalid hostname {get_host(test_url)} for HTTP request") + self.assertEqual(str(e.exception), f"Invalid hostname {get_parsed_url(test_url).hostname} for HTTP request") mock_request.side_effect = ValueError with self.assertRaises(RequestException) as e: requester.request() diff --git a/tests/conn/requesters/test_subdomain_requester.py b/tests/conn/requesters/test_subdomain_requester.py index ae4a06a7..5c14b0bd 100644 --- a/tests/conn/requesters/test_subdomain_requester.py +++ b/tests/conn/requesters/test_subdomain_requester.py @@ -5,7 +5,7 @@ from requests.models import Response from src.fuzzingtool.conn.requesters.subdomain_requester import SubdomainRequester -from src.fuzzingtool.utils.consts import SUBDOMAIN_FUZZING +from src.fuzzingtool.utils.consts import FuzzType from src.fuzzingtool.exceptions.request_exceptions import InvalidHostname @@ -32,23 +32,22 @@ def test_request(self, mock_resolve_hostname: Mock, mock_request: Mock): expected_ip = "127.0.0.1" - expected_ip_dict = {'ip': expected_ip} test_payload = '' requester = SubdomainRequester("https://test-url.com/") mock_resolve_hostname.return_value = expected_ip mock_request.return_value = (Response(), 0.0) returned_data = requester.request(test_payload) mock_resolve_hostname.assert_called_once_with("test-url.com") - mock_request.assert_called_once_with(test_payload) + mock_request.assert_called_once_with(test_payload, False) returned_response, returned_rtt, returned_ip = returned_data self.assertIsInstance(returned_response, Response) self.assertIsInstance(returned_rtt, float) self.assertEqual(returned_rtt, 0.0) - self.assertIsInstance(returned_ip, dict) - self.assertDictEqual(returned_ip, expected_ip_dict) + self.assertIsInstance(returned_ip, str) + self.assertEqual(returned_ip, expected_ip) def test_set_fuzzing_type(self): - return_expected = SUBDOMAIN_FUZZING + return_expected = FuzzType.SUBDOMAIN_FUZZING returned_data = SubdomainRequester("https://test-url.com/")._set_fuzzing_type() self.assertIsInstance(returned_data, int) self.assertEqual(returned_data, return_expected) diff --git a/tests/core/test_blacklist_status.py b/tests/core/test_blacklist_status.py index 3f0a82b8..fb426659 100644 --- a/tests/core/test_blacklist_status.py +++ b/tests/core/test_blacklist_status.py @@ -1,7 +1,7 @@ import unittest from src.fuzzingtool.core.blacklist_status import BlacklistStatus -from src.fuzzingtool.exceptions.main_exceptions import BadArgumentType +from src.fuzzingtool.exceptions import BadArgumentType class TestBlacklistStatus(unittest.TestCase): diff --git a/tests/core/test_filter.py b/tests/core/test_filter.py new file mode 100644 index 00000000..933f6cc1 --- /dev/null +++ b/tests/core/test_filter.py @@ -0,0 +1,55 @@ +import unittest + +from src.fuzzingtool.core.filter import Filter +from src.fuzzingtool.objects.result import Result, HttpHistory +from src.fuzzingtool.exceptions import BadArgumentType, BadArgumentFormat +from ..mock_utils.response_mock import ResponseMock + + +class TestFilter(unittest.TestCase): + def test_build_status_codes(self): + return_expected = [404, 500] + test_status = "404,500" + returned_status_code = Filter._Filter__build_status_codes(Filter, test_status) + self.assertIsInstance(returned_status_code, list) + self.assertEqual(returned_status_code, return_expected) + + def test_build_status_codes_with_invalid_status_type(self): + test_status = "404a" + with self.assertRaises(BadArgumentType) as e: + Filter._Filter__build_status_codes(Filter, test_status) + self.assertEqual(str(e.exception), f"The filter status argument ({test_status}) must be integer") + + def test_build_regexer_with_invalid_regex(self): + test_regex = r"[a-z][A-Z]((?" + with self.assertRaises(BadArgumentFormat) as e: + Filter(regex=test_regex) + self.assertEqual(str(e.exception), f"Invalid regex format {test_regex} on Filter") + + def test_check_with_found_status(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock())) + test_result.history.status = 404 + returned_check_flag = Filter( + status_code="404", + ).check(test_result) + self.assertIsInstance(returned_check_flag, bool) + self.assertEqual(returned_check_flag, return_expected) + + def test_check_with_found_regex(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock())) + returned_check_flag = Filter( + regex=r"[a-z][A-Z]", + ).check(test_result) + self.assertIsInstance(returned_check_flag, bool) + self.assertEqual(returned_check_flag, return_expected) + + def test_check_with_not_found(self): + return_expected = True + test_result = Result(HttpHistory(response=ResponseMock())) + returned_check_flag = Filter( + status_code="500", + ).check(test_result) + self.assertIsInstance(returned_check_flag, bool) + self.assertEqual(returned_check_flag, return_expected) diff --git a/tests/core/test_job_manager.py b/tests/core/test_job_manager.py new file mode 100644 index 00000000..81c0c6d9 --- /dev/null +++ b/tests/core/test_job_manager.py @@ -0,0 +1,74 @@ +import unittest +from queue import Queue + +from src.fuzzingtool.core.job_manager import JobManager +from src.fuzzingtool.core.dictionary import Dictionary +from src.fuzzingtool.objects import Payload, HttpHistory, Result +from ..mock_utils.response_mock import ResponseMock + + +class TestJobManager(unittest.TestCase): + def test_update(self): + test_result = Result(HttpHistory(response=ResponseMock())) + test_provider = "TestProvider" + job_manager = JobManager( + dictionary=Dictionary(wordlist=["test-payload"]), + job_providers={}, + max_rlevel=1, + ) + job_manager.update(test_provider, test_result) + self.assertEqual(job_manager.total_jobs, 2) + self.assertEqual(test_result.job_description, f"Enqueued new job from {test_provider}") + + def test_get_job(self): + job_manager = JobManager( + dictionary=Dictionary(wordlist=["test-payload"]), + job_providers={}, + max_rlevel=1, + ) + job_manager.get_job() + self.assertEqual(job_manager.current_job_name, "wordlist") + self.assertEqual(job_manager.total_requests, 1) + + def test_has_pending_jobs(self): + job_manager = JobManager( + dictionary=Dictionary(wordlist=[]), + job_providers={}, + max_rlevel=1, + ) + self.assertEqual(job_manager.has_pending_jobs(), True) + job_manager.get_job() + self.assertEqual(job_manager.has_pending_jobs(), False) + + def test_has_pending_jobs_from_providers_without_job(self): + test_provider_queue = Queue() + job_manager = JobManager( + dictionary=Dictionary(wordlist=[]), + job_providers={'test_provider': test_provider_queue}, + max_rlevel=1, + ) + self.assertEqual(job_manager.has_pending_jobs_from_providers(), False) + + def test_has_pending_jobs_from_providers_with_job(self): + test_provider_queue = Queue() + job_manager = JobManager( + dictionary=Dictionary(wordlist=[]), + job_providers={'test_provider': test_provider_queue}, + max_rlevel=1, + ) + test_provider_queue.put("test-payload-job") + self.assertEqual(job_manager.has_pending_jobs_from_providers(), True) + + def test_check_for_new_jobs(self): + test_provider_queue = Queue() + job_manager = JobManager( + dictionary=Dictionary(wordlist=[]), + job_providers={'test_provider': test_provider_queue}, + max_rlevel=1, + ) + job_manager.get_job() + test_provider_queue.put(Payload("test-payload-job")) + job_manager.check_for_new_jobs() + job_manager.get_job() + self.assertEqual(job_manager.current_job_name, "test_provider") + self.assertEqual(job_manager.total_requests, 1) diff --git a/tests/core/test_matcher.py b/tests/core/test_matcher.py index 11500812..af21ec3f 100644 --- a/tests/core/test_matcher.py +++ b/tests/core/test_matcher.py @@ -1,49 +1,56 @@ import unittest +from unittest.mock import Mock, patch import operator from src.fuzzingtool.core.matcher import Matcher -from src.fuzzingtool.objects.result import Result -from src.fuzzingtool.exceptions.main_exceptions import BadArgumentType +from src.fuzzingtool.objects.result import Result, HttpHistory +from src.fuzzingtool.exceptions import BadArgumentType, BadArgumentFormat from ..mock_utils.response_mock import ResponseMock class TestMatcher(unittest.TestCase): - def test_build_allowed_status_without_status(self): + def test_build_status_code_without_status(self): return_expected = { 'is_default': True, 'list': [200], 'range': [] } - returned_allowed_status_dict = Matcher._Matcher__build_allowed_status(Matcher, None) - self.assertIsInstance(returned_allowed_status_dict, dict) - self.assertDictEqual(returned_allowed_status_dict, return_expected) + returned_status_code_dict = Matcher._Matcher__build_status_code(Matcher, None) + self.assertIsInstance(returned_status_code_dict, dict) + self.assertDictEqual(returned_status_code_dict, return_expected) - def test_build_allowed_status_with_list_and_range(self): + def test_build_status_code_with_list_and_range(self): return_expected = { 'is_default': False, 'list': [401, 403], 'range': [200, 399] } - returned_allowed_status_dict = Matcher._Matcher__build_allowed_status(Matcher, "200-399,401,403") - self.assertIsInstance(returned_allowed_status_dict, dict) - self.assertDictEqual(returned_allowed_status_dict, return_expected) + returned_status_code_dict = Matcher._Matcher__build_status_code(Matcher, "200-399,401,403") + self.assertIsInstance(returned_status_code_dict, dict) + self.assertDictEqual(returned_status_code_dict, return_expected) - def test_build_allowed_status_with_inverted_range(self): + def test_build_status_code_with_inverted_range(self): return_expected = { 'is_default': False, 'list': [], 'range': [200, 399] } - returned_allowed_status_dict = Matcher._Matcher__build_allowed_status(Matcher, "399-200") - self.assertIsInstance(returned_allowed_status_dict, dict) - self.assertDictEqual(returned_allowed_status_dict, return_expected) + returned_status_code_dict = Matcher._Matcher__build_status_code(Matcher, "399-200") + self.assertIsInstance(returned_status_code_dict, dict) + self.assertDictEqual(returned_status_code_dict, return_expected) - def test_build_allowed_status_with_invalid_status_type(self): + def test_build_status_code_with_invalid_status_type(self): test_status = "200-399a" with self.assertRaises(BadArgumentType) as e: - Matcher._Matcher__build_allowed_status(Matcher, test_status) + Matcher._Matcher__build_status_code(Matcher, test_status) self.assertEqual(str(e.exception), f"The match status argument ({test_status}) must be integer") + def test_build_regexer_with_invalid_regex(self): + test_regex = r"[a-z][A-Z]((?" + with self.assertRaises(BadArgumentFormat) as e: + Matcher(regex=test_regex) + self.assertEqual(str(e.exception), f"Invalid regex format {test_regex} on Matcher") + def test_get_comparator_and_callback_with_operator_ge(self): return_expected = ('25', operator.ge) returned_data = Matcher._Matcher__get_comparator_and_callback(Matcher, '>=25') @@ -139,59 +146,92 @@ def test_comparator_is_set_without_set(self): self.assertIsInstance(returned_data, bool) self.assertEqual(returned_data, return_expected) - def test_match_status_with_match(self): + @patch("src.fuzzingtool.core.matcher.Matcher._Matcher__build_status_code") + def test_set_status_code(self, mock_build_status_code: Mock): + test_status = "200" + Matcher.set_status_code(Matcher, test_status) + mock_build_status_code.assert_called_once_with(test_status) + + @patch("src.fuzzingtool.core.matcher.Matcher._Matcher__build_comparator") + def test_set_comparator(self, mock_build_comparator: Mock): + test_comparator = ('5', '', '', '') + Matcher.set_comparator(Matcher, *test_comparator) + mock_build_comparator.assert_called_once_with(*test_comparator) + + def test_match_with_match(self): return_expected = True - test_result = Result(response=ResponseMock()) + test_result = Result(HttpHistory(response=ResponseMock())) returned_match_flag = Matcher( - allowed_status="200", + status_code="200", ).match(test_result) self.assertIsInstance(returned_match_flag, bool) self.assertEqual(returned_match_flag, return_expected) def test_match_status_without_match(self): return_expected = False - test_result = Result(response=ResponseMock()) + test_result = Result(HttpHistory(response=ResponseMock())) returned_match_flag = Matcher( - allowed_status="401", + status_code="401", ).match(test_result) self.assertIsInstance(returned_match_flag, bool) self.assertEqual(returned_match_flag, return_expected) - def test_match_time(self): - return_expected = True - test_result = Result(response=ResponseMock(), rtt=3.0) + def test_match_time_without_match(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock(), rtt=3.0)) returned_match_flag = Matcher( - allowed_status="200", - time="<=4" + status_code="200", + time=">4" ).match(test_result) self.assertIsInstance(returned_match_flag, bool) self.assertEqual(returned_match_flag, return_expected) - def test_match_size(self): - return_expected = True - test_result = Result(response=ResponseMock()) + def test_match_size_without_match(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock())) returned_match_flag = Matcher( - allowed_status="200", - size=">=10", + status_code="200", + size="<10", ).match(test_result) self.assertIsInstance(returned_match_flag, bool) self.assertEqual(returned_match_flag, return_expected) - def test_match_words(self): + def test_match_words_without_match(self): return_expected = False - test_result = Result(response=ResponseMock(), rtt=3.0) + test_result = Result(HttpHistory(response=ResponseMock())) returned_match_flag = Matcher( - allowed_status="200", - words="<=4" + status_code="200", + words=">10" ).match(test_result) self.assertIsInstance(returned_match_flag, bool) self.assertEqual(returned_match_flag, return_expected) - def test_match_lines(self): - return_expected = True - test_result = Result(response=ResponseMock(), rtt=3.0) + def test_match_lines_without_match(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock())) + returned_match_flag = Matcher( + status_code="200", + lines="!=2" + ).match(test_result) + self.assertIsInstance(returned_match_flag, bool) + self.assertEqual(returned_match_flag, return_expected) + + def test_match_regex_without_match(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock())) + returned_match_flag = Matcher( + status_code="200", + regex="Invalid test regex" + ).match(test_result) + self.assertIsInstance(returned_match_flag, bool) + self.assertEqual(returned_match_flag, return_expected) + + def test_not_match_with_two_configs(self): + return_expected = False + test_result = Result(HttpHistory(response=ResponseMock())) returned_match_flag = Matcher( - allowed_status="200", + status_code="200", + words=">5", lines="==2" ).match(test_result) self.assertIsInstance(returned_match_flag, bool) diff --git a/tests/core/test_payloader.py b/tests/core/test_payloader.py index 4cc41f42..41fd8734 100644 --- a/tests/core/test_payloader.py +++ b/tests/core/test_payloader.py @@ -4,7 +4,7 @@ from src.fuzzingtool.core.payloader import Payloader, EncodeManager from src.fuzzingtool.core.plugins.encoders.hex import Hex from src.fuzzingtool.objects.payload import Payload -from src.fuzzingtool.exceptions.main_exceptions import BadArgumentFormat +from src.fuzzingtool.exceptions import BadArgumentFormat def assert_payload_list_is_equal(payloads: List[Payload], other_payloads: List[Payload]) -> None: @@ -13,6 +13,7 @@ def assert_payload_list_is_equal(payloads: List[Payload], other_payloads: List[P for i, payload in enumerate(payloads): assert payload.raw == other_payloads[i].raw assert payload.final == other_payloads[i].final + assert payload.rlevel == other_payloads[i].rlevel assert payload.config == other_payloads[i].config @@ -65,18 +66,18 @@ def tearDown(self): Payloader.case = lambda ajusted_payload: ajusted_payload def test_get_customized_payload_without_mutation(self): - test_payload = "test_payload" - return_expected = [Payload(test_payload)] + test_payload = Payload("test_payload") + return_expected = [test_payload] returned_payloads = Payloader.get_customized_payload(test_payload) self.assertIsInstance(returned_payloads, list) assert_payload_list_is_equal(returned_payloads, return_expected) def test_get_customized_payload_with_prefix(self): - test_payload = "test_payload" + test_payload = Payload("test_payload") test_prefix = ['<', '|'] return_expected = [ - Payload(test_payload).with_prefix(test_prefix[0]), - Payload(test_payload).with_prefix(test_prefix[1]) + Payload().update(test_payload).with_prefix(test_prefix[0]), + Payload().update(test_payload).with_prefix(test_prefix[1]) ] Payloader.set_prefix(test_prefix) returned_payloads = Payloader.get_customized_payload(test_payload) @@ -84,11 +85,11 @@ def test_get_customized_payload_with_prefix(self): assert_payload_list_is_equal(returned_payloads, return_expected) def test_get_customized_payload_with_suffix(self): - test_payload = "test_payload" + test_payload = Payload("test_payload") test_suffix = ['>', '|'] return_expected = [ - Payload(test_payload).with_suffix(test_suffix[0]), - Payload(test_payload).with_suffix(test_suffix[1]) + Payload().update(test_payload).with_suffix(test_suffix[0]), + Payload().update(test_payload).with_suffix(test_suffix[1]) ] Payloader.set_suffix(test_suffix) returned_payloads = Payloader.get_customized_payload(test_payload) @@ -96,24 +97,24 @@ def test_get_customized_payload_with_suffix(self): assert_payload_list_is_equal(returned_payloads, return_expected) def test_get_customized_payload_with_upper(self): - test_payload = "test_payload" - return_expected = [Payload(test_payload).with_case(str.upper, "Upper")] + test_payload = Payload("test_payload") + return_expected = [test_payload.with_case(str.upper, "Upper")] Payloader.set_uppercase() returned_payloads = Payloader.get_customized_payload(test_payload) self.assertIsInstance(returned_payloads, list) assert_payload_list_is_equal(returned_payloads, return_expected) def test_get_customized_payload_with_lower(self): - test_payload = "test_payload" - return_expected = [Payload(test_payload).with_case(str.lower, "Lower")] + test_payload = Payload("test_payload") + return_expected = [test_payload.with_case(str.lower, "Lower")] Payloader.set_lowercase() returned_payloads = Payloader.get_customized_payload(test_payload) self.assertIsInstance(returned_payloads, list) assert_payload_list_is_equal(returned_payloads, return_expected) def test_get_customized_payload_with_capitalize(self): - test_payload = "test_payload" - return_expected = [Payload(test_payload).with_case(str.capitalize, "Capitalize")] + test_payload = Payload("test_payload") + return_expected = [test_payload.with_case(str.capitalize, "Capitalize")] Payloader.set_capitalize() returned_payloads = Payloader.get_customized_payload(test_payload) self.assertIsInstance(returned_payloads, list) diff --git a/tests/core/test_recursion_manager.py b/tests/core/test_recursion_manager.py new file mode 100644 index 00000000..a1b4947d --- /dev/null +++ b/tests/core/test_recursion_manager.py @@ -0,0 +1,52 @@ +import unittest +from unittest.mock import Mock, patch + +from src.fuzzingtool.core.recursion_manager import RecursionManager +from src.fuzzingtool.objects import Payload, Result, HttpHistory +from ..mock_utils.response_mock import ResponseMock + + +class TestRecursionManager(unittest.TestCase): + def setUp(self): + self.recursion_manager = RecursionManager( + max_rlevel=1, + wordlist=['test_1', 'test_2'] + ) + + def test_notify(self): + mock_observer = Mock() + mock_observer.update = Mock() + self.recursion_manager.set_observer(mock_observer) + test_path = "/test_path/" + test_result = Result(HttpHistory(response=ResponseMock())) + self.recursion_manager.notify(test_result, test_path) + mock_observer.update.assert_called_once_with(f"directory recursion on path {test_path}", test_result) + + def test_has_recursive_job(self): + test_has_recursive_job = self.recursion_manager.has_recursive_job() + self.assertIsInstance(test_has_recursive_job, bool) + self.assertEqual(test_has_recursive_job, False) + + @patch("src.fuzzingtool.core.recursion_manager.RecursionManager.notify") + def test_check_for_recursion_with_recursion(self, mock_notify: Mock): + test_directory = "test_directory/" + test_result = Result(HttpHistory(response=ResponseMock())) + test_result.history.url += test_directory + self.recursion_manager.check_for_recursion(test_result) + mock_notify.assert_called_once_with(test_result, f"/{test_directory}") + self.assertEqual(self.recursion_manager.directories_queue.empty(), False) + enqueued_directory: Payload = self.recursion_manager.directories_queue.get() + self.assertIsInstance(enqueued_directory, Payload) + self.assertEqual(enqueued_directory.final, test_directory) + + def test_fill_payloads_queue(self): + test_directory = "test_directory/" + test_payload = Payload().with_recursion(test_directory) + self.recursion_manager.directories_queue.put(test_payload) + self.recursion_manager.fill_payloads_queue() + wordlist = self.recursion_manager.wordlist + i = 0 + while not self.recursion_manager.payloads_queue.empty(): + this_payload: Payload = self.recursion_manager.payloads_queue.get() + self.assertEqual(this_payload.final, f"{test_directory}{wordlist[i]}") + i += 1 diff --git a/tests/decorators/test_plugin_meta.py b/tests/decorators/test_plugin_meta.py index 88874715..f29e23f2 100644 --- a/tests/decorators/test_plugin_meta.py +++ b/tests/decorators/test_plugin_meta.py @@ -1,7 +1,7 @@ import unittest from src.fuzzingtool.decorators.plugin_meta import plugin_meta -from src.fuzzingtool.exceptions.main_exceptions import MetadataException +from src.fuzzingtool.exceptions import MetadataException class TestPluginMeta(unittest.TestCase): @@ -19,7 +19,7 @@ class TestPlugin: __author__ = "Test Author" __params__ = {} __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None self.assertEqual(str(e.exception), "Metadata __version__ not specified on plugin TestPlugin") def test_blank_meta_on_author(self): @@ -29,7 +29,7 @@ class TestPlugin: __author__ = '' __params__ = {} __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "Author cannot be empty on plugin TestPlugin") @@ -40,7 +40,7 @@ class TestPlugin: __author__ = "Test Author" __params__ = {} __desc__ = '' - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "Description cannot be blank on plugin TestPlugin") @@ -51,7 +51,7 @@ class TestPlugin: __author__ = "Test Author" __params__ = {} __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = '' self.assertEqual(str(e.exception), "Version cannot be blank on plugin TestPlugin") @@ -62,7 +62,7 @@ class TestPlugin: __author__ = "TestAuthor" __params__ = "Test Param" __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "The parameters must be a dictionary on plugin TestPlugin") @@ -75,7 +75,7 @@ class TestPlugin: 'metavar': "TEST_METAVAR" } __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "Key type must be in parameters dict on plugin TestPlugin") @@ -89,7 +89,7 @@ class TestPlugin: 'type': None } __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "Value of type cannot be empty in parameters dict on plugin TestPlugin") @@ -103,7 +103,7 @@ class TestPlugin: 'type': list } __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "The key 'cli_list_separator' must be present when parameter type is list on plugin TestPlugin") @@ -118,6 +118,17 @@ class TestPlugin: 'cli_list_separator': '' } __desc__ = "Test Description" - __type__ = "Test Type" + __type__ = None __version__ = "Test Version" self.assertEqual(str(e.exception), "Value of 'cli_list_separator' cannot be blank on TestPlugin") + + def test_invalid_fuzz_type(self): + with self.assertRaises(MetadataException) as e: + @plugin_meta + class TestPlugin: + __author__ = "Test Author" + __params__ = {} + __desc__ = "Test Description" + __type__ = "Fuzz Type" + __version__ = "Test Version" + self.assertEqual(str(e.exception), "Plugin type should be None or a valid FuzzType on plugin TestPlugin") diff --git a/tests/factories/test_plugin_factory.py b/tests/factories/test_plugin_factory.py index d7a97a3f..311f7abe 100644 --- a/tests/factories/test_plugin_factory.py +++ b/tests/factories/test_plugin_factory.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import Mock, patch +from src.fuzzingtool.utils.consts import PluginCategory from src.fuzzingtool.factories.plugin_factory import PluginFactory from src.fuzzingtool.core.plugins import scanners, Grep, Reflected from src.fuzzingtool.exceptions.plugin_exceptions import InvalidPlugin, InvalidPluginCategory, PluginCreationError @@ -8,7 +9,7 @@ class TestPluginFactory(unittest.TestCase): def test_get_plugins_from_category(self): - test_category = "scanners" + test_category = PluginCategory.scanner return_expected = [Grep] with patch.dict(scanners.__dict__, {'Grep': Grep, 'Test': None}, clear=True): returned_data = PluginFactory.get_plugins_from_category(test_category) @@ -26,12 +27,12 @@ def test_class_creator(self, mock_import_module: Mock, mock_getattr: Mock): test_plugin = "Grep" - test_category = "scanners" + test_category = PluginCategory.scanner test_module = scanners return_expected = Grep mock_import_module.return_value = test_module mock_getattr.return_value = return_expected - returned_data = PluginFactory.class_creator(test_plugin, test_category) + returned_data = PluginFactory.class_creator(test_category, test_plugin) mock_import_module.assert_called_once_with( f"fuzzingtool.core.plugins.{test_category}", package=test_plugin @@ -43,52 +44,52 @@ def test_class_creator(self, @patch("src.fuzzingtool.factories.plugin_factory.import_module") def test_class_creator_with_invalid_plugin(self, mock_import_module: Mock): test_plugin = "InvalidPluginTest" - test_category = "scanners" + test_category = PluginCategory.scanner mock_import_module.return_value = scanners with self.assertRaises(InvalidPlugin): - PluginFactory.class_creator(test_plugin, test_category) + PluginFactory.class_creator(test_category, test_plugin) @patch("src.fuzzingtool.factories.plugin_factory.PluginFactory.class_creator") def test_object_creator_with_invalid_plugin(self, mock_class_creator: Mock): test_plugin = "InvalidPluginTest" - test_category = "scanners" + test_category = PluginCategory.scanner test_params = '' mock_class_creator.side_effect = InvalidPlugin with self.assertRaises(PluginCreationError): - PluginFactory.object_creator(test_plugin, test_category, test_params) + PluginFactory.object_creator(test_category, test_plugin, test_params) @patch("src.fuzzingtool.factories.plugin_factory.PluginFactory.class_creator") def test_object_creator_with_params(self, mock_class_creator: Mock): test_name = "Grep" - test_category = "scanners" + test_category = PluginCategory.scanner test_params = "email" mock_class_creator.return_value = Grep - returned_data = PluginFactory.object_creator(test_name, test_category, test_params) + returned_data = PluginFactory.object_creator(test_category, test_name, test_params) self.assertIsInstance(returned_data, Grep) @patch("src.fuzzingtool.factories.plugin_factory.PluginFactory.class_creator") def test_object_creator_without_params(self, mock_class_creator: Mock): test_name = "Reflected" - test_category = "scanners" + test_category = PluginCategory.scanner test_params = '' # No need to have params on constructor mock_class_creator.return_value = Reflected - returned_data = PluginFactory.object_creator(test_name, test_category, test_params) + returned_data = PluginFactory.object_creator(test_category, test_name, test_params) self.assertIsInstance(returned_data, Reflected) @patch("src.fuzzingtool.factories.plugin_factory.PluginFactory.class_creator") def test_object_creator_with_blank_params(self, mock_class_creator: Mock): test_name = "Grep" - test_category = "scanners" + test_category = PluginCategory.scanner test_params = '' mock_class_creator.return_value = Grep with self.assertRaises(PluginCreationError): - PluginFactory.object_creator(test_name, test_category, test_params) + PluginFactory.object_creator(test_category, test_name, test_params) @patch("src.fuzzingtool.factories.plugin_factory.PluginFactory.class_creator") def test_object_creator_with_invalid_params(self, mock_class_creator: Mock): test_name = "Grep" - test_category = "scanners" + test_category = PluginCategory.scanner test_params = "\\" # Invalid regex mock_class_creator.return_value = Grep with self.assertRaises(PluginCreationError): - PluginFactory.object_creator(test_name, test_category, test_params) + PluginFactory.object_creator(test_category, test_name, test_params) diff --git a/tests/factories/test_wordlist_factory.py b/tests/factories/test_wordlist_factory.py index 1b6c2203..c19c15d1 100644 --- a/tests/factories/test_wordlist_factory.py +++ b/tests/factories/test_wordlist_factory.py @@ -5,9 +5,9 @@ from src.fuzzingtool.conn.requesters import Requester, SubdomainRequester from src.fuzzingtool.core.defaults.wordlists import ListWordlist, FileWordlist from src.fuzzingtool.core.plugins.wordlists import Robots, CrtSh -from src.fuzzingtool.exceptions.main_exceptions import WordlistCreationError +from src.fuzzingtool.exceptions import WordlistCreationError from src.fuzzingtool.exceptions.plugin_exceptions import InvalidPlugin, PluginCreationError -from src.fuzzingtool.utils.consts import FUZZING_MARK +from src.fuzzingtool.utils.consts import PluginCategory, FUZZING_MARK class TestWordlistFactory(unittest.TestCase): @@ -16,15 +16,15 @@ def test_creator_with_list(self, mock_plugin_class_creator: Mock): test_name = "[1,2,3,4,5]" mock_plugin_class_creator.side_effect = InvalidPlugin returned_data = WordlistFactory.creator(test_name, '', None) - mock_plugin_class_creator.assert_called_once_with(test_name, "wordlists") + mock_plugin_class_creator.assert_called_once_with(PluginCategory.wordlist, test_name) self.assertIsInstance(returned_data, ListWordlist) - + @patch("src.fuzzingtool.factories.wordlist_factory.PluginFactory.class_creator") def test_creator_with_file(self, mock_plugin_class_creator: Mock): test_name = "/home/test_wordlists/wordlist.txt" mock_plugin_class_creator.side_effect = InvalidPlugin returned_data = WordlistFactory.creator(test_name, '', None) - mock_plugin_class_creator.assert_called_once_with(test_name, "wordlists") + mock_plugin_class_creator.assert_called_once_with(PluginCategory.wordlist, test_name) self.assertIsInstance(returned_data, FileWordlist) @patch("src.fuzzingtool.factories.wordlist_factory.PluginFactory.object_creator") @@ -39,8 +39,8 @@ def test_creator_with_plugin_and_params( mock_plugin_class_creator.return_value = Robots mock_plugin_object_creator.return_value = Robots(test_params) returned_data = WordlistFactory.creator(test_name, test_params, None) - mock_plugin_class_creator.assert_called_with(test_name, "wordlists") - mock_plugin_object_creator.assert_called_once_with(test_name, "wordlists", test_params) + mock_plugin_class_creator.assert_called_with(PluginCategory.wordlist, test_name) + mock_plugin_object_creator.assert_called_once_with(PluginCategory.wordlist, test_name, test_params) self.assertIsInstance(returned_data, Robots) @patch("src.fuzzingtool.factories.wordlist_factory.Requester.get_url") @@ -63,22 +63,22 @@ def test_creator_with_path_fuzzing_plugin_and_requester( mock_get_pure_url.return_value = test_pure_url mock_requester_get_url.return_value = test_requester_url returned_data = WordlistFactory.creator(test_name, '', test_requester) - mock_plugin_class_creator.assert_called_with(test_name, "wordlists") - mock_plugin_object_creator.assert_called_once_with(test_name, "wordlists", test_pure_url) + mock_plugin_class_creator.assert_called_with(PluginCategory.wordlist, test_name) + mock_plugin_object_creator.assert_called_once_with(PluginCategory.wordlist, test_name, test_pure_url) mock_get_pure_url.assert_called_once_with(test_requester_url) mock_requester_get_url.assert_called_once() self.assertIsInstance(returned_data, Robots) @patch("src.fuzzingtool.factories.wordlist_factory.Requester.get_url") @patch("src.fuzzingtool.factories.wordlist_factory.get_pure_url") - @patch("src.fuzzingtool.factories.wordlist_factory.get_host") + @patch("src.fuzzingtool.factories.wordlist_factory.get_parsed_url") @patch("src.fuzzingtool.factories.wordlist_factory.PluginFactory.object_creator") @patch("src.fuzzingtool.factories.wordlist_factory.PluginFactory.class_creator") def test_creator_with_subdomain_fuzzing_plugin_and_requester( self, mock_plugin_class_creator: Mock, mock_plugin_object_creator: Mock, - mock_get_host: Mock, + mock_get_parsed_url: Mock, mock_get_pure_url: Mock, mock_requester_get_url: Mock ): @@ -89,13 +89,13 @@ def test_creator_with_subdomain_fuzzing_plugin_and_requester( test_host = "test-url.com" mock_plugin_class_creator.return_value = CrtSh mock_plugin_object_creator.return_value = CrtSh(test_host) - mock_get_host.return_value = test_host + mock_get_parsed_url.return_value.hostname = test_host mock_get_pure_url.return_value = test_pure_url mock_requester_get_url.return_value = test_requester_url returned_data = WordlistFactory.creator(test_name, '', test_requester) - mock_plugin_class_creator.assert_called_with(test_name, "wordlists") - mock_plugin_object_creator.assert_called_once_with(test_name, "wordlists", test_host) - mock_get_host.assert_called_once_with(test_pure_url) + mock_plugin_class_creator.assert_called_with(PluginCategory.wordlist, test_name) + mock_plugin_object_creator.assert_called_once_with(PluginCategory.wordlist, test_name, test_host) + mock_get_parsed_url.assert_called_once_with(test_pure_url) mock_get_pure_url.assert_called_once_with(test_requester_url) mock_requester_get_url.assert_called_once() self.assertIsInstance(returned_data, CrtSh) diff --git a/tests/interfaces/cli/test_cli_arguments.py b/tests/interfaces/cli/test_cli_arguments.py index 5ff156d6..2a4272a7 100644 --- a/tests/interfaces/cli/test_cli_arguments.py +++ b/tests/interfaces/cli/test_cli_arguments.py @@ -2,7 +2,7 @@ from argparse import Namespace from src.fuzzingtool.interfaces.cli.cli_arguments import CliArguments -from src.fuzzingtool.exceptions.main_exceptions import BadArgumentFormat +from src.fuzzingtool.exceptions import BadArgumentFormat from ...mock_utils.args_decorator import mock_sys_args diff --git a/tests/interfaces/cli/test_cli_output.py b/tests/interfaces/cli/test_cli_output.py index ee95a1ac..8e37a595 100644 --- a/tests/interfaces/cli/test_cli_output.py +++ b/tests/interfaces/cli/test_cli_output.py @@ -3,10 +3,8 @@ from datetime import datetime from src.fuzzingtool.interfaces.cli.cli_output import Colors, CliOutput -from src.fuzzingtool.objects import Payload, Result -from src.fuzzingtool.utils.consts import PATH_FUZZING, SUBDOMAIN_FUZZING -from src.fuzzingtool.utils.http_utils import get_host, get_path -from src.fuzzingtool.utils.result_utils import ResultUtils +from src.fuzzingtool.objects import Payload, Result, HttpHistory +from src.fuzzingtool.utils.consts import FuzzType from ...mock_utils.response_mock import ResponseMock @@ -72,7 +70,7 @@ def test_get_not_worked(self): def test_get_formatted_payload(self): test_result = Result( - response=ResponseMock(), + HttpHistory(response=ResponseMock()), payload=Payload("test-payload"), ) returned_payload = CliOutput()._CliOutput__get_formatted_payload(test_result) @@ -81,32 +79,31 @@ def test_get_formatted_payload(self): def test_get_formatted_payload_with_path_fuzz(self): test_result = Result( - response=ResponseMock(), - fuzz_type=PATH_FUZZING, + HttpHistory(response=ResponseMock()), + fuzz_type=FuzzType.PATH_FUZZING, ) returned_payload = CliOutput()._CliOutput__get_formatted_payload(test_result) self.assertIsInstance(returned_payload, str) - self.assertEqual(returned_payload, get_path(test_result.url)) + self.assertEqual(returned_payload, test_result.history.parsed_url.path) - @patch("src.fuzzingtool.interfaces.cli.cli_output.get_path") - def test_get_formatted_payload_with_path_fuzz_and_raise_exception(self, mock_get_path: Mock): + def test_get_formatted_payload_with_path_fuzz_without_directory(self): test_result = Result( - response=ResponseMock(), - fuzz_type=PATH_FUZZING, + HttpHistory(response=ResponseMock()), + fuzz_type=FuzzType.PATH_FUZZING, ) - mock_get_path.side_effect = ValueError + test_result.history.url = "http://test-url.com" returned_payload = CliOutput()._CliOutput__get_formatted_payload(test_result) self.assertIsInstance(returned_payload, str) - self.assertEqual(returned_payload, test_result.url) + self.assertEqual(returned_payload, test_result.history.url) - def test_get_formatted_payload_with_path_fuzz(self): + def test_get_formatted_payload_with_subdomain_fuzz(self): test_result = Result( - response=ResponseMock(), - fuzz_type=SUBDOMAIN_FUZZING, + HttpHistory(response=ResponseMock()), + fuzz_type=FuzzType.SUBDOMAIN_FUZZING, ) returned_payload = CliOutput()._CliOutput__get_formatted_payload(test_result) self.assertIsInstance(returned_payload, str) - self.assertEqual(returned_payload, get_host(test_result.url)) + self.assertEqual(returned_payload, test_result.history.parsed_url.hostname) def test_get_formatted_status_with_status_404(self): test_status = 404 @@ -160,7 +157,7 @@ def test_get_formatted_status_with_status_500(self): @patch("src.fuzzingtool.interfaces.cli.cli_output.ResultUtils.get_formatted_result") def test_get_formatted_result_items(self, mock_format_result: Mock, mock_format_status: Mock): test_result = Result( - response=ResponseMock(), + HttpHistory(response=ResponseMock()), payload=Payload("test-payload") ) expected_status = "test_status" @@ -169,11 +166,11 @@ def test_get_formatted_result_items(self, mock_format_result: Mock, mock_format_ mock_format_result.return_value = ('', '', '', '', '') cli_output = CliOutput() returned_items = cli_output._CliOutput__get_formatted_result_items(test_result) - mock_format_status.assert_called_once_with(test_result.status) + mock_format_status.assert_called_once_with(test_result.history.status) mock_format_result.assert_called_once_with( cli_output._CliOutput__get_formatted_payload(test_result), - test_result.rtt, - test_result.body_size, + test_result.history.rtt, + test_result.history.body_size, test_result.words, test_result.lines ) @@ -181,37 +178,7 @@ def test_get_formatted_result_items(self, mock_format_result: Mock, mock_format_ self.assertEqual(returned_items, return_expected) @patch("src.fuzzingtool.interfaces.cli.cli_output.CliOutput._CliOutput__get_formatted_result_items") - def test_get_formatted_result_without_result_custom(self, mock_format_items: Mock): - test_payload = "test_payload" - test_status_code = "200" - test_rtt = "300 ms" - test_length = "50 KB" - test_words = "50" - test_lines = "10" - mock_format_items.return_value = ( - test_payload, - test_status_code, - test_rtt, - test_length, - test_words, - test_lines - ) - test_result = Result(response=ResponseMock()) - return_expected = ( - f"{test_payload} {Colors.GRAY}[" - f"{Colors.LIGHT_GRAY}Code{Colors.RESET} {test_status_code} | " - f"{Colors.LIGHT_GRAY}RTT{Colors.RESET} {test_rtt} | " - f"{Colors.LIGHT_GRAY}Size{Colors.RESET} {test_length} | " - f"{Colors.LIGHT_GRAY}Words{Colors.RESET} {test_words} | " - f"{Colors.LIGHT_GRAY}Lines{Colors.RESET} {test_lines}{Colors.GRAY}]{Colors.RESET}" - ) - returned_data = CliOutput()._CliOutput__get_formatted_result(test_result) - mock_format_items.assert_called_once_with(test_result) - self.assertIsInstance(returned_data, str) - self.assertEqual(returned_data, return_expected) - - @patch("src.fuzzingtool.interfaces.cli.cli_output.CliOutput._CliOutput__get_formatted_result_items") - def test_get_formatted_result_with_result_custom(self, mock_format_items: Mock): + def test_get_formatted_result(self, mock_format_items: Mock): test_payload = "test_payload" test_status_code = "200" test_rtt = "300 ms" @@ -226,9 +193,7 @@ def test_get_formatted_result_with_result_custom(self, mock_format_items: Mock): test_words, test_lines ) - test_result = Result(response=ResponseMock()) - test_result.custom['test_0'] = None - test_result.custom['test_1'] = "test_custom" + test_result = Result(HttpHistory(response=ResponseMock())) return_expected = ( f"{test_payload} {Colors.GRAY}[" f"{Colors.LIGHT_GRAY}Code{Colors.RESET} {test_status_code} | " @@ -236,8 +201,7 @@ def test_get_formatted_result_with_result_custom(self, mock_format_items: Mock): f"{Colors.LIGHT_GRAY}Size{Colors.RESET} {test_length} | " f"{Colors.LIGHT_GRAY}Words{Colors.RESET} {test_words} | " f"{Colors.LIGHT_GRAY}Lines{Colors.RESET} {test_lines}{Colors.GRAY}]{Colors.RESET}" - f"\n{Colors.LIGHT_YELLOW}|_ test_1: " - f"{ResultUtils.format_custom_field(test_result.custom['test_1'])}{Colors.RESET}" + f"{Colors.LIGHT_YELLOW}{test_result.get_description()}{Colors.RESET}" ) returned_data = CliOutput()._CliOutput__get_formatted_result(test_result) mock_format_items.assert_called_once_with(test_result) diff --git a/tests/mock_utils/wordlist_mock.py b/tests/mock_utils/wordlist_mock.py index d48a7cfe..071520ce 100644 --- a/tests/mock_utils/wordlist_mock.py +++ b/tests/mock_utils/wordlist_mock.py @@ -1,5 +1,5 @@ from src.fuzzingtool.core.bases.base_wordlist import BaseWordlist -from src.fuzzingtool.exceptions.main_exceptions import WordlistCreationError, BuildWordlistFails +from src.fuzzingtool.exceptions import WordlistCreationError, BuildWordlistFails class WordlistMock(BaseWordlist): diff --git a/tests/objects/test_http_history.py b/tests/objects/test_http_history.py new file mode 100644 index 00000000..809d10ec --- /dev/null +++ b/tests/objects/test_http_history.py @@ -0,0 +1,48 @@ +import unittest +from unittest.mock import Mock, patch + +from src.fuzzingtool.objects.http_history import HttpHistory +from src.fuzzingtool.utils.http_utils import UrlParse +from ..mock_utils.response_mock import ResponseMock + + +class TestHttpHistory(unittest.TestCase): + def test_http_history(self): + test_response = ResponseMock() + test_history = HttpHistory( + response=test_response, + rtt=3.0 + ) + self.assertIsInstance(test_history.parsed_url, UrlParse) + self.assertEqual(test_history.body_size, 25) + self.assertEqual(test_history.response_time, 2.0) + self.assertEqual(test_history.request_time, 1.0) + self.assertEqual(test_history.ip, '') + self.assertEqual(test_history.request, test_response.request) + self.assertEqual(test_history.response, test_response) + + def test_http_history_with_ip(self): + expected_ip = '127.0.0.1' + test_history = HttpHistory(ResponseMock(), 3.0, expected_ip) + self.assertEqual(test_history.ip, expected_ip) + + @patch("src.fuzzingtool.objects.http_history.build_raw_response_header") + def test_headers(self, mock_build_raw_response_header: Mock): + test_headers = ( + "HTTP/1.1 200 OK\r\n" + "Server: nginx/1.19.0\r\n" + "Date: Fri, 17 Dec 2021 17:42:14 GMT\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "Transfer-Encoding: chunked\r\n" + "Connection: keep-alive\r\n" + "X-Powered-By: PHP/5.6.40-38+ubuntu20.04.1+deb.sury.org+1\r\n" + "\r\n" + ) + mock_build_raw_response_header.return_value = test_headers + test_response = ResponseMock() + test_history = HttpHistory( + response=test_response, + rtt=3.0 + ) + self.assertEqual(test_history.raw_headers, test_headers) + self.assertEqual(test_history.headers_length, 228) diff --git a/tests/objects/test_payload.py b/tests/objects/test_payload.py index f245d6c4..72ff0734 100644 --- a/tests/objects/test_payload.py +++ b/tests/objects/test_payload.py @@ -9,12 +9,21 @@ def test_str(self): returned_payload = Payload(expected_result) self.assertEqual(str(returned_payload), expected_result) + def test_update(self): + test_payload = Payload("test") + returned_payload = Payload().update(test_payload) + self.assertIsInstance(returned_payload, Payload) + self.assertEqual(returned_payload.raw, test_payload.raw) + self.assertEqual(returned_payload.final, test_payload.final) + self.assertEqual(returned_payload.rlevel, test_payload.rlevel) + self.assertDictEqual(returned_payload.config, test_payload.config) + def test_with_prefix(self): test_payload_str = "test" test_prefix = "" expected_config = {'prefix': test_prefix} expected_final_payload = f"{test_prefix}{test_payload_str}" - returned_payload: Payload = Payload(test_payload_str).with_prefix(test_prefix) + returned_payload = Payload(test_payload_str).with_prefix(test_prefix) self.assertIsInstance(returned_payload, Payload) self.assertDictEqual(returned_payload.config, expected_config) self.assertEqual(returned_payload.final, expected_final_payload) @@ -24,7 +33,7 @@ def test_with_suffix(self): test_suffix = "" expected_config = {'suffix': test_suffix} expected_final_payload = f"{test_payload_str}{test_suffix}" - returned_payload: Payload = Payload(test_payload_str).with_suffix(test_suffix) + returned_payload = Payload(test_payload_str).with_suffix(test_suffix) self.assertIsInstance(returned_payload, Payload) self.assertDictEqual(returned_payload.config, expected_config) self.assertEqual(returned_payload.final, expected_final_payload) @@ -35,7 +44,7 @@ def test_with_case(self): test_case_method = "Upper" expected_config = {'case': test_case_method} expected_final_payload = test_payload_str.upper() - returned_payload: Payload = Payload(test_payload_str).with_case( + returned_payload = Payload(test_payload_str).with_case( test_case_callback, test_case_method ) self.assertIsInstance(returned_payload, Payload) @@ -48,17 +57,18 @@ def test_with_encoder(self): test_encoder = "TestEncoder" expected_config = {'encoder': test_encoder} expected_final_payload = test_encoded - returned_payload: Payload = Payload(test_payload_str).with_encoder( + returned_payload = Payload(test_payload_str).with_encoder( test_encoded, test_encoder ) self.assertIsInstance(returned_payload, Payload) self.assertDictEqual(returned_payload.config, expected_config) self.assertEqual(returned_payload.final, expected_final_payload) - def test_update(self): - test_payload: Payload = Payload("test").with_suffix("") - returned_payload: Payload = Payload().update(test_payload) + def test_with_recursion(self): + test_recursion_payload = "test-payload" + expected_config = {'rlevel_0': ''} + returned_payload = Payload().with_recursion(test_recursion_payload) self.assertIsInstance(returned_payload, Payload) - self.assertEqual(returned_payload.raw, test_payload.raw) - self.assertEqual(returned_payload.final, test_payload.final) - self.assertDictEqual(returned_payload.config, test_payload.config) + self.assertEqual(returned_payload.final, test_recursion_payload) + self.assertEqual(returned_payload.rlevel, 1) + self.assertDictEqual(returned_payload.config, expected_config) diff --git a/tests/objects/test_result.py b/tests/objects/test_result.py index 67837d33..e46a184d 100644 --- a/tests/objects/test_result.py +++ b/tests/objects/test_result.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import Mock, patch -from src.fuzzingtool.objects import Payload, Result +from src.fuzzingtool.objects import Payload, Result, HttpHistory, ScannerResult from src.fuzzingtool.objects.base_objects import BaseItem from src.fuzzingtool.utils.result_utils import ResultUtils from ..mock_utils.response_mock import ResponseMock @@ -9,94 +9,93 @@ class TestResult(unittest.TestCase): def setUp(self): - self.test_headers = ( - "HTTP/1.1 200 OK\r\n" - "Server: nginx/1.19.0\r\n" - "Date: Fri, 17 Dec 2021 17:42:14 GMT\r\n" - "Content-Type: text/html; charset=UTF-8\r\n" - "Transfer-Encoding: chunked\r\n" - "Connection: keep-alive\r\n" - "X-Powered-By: PHP/5.6.40-38+ubuntu20.04.1+deb.sury.org+1\r\n" - "\r\n" - ) BaseItem.reset_index() def tearDown(self): BaseItem.reset_index() - @patch("src.fuzzingtool.objects.result.build_raw_response_header") - def test_result(self, mock_build_raw_response_header: Mock): + def test_result(self): test_response = ResponseMock() - mock_build_raw_response_header.return_value = self.test_headers result = Result( - response=test_response, - rtt=3.0, + history=HttpHistory(response=test_response, rtt=3.0), payload=Payload('test-payload') ) - self.assertEqual(result.request_time, 1.0) - self.assertEqual(result.headers_length, 228) - self.assertEqual(result.body_size, 25) self.assertEqual(result.words, 5) self.assertEqual(result.lines, 2) - self.assertEqual(result.get_response(), test_response) - @patch("src.fuzzingtool.objects.result.build_raw_response_header") - def test_result_str(self, mock_build_raw_response_header: Mock): + def test_get_description(self): + test_job_description = "Enqueued new job from Test" + result = Result( + history=HttpHistory(response=ResponseMock(), rtt=3.0), + payload=Payload('test-payload') + ) + result.job_description = test_job_description + test_scanner = "test-scanner" + test_enqueued_payloads = 5 + result.scanners_res[test_scanner] = ScannerResult(test_scanner) + result.scanners_res[test_scanner].data['test_0'] = None + result.scanners_res[test_scanner].data['test_1'] = "test-value" + result.scanners_res[test_scanner].enqueued_payloads = test_enqueued_payloads + return_expected = ( + f"\n|_ {test_job_description}" + f"\n|_ test_1: test-value" + f"\n|_ Scanner {test_scanner} enqueued {test_enqueued_payloads} payloads" + ) + returned_description = result.get_description() + self.assertIsInstance(returned_description, str) + self.assertEqual(returned_description, return_expected) + + def test_result_str(self): test_response = ResponseMock() - mock_build_raw_response_header.return_value = self.test_headers result = Result( - response=test_response, - rtt=3.0, + history=HttpHistory(response=test_response, rtt=3.0), payload=Payload('test-payload') ) - result.custom['test_0'] = None - result.custom['test_1'] = "test_value" payload, rtt, length, words, lines = ResultUtils.get_formatted_result( - result.payload, result.rtt, result.body_size, + result.payload, result.history.rtt, result.history.body_size, result.words, result.lines ) return_expected = ( f"{payload} [" - f"Code {result.status} | " + f"Code {result.history.status} | " f"RTT {rtt} | " f"Size {length} | " f"Words {words} | " f"Lines {lines}]" - f"\n|_ test_1: {result.custom['test_1']}" ) self.assertEqual(str(result), return_expected) - @patch("src.fuzzingtool.objects.result.build_raw_response_header") - def test_result_iter(self, mock_build_raw_response_header: Mock): + def test_result_iter(self): test_prefix = "test-prefix|" payload: Payload = Payload("test-payload").with_prefix(test_prefix) - mock_build_raw_response_header.return_value = self.test_headers Result.save_payload_configs = True Result.save_headers = True Result.save_body = True result = Result( - response=ResponseMock(), - rtt=3.0, + history=HttpHistory(ResponseMock(), 3.0, '127.0.0.1'), payload=payload ) - result.custom['test-key'] = "test-value" + test_scanner = "test-scanner" + result.scanners_res[test_scanner] = ScannerResult(test_scanner) + result.scanners_res[test_scanner].data['test-key'] = "test-value" expected_result_dict = { - 'index': 1, - 'url': "https://test-url.com/", - 'method': "GET", - 'rtt': 3.0, - 'request_time': 1.0, - 'response_time': 2.0, - 'status': 200, - 'headers_length': 228, - 'body_size': 25, - 'words': 5, - 'lines': 2, + 'index': result.index, + 'url': result.history.url, + 'method': result.history.method, + 'rtt': result.history.rtt, + 'request_time': result.history.request_time, + 'response_time': result.history.response_time, + 'status': result.history.status, + 'headers_length': result.history.headers_length, + 'body_size': result.history.body_size, + 'words': result.words, + 'lines': result.lines, + 'ip': result.history.ip, 'test-key': "test-value", - 'payload': payload.final, + 'payload': result.payload, 'payload_raw': payload.raw, 'payload_prefix': test_prefix, - 'headers': self.test_headers, - 'body': "My Body Text\nFooter Text\n" + 'headers': result.history.raw_headers, + 'body': result.history.response.text } self.assertDictEqual(dict(result), expected_result_dict) diff --git a/tests/utils/test_logger.py b/tests/persistence/test_logger.py similarity index 90% rename from tests/utils/test_logger.py rename to tests/persistence/test_logger.py index 56b4390e..12e2f723 100644 --- a/tests/utils/test_logger.py +++ b/tests/persistence/test_logger.py @@ -3,7 +3,7 @@ from datetime import datetime from pathlib import Path -from src.fuzzingtool.utils.logger import Logger +from src.fuzzingtool.persistence.logger import Logger from src.fuzzingtool.utils.consts import OUTPUT_DIRECTORY @@ -14,7 +14,7 @@ def setUp(self): self.test_path = Path(f"{OUTPUT_DIRECTORY}/{self.test_host}") self.test_output = Path(f"{self.test_path}/logs/log-{self.default_datetime.strftime('%Y-%m-%d_%H:%M')}.log") - @patch("src.fuzzingtool.utils.logger.datetime") + @patch("src.fuzzingtool.persistence.logger.datetime") @patch("builtins.open", new_callable=mock_open) def test_setup(self, mock_file: Mock, mock_date: Mock): test_datetime_now = self.default_datetime.strftime('%Y/%m/%d %H:%M') @@ -26,7 +26,7 @@ def test_setup(self, mock_file: Mock, mock_date: Mock): self.assertIsInstance(returned_data, str) self.assertEqual(returned_data, str(self.test_output)) - @patch("src.fuzzingtool.utils.logger.datetime") + @patch("src.fuzzingtool.persistence.logger.datetime") def test_setup_with_path_mkdir(self, mock_date: Mock): mock_date.now.return_value = self.default_datetime Logger().setup(self.test_host) @@ -37,7 +37,7 @@ def test_setup_with_path_mkdir(self, mock_date: Mock): self.test_path.rmdir() self.assertEqual(self.test_path.exists(), False) - @patch("src.fuzzingtool.utils.logger.datetime") + @patch("src.fuzzingtool.persistence.logger.datetime") @patch("builtins.open", new_callable=mock_open) def test_write(self, mock_file: Mock, mock_date: Mock): test_payload = "wp-admin.php" diff --git a/tests/reports/test_report.py b/tests/persistence/test_report.py similarity index 77% rename from tests/reports/test_report.py rename to tests/persistence/test_report.py index 8666b336..d81ee957 100644 --- a/tests/reports/test_report.py +++ b/tests/persistence/test_report.py @@ -2,11 +2,11 @@ from unittest.mock import Mock, patch from datetime import datetime -from src.fuzzingtool.reports import reports -from src.fuzzingtool.reports.base_report import BaseReport -from src.fuzzingtool.reports.report import Report, get_report_name_and_type -from src.fuzzingtool.reports.reports import TxtReport -from src.fuzzingtool.exceptions.main_exceptions import InvalidArgument +from src.fuzzingtool.persistence import reports +from src.fuzzingtool.persistence.base_report import BaseReport +from src.fuzzingtool.persistence.report import Report, get_report_name_and_type +from src.fuzzingtool.persistence.reports import TxtReport +from src.fuzzingtool.exceptions import InvalidArgument class TestReport(unittest.TestCase): @@ -17,7 +17,7 @@ def test_get_report_name_and_type_with_full_name(self): self.assertIsInstance(returned_data, tuple) self.assertEqual(returned_data, return_expected) - @patch("src.fuzzingtool.reports.report.datetime") + @patch("src.fuzzingtool.persistence.report.datetime") def test_get_report_name_and_type_with_only_extension(self, mock_datetime: Mock): test_datetime_now = datetime(2021, 1, 1, 0, 0) mock_datetime.now.return_value = test_datetime_now @@ -34,14 +34,14 @@ def test_get_available_reports(self): self.assertIsInstance(returned_data, dict) self.assertEqual(returned_data, return_expected) - @patch("src.fuzzingtool.reports.report.Report.get_available_reports") + @patch("src.fuzzingtool.persistence.report.Report.get_available_reports") def test_build(self, mock_get_available_reports: Mock): test_name = "test_report.txt" mock_get_available_reports.return_value = {'txt': TxtReport} returned_data = Report.build(test_name) self.assertIsInstance(returned_data, BaseReport) - @patch("src.fuzzingtool.reports.report.Report.get_available_reports") + @patch("src.fuzzingtool.persistence.report.Report.get_available_reports") def test_build_with_invalid_format(self, mock_get_available_reports: Mock): test_name = "test_report.test" mock_get_available_reports.return_value = {'txt': TxtReport} diff --git a/tests/test_fuzz_lib.py b/tests/test_fuzz_lib.py new file mode 100644 index 00000000..ef6c16ad --- /dev/null +++ b/tests/test_fuzz_lib.py @@ -0,0 +1,178 @@ +import unittest +from unittest.mock import Mock, patch + +from src.fuzzingtool.fuzz_lib import FuzzLib +from src.fuzzingtool.conn.requesters import Requester, SubdomainRequester +from src.fuzzingtool.utils.consts import PluginCategory, FUZZING_MARK +from src.fuzzingtool.exceptions import FuzzLibException, WordlistCreationError +from src.fuzzingtool.core.defaults.scanners import DataScanner, PathScanner, SubdomainScanner +from src.fuzzingtool.core.plugins.scanners import Reflected +from src.fuzzingtool.core.plugins.encoders import Html +from .mock_utils.wordlist_mock import WordlistMock + + +class TestFuzzController(unittest.TestCase): + def test_init_requester_with_common_requester(self): + test_url = "http://test-url.com/" + test_fuzz_lib = FuzzLib(url=test_url) + test_fuzz_lib._init_requester() + self.assertIsInstance(test_fuzz_lib.requester, Requester) + + def test_init_requester_with_subdomain_requester(self): + test_url = f"http://{FUZZING_MARK}.test-url.com/" + test_fuzz_lib = FuzzLib(url=test_url) + test_fuzz_lib._init_requester() + self.assertIsInstance(test_fuzz_lib.requester, SubdomainRequester) + + @patch("src.fuzzingtool.fuzz_lib.build_target_from_raw_http") + def test_init_requester_with_raw_http( + self, + mock_build_target_from_raw_http: Mock + ): + return_target = { + 'url': "http://test-url.com/", + 'method': 'GET', + 'body': '', + 'header': { + 'test-key': "test-value" + } + } + test_raw_filename = "/home/test/test_raw.txt" + mock_build_target_from_raw_http.return_value = return_target + test_fuzz_lib = FuzzLib(raw_http=test_raw_filename) + test_fuzz_lib._init_requester() + mock_build_target_from_raw_http.assert_called_once_with(test_raw_filename, None) + self.assertIsInstance(test_fuzz_lib.requester, Requester) + + def test_init_requester_with_raise_exception(self): + with self.assertRaises(FuzzLibException) as e: + FuzzLib(wordlist="test")._init_requester() + self.assertEqual(str(e.exception), "A target is needed to make the fuzzing") + + @patch("src.fuzzingtool.fuzz_lib.Matcher.set_status_code") + def test_init_matcher(self, mock_set_status_code: Mock): + test_fuzz_lib = FuzzLib(url=f"http://test-url.com/{FUZZING_MARK}") + test_fuzz_lib._init_requester() + test_fuzz_lib._init_matcher() + mock_set_status_code.assert_called_once_with("200-399,401,403") + + def test_get_default_scanner_with_path_scanner(self): + test_fuzz_lib = FuzzLib(url=f"http://test-url.com/{FUZZING_MARK}") + test_fuzz_lib._init_requester() + returned_scanner = test_fuzz_lib._FuzzLib__get_default_scanner() + self.assertIsInstance(returned_scanner, PathScanner) + + def test_get_default_scanner_with_subdomain_scanner(self): + test_fuzz_lib = FuzzLib(url=f"http://{FUZZING_MARK}.test-url.com/") + test_fuzz_lib._init_requester() + returned_scanner = test_fuzz_lib._FuzzLib__get_default_scanner() + self.assertIsInstance(returned_scanner, SubdomainScanner) + + def test_get_default_scanner_with_data_scanner(self): + test_fuzz_lib = FuzzLib(url=f"http://test-url.com/", data=f"a={FUZZING_MARK}") + test_fuzz_lib._init_requester() + returned_scanner = test_fuzz_lib._FuzzLib__get_default_scanner() + self.assertIsInstance(returned_scanner, DataScanner) + + @patch("src.fuzzingtool.fuzz_lib.PluginFactory.object_creator") + def test_init_scanners_with_plugin_scanner(self, mock_object_creator: Mock): + mock_object_creator.return_value = Reflected() + test_fuzz_lib = FuzzLib(url=f"http://test-url.com/", scanner="Reflected") + test_fuzz_lib._init_requester() + test_fuzz_lib._init_scanners() + mock_object_creator.assert_called_once_with(PluginCategory.scanner, "Reflected", '') + + @patch("src.fuzzingtool.fuzz_lib.FuzzLib._FuzzLib__get_default_scanner") + def test_init_scanners_with_default_scanner(self, mock_get_default_scanner: Mock): + FuzzLib(url=f"http://test-url.com/{FUZZING_MARK}")._init_scanners() + mock_get_default_scanner.assert_called_once() + + @patch("src.fuzzingtool.fuzz_lib.PluginFactory.object_creator") + def test_build_encoders_with_encoders(self, mock_object_creator: Mock): + expected_encoder = Html() + return_expected = ([expected_encoder], []) + mock_object_creator.return_value = expected_encoder + returned_encoders = FuzzLib(encoder="Html")._FuzzLib__build_encoders() + mock_object_creator.assert_called_once_with(PluginCategory.encoder, "Html", '') + self.assertEqual(returned_encoders, return_expected) + + @patch("src.fuzzingtool.fuzz_lib.PluginFactory.object_creator") + def test_build_encoders_with_chain_encoders(self, mock_object_creator: Mock): + expected_encoder = Html() + return_expected = ([], [[expected_encoder, expected_encoder]]) + mock_object_creator.return_value = expected_encoder + returned_encoders = FuzzLib(encoder="Html@Html")._FuzzLib__build_encoders() + mock_object_creator.assert_called_with(PluginCategory.encoder, "Html", '') + self.assertEqual(returned_encoders, return_expected) + + @patch("src.fuzzingtool.fuzz_lib.Payloader.encoder.set_regex") + @patch("src.fuzzingtool.fuzz_lib.PluginFactory.object_creator") + def test_build_encoders_with_encode_only(self, + mock_object_creator: Mock, + mock_set_regex: Mock): + test_encode_only = "<|>|;" + mock_object_creator.return_value = Html() + FuzzLib(encoder="Html", encode_only=test_encode_only)._FuzzLib__build_encoders() + mock_set_regex.assert_called_once_with(test_encode_only) + + @patch("src.fuzzingtool.fuzz_lib.Payloader.set_prefix") + def test_configure_payloader_with_prefix(self, mock_set_prefix: Mock): + FuzzLib(prefix="test,test2")._FuzzLib__configure_payloader() + mock_set_prefix.assert_called_once_with(["test", "test2"]) + + @patch("src.fuzzingtool.fuzz_lib.Payloader.set_suffix") + def test_configure_payloader_with_suffix(self, mock_set_suffix: Mock): + FuzzLib(suffix="test,test2")._FuzzLib__configure_payloader() + mock_set_suffix.assert_called_once_with(["test", "test2"]) + + @patch("src.fuzzingtool.fuzz_lib.Payloader.set_lowercase") + def test_configure_payloader_with_lowercase(self, mock_set_lowercase: Mock): + FuzzLib(lower=True)._FuzzLib__configure_payloader() + mock_set_lowercase.assert_called_once() + + @patch("src.fuzzingtool.fuzz_lib.Payloader.set_uppercase") + def test_configure_payloader_with_uppercase(self, mock_set_uppercase: Mock): + FuzzLib(upper=True)._FuzzLib__configure_payloader() + mock_set_uppercase.assert_called_once() + + @patch("src.fuzzingtool.fuzz_lib.Payloader.set_capitalize") + def test_configure_payloader_with_capitalize(self, mock_set_capitalize: Mock): + FuzzLib(capitalize=True)._FuzzLib__configure_payloader() + mock_set_capitalize.assert_called_once() + + @patch("src.fuzzingtool.fuzz_lib.FuzzLib._FuzzLib__build_encoders") + @patch("src.fuzzingtool.fuzz_lib.Payloader.encoder.set_encoders") + def test_configure_payloader_with_encoders(self, + mock_set_encoders: Mock, + mock_build_encoders: Mock): + build_encoders_return = ([Html()], []) + mock_build_encoders.return_value = build_encoders_return + FuzzLib(encoder="Html")._FuzzLib__configure_payloader() + mock_set_encoders.assert_called_once_with(build_encoders_return) + + @patch("src.fuzzingtool.fuzz_lib.WordlistFactory.creator") + def test_build_wordlist(self, mock_creator: Mock): + test_wordlist = WordlistMock('1') + mock_creator.return_value = test_wordlist + returned_wordlist = FuzzLib( + url="http://test-url.com/", wordlist="test=1" + )._FuzzLib__build_wordlist([("test", '1')]) + mock_creator.assert_called_once_with("test", '1', None) + self.assertIsInstance(returned_wordlist, list) + self.assertEqual(returned_wordlist, test_wordlist._build()) + + @patch("src.fuzzingtool.fuzz_lib.WordlistFactory.creator") + def test_build_wordlist_with_blank_wordlist(self, mock_creator: Mock): + mock_creator.side_effect = WordlistCreationError() + test_fuzz_lib = FuzzLib(url="http://test-url.com/", wordlist="test") + with self.assertRaises(FuzzLibException) as e: + test_fuzz_lib._FuzzLib__build_wordlist([("test", '')]) + self.assertEqual(str(e.exception), "The wordlist is empty") + + @patch("src.fuzzingtool.fuzz_lib.FuzzLib._FuzzLib__build_wordlist") + def test_init_dictionary(self, mock_build_wordlist: Mock): + mock_build_wordlist.return_value = ["test", "test", "test2"] + test_fuzz_lib = FuzzLib(wordlist="test", unique=True) + test_fuzz_lib._init_dictionary() + self.assertEqual(test_fuzz_lib.dict_metadata["removed"], 1) + self.assertEqual(test_fuzz_lib.dict_metadata["len"], 2) diff --git a/tests/test_version.py b/tests/test_version.py deleted file mode 100644 index 5c19d945..00000000 --- a/tests/test_version.py +++ /dev/null @@ -1,11 +0,0 @@ -import unittest - -from src.fuzzingtool import version, APP_VERSION - - -class TestVersion(unittest.TestCase): - def test_version(self): - return_expected = '.'.join([str(value) for value in APP_VERSION.values()]) - returned_data = version() - self.assertIsInstance(returned_data, str) - self.assertEqual(returned_data, return_expected) \ No newline at end of file diff --git a/tests/interfaces/test_argument_builder.py b/tests/utils/test_argument_utils.py similarity index 76% rename from tests/interfaces/test_argument_builder.py rename to tests/utils/test_argument_utils.py index 3da0e48d..5ac907ee 100644 --- a/tests/interfaces/test_argument_builder.py +++ b/tests/utils/test_argument_utils.py @@ -1,21 +1,21 @@ import unittest from unittest.mock import patch, Mock -from src.fuzzingtool.interfaces.argument_builder import ArgumentBuilder as AB +from src.fuzzingtool.utils.argument_utils import * -class TestArgumentBuilder(unittest.TestCase): +class TestArgumentUtils(unittest.TestCase): def test_build_target_from_args(self): test_url = "http://test-url.com/" - test_method = "GET,POST" + test_method = "HEAD" test_body = "user=test&pass=test" return_expected = { 'url': test_url, - 'methods': ["GET", "POST"], + 'method': "HEAD", 'body': test_body, 'header': {}, } - returned_target = AB.build_target_from_args(test_url, test_method, test_body) + returned_target = build_target_from_args(test_url, test_method, test_body) self.assertIsInstance(returned_target, dict) self.assertDictEqual(returned_target, return_expected) @@ -25,11 +25,11 @@ def test_build_target_from_args_without_method_and_without_body(self): test_body = '' return_expected = { 'url': test_url, - 'methods': ["GET"], + 'method': "GET", 'body': test_body, 'header': {}, } - returned_target = AB.build_target_from_args(test_url, test_method, test_body) + returned_target = build_target_from_args(test_url, test_method, test_body) self.assertIsInstance(returned_target, dict) self.assertDictEqual(returned_target, return_expected) @@ -39,15 +39,15 @@ def test_build_target_from_args_without_method_and_with_body(self): test_body = "user=test&pass=test" return_expected = { 'url': test_url, - 'methods': ["POST"], + 'method': "POST", 'body': test_body, 'header': {}, } - returned_target = AB.build_target_from_args(test_url, test_method, test_body) + returned_target = build_target_from_args(test_url, test_method, test_body) self.assertIsInstance(returned_target, dict) self.assertDictEqual(returned_target, return_expected) - @patch("src.fuzzingtool.interfaces.argument_builder.read_file") + @patch("src.fuzzingtool.utils.argument_utils.read_file") def test_build_target_from_raw_http(self, mock_read_file: Mock): expected_header = { 'Host': "test-url.com", @@ -57,7 +57,7 @@ def test_build_target_from_raw_http(self, mock_read_file: Mock): test_scheme = "https" return_expected = { 'url': f"{test_scheme}://test-url.com/", - 'methods': ["GET"], + 'method': "GET", 'body': '', 'header': expected_header, } @@ -68,11 +68,11 @@ def test_build_target_from_raw_http(self, mock_read_file: Mock): "User-Agent: Test User Agent", "Cookie: TESTSESSID=testcookie" ] - returned_target = AB.build_target_from_raw_http(test_filename, test_scheme) + returned_target = build_target_from_raw_http(test_filename, test_scheme) self.assertIsInstance(returned_target, dict) self.assertDictEqual(returned_target, return_expected) - @patch("src.fuzzingtool.interfaces.argument_builder.read_file") + @patch("src.fuzzingtool.utils.argument_utils.read_file") def test_build_target_from_raw_http_with_body(self, mock_read_file: Mock): expected_header = { 'Host': "test-url.com", @@ -83,7 +83,7 @@ def test_build_target_from_raw_http_with_body(self, mock_read_file: Mock): test_body = "user=test&pass=test" return_expected = { 'url': f"{test_scheme}://test-url.com/", - 'methods': ["POST"], + 'method': "POST", 'body': test_body, 'header': expected_header, } @@ -96,60 +96,60 @@ def test_build_target_from_raw_http_with_body(self, mock_read_file: Mock): '', test_body ] - returned_target = AB.build_target_from_raw_http(test_filename, test_scheme) + returned_target = build_target_from_raw_http(test_filename, test_scheme) self.assertIsInstance(returned_target, dict) self.assertDictEqual(returned_target, return_expected) def test_build_wordlist(self): return_expected = [('DnsZone', ''), ('Robots', 'http://test-url.com/')] - returned_wordlist = AB.build_wordlist('DnsZone;Robots=http://test-url.com/') + returned_wordlist = build_wordlist('DnsZone;Robots=http://test-url.com/') self.assertIsInstance(returned_wordlist, list) self.assertEqual(returned_wordlist, return_expected) def test_build_encoder(self): return_expected = [[('Plain', '')], [('Url', '5'), ('Hex', '')]] - returned_encoders = AB.build_encoder('Plain,Url=5@Hex') + returned_encoders = build_encoder('Plain,Url=5@Hex') self.assertIsInstance(returned_encoders, list) self.assertEqual(returned_encoders, return_expected) def test_build_scanner(self): return_expected = ('Grep', 'email') - returned_scanner = AB.build_scanner('Grep=email') + returned_scanner = build_scanner('Grep=email') self.assertIsInstance(returned_scanner, tuple) self.assertEqual(returned_scanner, return_expected) def test_build_verbose_mode_without_verbose(self): return_expected = [False, False] - returned_verbose = AB.build_verbose_mode(False, False) + returned_verbose = build_verbose_mode(False, False) self.assertIsInstance(returned_verbose, list) self.assertEqual(returned_verbose, return_expected) def test_build_verbose_mode_with_common_verbose(self): return_expected = [True, False] - returned_verbose = AB.build_verbose_mode(True, False) + returned_verbose = build_verbose_mode(True, False) self.assertIsInstance(returned_verbose, list) self.assertEqual(returned_verbose, return_expected) def test_build_verbose_mode_with_detailed_verbose(self): return_expected = [True, True] - returned_verbose = AB.build_verbose_mode(False, True) + returned_verbose = build_verbose_mode(False, True) self.assertIsInstance(returned_verbose, list) self.assertEqual(returned_verbose, return_expected) def test_build_blacklist_status_without_action(self): return_expected = ('429', 'stop', '') - returned_blacklist = AB.build_blacklist_status('429') + returned_blacklist = build_blacklist_status('429') self.assertIsInstance(returned_blacklist, tuple) self.assertEqual(returned_blacklist, return_expected) def test_build_blacklist_status_with_action(self): return_expected = ('429', 'stop', '') - returned_blacklist = AB.build_blacklist_status('429:stop') + returned_blacklist = build_blacklist_status('429:stop') self.assertIsInstance(returned_blacklist, tuple) self.assertEqual(returned_blacklist, return_expected) def test_build_blacklist_status_with_action_and_param(self): return_expected = ('429', 'wait', '5') - returned_blacklist = AB.build_blacklist_status('429:wait=5') + returned_blacklist = build_blacklist_status('429:wait=5') self.assertIsInstance(returned_blacklist, tuple) self.assertEqual(returned_blacklist, return_expected) diff --git a/tests/utils/test_http_utils.py b/tests/utils/test_http_utils.py index cb2b1ca9..1aaf134b 100644 --- a/tests/utils/test_http_utils.py +++ b/tests/utils/test_http_utils.py @@ -1,5 +1,4 @@ import unittest -from unittest.mock import Mock, patch from src.fuzzingtool.utils.http_utils import * from ..mock_utils.response_mock import ResponseMock @@ -41,36 +40,6 @@ def test_get_pure_url_with_mark_and_dot(self): self.assertIsInstance(returned_data, str) self.assertEqual(returned_data, return_expected) - @patch("src.fuzzingtool.utils.http_utils.get_url_without_scheme") - def test_get_path(self, mock_get_url_without_scheme: Mock): - return_expected = "/" - test_url = "https://test-url.com/" - mock_get_url_without_scheme.return_value = "test-url.com/" - returned_data = get_path(test_url) - mock_get_url_without_scheme.assert_called_once_with(test_url) - self.assertIsInstance(returned_data, str) - self.assertEqual(returned_data, return_expected) - - @patch("src.fuzzingtool.utils.http_utils.get_url_without_scheme") - def test_get_host_without_root_directory(self, mock_get_url_without_scheme: Mock): - return_expected = "test-url.com" - test_url = "https://test-url.com" - mock_get_url_without_scheme.return_value = "test-url.com" - returned_data = get_host(test_url) - mock_get_url_without_scheme.assert_called_once_with(test_url) - self.assertIsInstance(returned_data, str) - self.assertEqual(returned_data, return_expected) - - @patch("src.fuzzingtool.utils.http_utils.get_url_without_scheme") - def test_get_host_with_root_directory(self, mock_get_url_without_scheme: Mock): - return_expected = "test-url.com" - test_url = "https://test-url.com/" - mock_get_url_without_scheme.return_value = "test-url.com/" - returned_data = get_host(test_url) - mock_get_url_without_scheme.assert_called_once_with(test_url) - self.assertIsInstance(returned_data, str) - self.assertEqual(returned_data, return_expected) - def test_build_raw_response_header(self): return_expected = ( "HTTP/1.1 200 OK\r\n" @@ -85,3 +54,24 @@ def test_build_raw_response_header(self): returned_headers = build_raw_response_header(ResponseMock()) self.assertIsInstance(returned_headers, str) self.assertEqual(returned_headers, return_expected) + + def test_urlparse_file(self): + return_expected = "my_file.php" + test_url = f"http://test-url.com/test-path/{return_expected}" + returned_file = get_parsed_url(test_url).file + self.assertIsInstance(returned_file, str) + self.assertEqual(returned_file, return_expected) + + def test_urlparse_file_name(self): + return_expected = "my_file" + test_url = f"http://test-url.com/test-path/{return_expected}.php" + returned_file = get_parsed_url(test_url).file_name + self.assertIsInstance(returned_file, str) + self.assertEqual(returned_file, return_expected) + + def test_urlparse_file_ext(self): + return_expected = ".php" + test_url = f"http://test-url.com/test-path/my_file{return_expected}" + returned_file = get_parsed_url(test_url).file_ext + self.assertIsInstance(returned_file, str) + self.assertEqual(returned_file, return_expected) diff --git a/tests/utils/test_result_utils.py b/tests/utils/test_result_utils.py index 403e7f75..31ce35bc 100644 --- a/tests/utils/test_result_utils.py +++ b/tests/utils/test_result_utils.py @@ -1,5 +1,6 @@ import unittest +from src.fuzzingtool.utils.consts import MAX_PAYLOAD_LENGTH_TO_OUTPUT from src.fuzzingtool.utils.result_utils import ResultUtils @@ -12,7 +13,7 @@ def test_get_formatted_result_with_only_int(self): test_words = 40 test_lines = 7 return_expected = ( - '{:<30}'.format(test_payload), + f"{test_payload:<{MAX_PAYLOAD_LENGTH_TO_OUTPUT}}", '{:>5}'.format(276) + " ms", '{:>7}'.format(200) + " B ", '{:>6}'.format(test_words), @@ -33,7 +34,7 @@ def test_get_formatted_result_with_rtt_float(self): test_words = 40 test_lines = 7 return_expected = ( - '{:<30}'.format(test_payload), + f"{test_payload:<{MAX_PAYLOAD_LENGTH_TO_OUTPUT}}", '{:>5}'.format(2.76) + " s ", '{:>7}'.format(200) + " B ", '{:>6}'.format(test_words), @@ -54,7 +55,7 @@ def test_get_formatted_result_with_length_float(self): test_words = 40 test_lines = 7 return_expected = ( - '{:<30}'.format(test_payload), + f"{test_payload:<{MAX_PAYLOAD_LENGTH_TO_OUTPUT}}", '{:>5}'.format(276) + " ms", '{:>7}'.format('1.50') + " KB", '{:>6}'.format(test_words), diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 9f8706eb..541825b4 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -70,6 +70,13 @@ def test_split_str_to_list_with_two_separators_and_ignorer(self): self.assertIsInstance(returned_data, list) self.assertEqual(returned_data, return_expected) + def test_split_str_to_list_with_one_separator_and_ignorer_with_blank_result(self): + return_expected = ["pay,loa", 'd', ''] + test_content = "pay\\,loa,d," + returned_data = split_str_to_list(test_content) + self.assertIsInstance(returned_data, list) + self.assertEqual(returned_data, return_expected) + def test_stringfy_list_with_empty_list(self): return_expected = '' test_list = []