From f487f645e478a62b75ea6447926d81b25d067a99 Mon Sep 17 00:00:00 2001 From: bretfourbe Date: Fri, 22 Sep 2023 17:34:41 +0200 Subject: [PATCH] Add time check for time-consuming modules --- wapitiCore/attack/attack.py | 5 +++++ wapitiCore/attack/mod_buster.py | 12 ++++++++++-- wapitiCore/attack/mod_drupal_enum.py | 3 +-- wapitiCore/attack/mod_exec.py | 11 +++++++++-- wapitiCore/attack/mod_file.py | 14 +++++++++++--- wapitiCore/attack/mod_nikto.py | 11 +++++++++-- wapitiCore/attack/mod_timesql.py | 10 +++++++++- wapitiCore/attack/mod_wp_enum.py | 15 ++++++++++++++- wapitiCore/main/wapiti.py | 1 + 9 files changed, 69 insertions(+), 13 deletions(-) diff --git a/wapitiCore/attack/attack.py b/wapitiCore/attack/attack.py index e3353e679..f0648aece 100644 --- a/wapitiCore/attack/attack.py +++ b/wapitiCore/attack/attack.py @@ -226,6 +226,7 @@ def __init__( self._stop_event = stop_event self.options = attack_options self.crawler_configuration = crawler_configuration + self.start = 0 # List of attack urls already launched in the current module self.attacked_get = [] @@ -286,6 +287,10 @@ def internal_endpoint(self): def external_endpoint(self): return self.options.get("external_endpoint", "http://wapiti3.ovh") + @property + def max_attack_time(self): + return self.options.get("max_attack_time", 0) + @property def proto_endpoint(self): parts = urlparse(self.external_endpoint) diff --git a/wapitiCore/attack/mod_buster.py b/wapitiCore/attack/mod_buster.py index a0ef40f0b..28db7e2b4 100644 --- a/wapitiCore/attack/mod_buster.py +++ b/wapitiCore/attack/mod_buster.py @@ -18,12 +18,13 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import asyncio -from typing import Optional from os.path import join as path_join +from time import monotonic +from typing import Optional from httpx import RequestError -from wapitiCore.main.log import log_red, log_verbose +from wapitiCore.main.log import log_red, log_verbose, logging from wapitiCore.attack.attack import Attack from wapitiCore.net import Request, Response from wapitiCore.definitions.buster import NAME, WSTG_CODE @@ -99,6 +100,12 @@ async def test_directory(self, path: str): with open(path_join(self.DATA_DIR, self.PATHS_FILE), encoding="utf-8", errors="ignore") as wordlist: while True: + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break + if pending_count < self.options["tasks"] and not self._stop_event.is_set(): try: candidate = next(wordlist).strip() @@ -132,6 +139,7 @@ async def test_directory(self, path: str): tasks.remove(task) async def attack(self, request: Request, response: Optional[Response] = None): + self.start = monotonic() self.finished = True if not self.do_get: return diff --git a/wapitiCore/attack/mod_drupal_enum.py b/wapitiCore/attack/mod_drupal_enum.py index d78fbbdab..e2699cded 100644 --- a/wapitiCore/attack/mod_drupal_enum.py +++ b/wapitiCore/attack/mod_drupal_enum.py @@ -1,7 +1,6 @@ import asyncio import hashlib import json -import logging from os.path import join as path_join from typing import Tuple, Optional from httpx import RequestError @@ -11,7 +10,7 @@ from wapitiCore.net.response import Response from wapitiCore.definitions.fingerprint_webapp import NAME as WEB_APP_VERSIONED from wapitiCore.definitions.fingerprint import NAME as TECHNO_DETECTED, WSTG_CODE -from wapitiCore.main.log import log_blue +from wapitiCore.main.log import log_blue, logging MSG_TECHNO_VERSIONED = "{0} {1} detected" MSG_NO_DRUPAL = "No Drupal Detected" diff --git a/wapitiCore/attack/mod_exec.py b/wapitiCore/attack/mod_exec.py index d984d6563..febaefbd6 100644 --- a/wapitiCore/attack/mod_exec.py +++ b/wapitiCore/attack/mod_exec.py @@ -16,12 +16,13 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from typing import Optional, Iterator from os.path import join as path_join +from time import monotonic +from typing import Optional, Iterator from httpx import ReadTimeout, RequestError -from wapitiCore.main.log import log_red, log_verbose, log_orange +from wapitiCore.main.log import log_red, log_verbose, log_orange, logging from wapitiCore.attack.attack import Attack from wapitiCore.language.vulnerability import Messages from wapitiCore.definitions.exec import NAME, WSTG_CODE @@ -74,6 +75,7 @@ def _find_warning_in_response(data) -> str: return vuln_info async def attack(self, request: Request, response: Optional[Response] = None): + self.start = monotonic() warned = False timeouted = False page = request.path @@ -82,6 +84,11 @@ async def attack(self, request: Request, response: Optional[Response] = None): vulnerable_parameter = False for mutated_request, parameter, payload_info in self.mutator.mutate(request, self.get_payloads): + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break if current_parameter != parameter: # Forget what we know about current parameter current_parameter = parameter diff --git a/wapitiCore/attack/mod_file.py b/wapitiCore/attack/mod_file.py index a3f1de8b9..55bc9a15a 100644 --- a/wapitiCore/attack/mod_file.py +++ b/wapitiCore/attack/mod_file.py @@ -16,14 +16,15 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from os.path import join as path_join -from collections import defaultdict, namedtuple import re +from collections import defaultdict, namedtuple +from os.path import join as path_join +from time import monotonic from typing import Optional, Iterator from httpx import ReadTimeout, RequestError -from wapitiCore.main.log import log_red, log_orange, log_verbose +from wapitiCore.main.log import log_red, log_orange, log_verbose, logging from wapitiCore.attack.attack import Attack from wapitiCore.model import PayloadInfo from wapitiCore.parsers.ini_payload_parser import IniPayloadReader, replace_tags @@ -144,6 +145,7 @@ async def is_false_positive(self, request, pattern): return False async def attack(self, request: Request, response: Optional[Response] = None): + self.start = monotonic() warned = False timeouted = False page = request.path @@ -152,6 +154,12 @@ async def attack(self, request: Request, response: Optional[Response] = None): vulnerable_parameter = False for mutated_request, parameter, payload_info in self.mutator.mutate(request, self.get_payloads): + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break + if current_parameter != parameter: # Forget what we know about current parameter current_parameter = parameter diff --git a/wapitiCore/attack/mod_nikto.py b/wapitiCore/attack/mod_nikto.py index 116671153..b7c26f72a 100644 --- a/wapitiCore/attack/mod_nikto.py +++ b/wapitiCore/attack/mod_nikto.py @@ -18,11 +18,12 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import asyncio import csv -import re import os import random -from urllib.parse import urlparse +import re +from time import monotonic from typing import List, Optional +from urllib.parse import urlparse from httpx import RequestError @@ -129,6 +130,7 @@ async def is_false_positive(self, evil_request: Request, expected_status_codes: return self.status_codes[request.path] in expected_status_codes async def attack(self, request: Request, response: Optional[Response] = None): + self.start = monotonic() try: with open(os.path.join(self.user_config_dir, self.NIKTO_DB), encoding='utf-8') as nikto_db_file: reader = csv.reader(nikto_db_file) @@ -150,6 +152,11 @@ async def attack(self, request: Request, response: Optional[Response] = None): with open(os.path.join(self.user_config_dir, self.NIKTO_DB), encoding='utf-8') as nikto_db_file: reader = csv.reader(nikto_db_file) while True: + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break if pending_count < self.options["tasks"] and not self._stop_event.is_set(): try: line = next(reader) diff --git a/wapitiCore/attack/mod_timesql.py b/wapitiCore/attack/mod_timesql.py index 229c744a4..73bf3289f 100644 --- a/wapitiCore/attack/mod_timesql.py +++ b/wapitiCore/attack/mod_timesql.py @@ -17,8 +17,9 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from math import ceil -from typing import Optional, Iterator from os.path import join as path_join +from time import monotonic +from typing import Optional, Iterator from httpx import ReadTimeout, RequestError @@ -56,12 +57,19 @@ def get_payloads(self) -> Iterator[PayloadInfo]: yield from parser async def attack(self, request: Request, response: Optional[Response] = None): + self.start = monotonic() page = request.path saw_internal_error = False current_parameter = None vulnerable_parameter = False for mutated_request, parameter, _payload in self.mutator.mutate(request, self.get_payloads): + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break + if current_parameter != parameter: # Forget what we know about current parameter current_parameter = parameter diff --git a/wapitiCore/attack/mod_wp_enum.py b/wapitiCore/attack/mod_wp_enum.py index d5ba55e2f..08ee31e87 100644 --- a/wapitiCore/attack/mod_wp_enum.py +++ b/wapitiCore/attack/mod_wp_enum.py @@ -3,6 +3,7 @@ import xml import xml.etree.ElementTree as ET from os.path import join as path_join +from time import monotonic from typing import Match, Optional from wapitiCore.attack.attack import Attack @@ -98,6 +99,12 @@ async def detect_plugin(self, url): if self._stop_event.is_set(): break + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break + request = Request(f'{url}/wp-content/plugins/{plugin}/readme.txt', 'GET') response = await self.crawler.async_send(request) @@ -156,6 +163,12 @@ async def detect_theme(self, url): if self._stop_event.is_set(): break + if monotonic() - self.start > self.max_attack_time >= 1: + logging.info( + f"Skipping: attack time reached for module {self.name}." + ) + break + request = Request(f'{url}/wp-content/themes/{theme}/readme.txt', 'GET') response = await self.crawler.async_send(request) if response.is_success: @@ -218,7 +231,7 @@ async def must_attack(self, request: Request, response: Optional[Response] = Non return request.url == await self.persister.get_root_url() async def attack(self, request: Request, response: Optional[Response] = None): - + self.start = monotonic() self.finished = True request_to_root = Request(request.url) diff --git a/wapitiCore/main/wapiti.py b/wapitiCore/main/wapiti.py index 4a4d96e69..b21fc5339 100755 --- a/wapitiCore/main/wapiti.py +++ b/wapitiCore/main/wapiti.py @@ -242,6 +242,7 @@ async def wapiti_main(): "tasks": args.tasks, "headless": wap.headless_mode, "excluded_urls": wap.excluded_urls, + "max_attack_time" : args.max_attack_time } if "dns_endpoint" in args: