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..033799783 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 datetime import datetime from typing import Optional from os.path import join as path_join 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 (datetime.utcnow() - self.start).total_seconds() > 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 = datetime.utcnow() 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..dae0e32a0 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 datetime import datetime from typing import Optional, Iterator from os.path import join as path_join 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 = datetime.utcnow() 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 (datetime.utcnow() - self.start).total_seconds() > 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..df3eb83df 100644 --- a/wapitiCore/attack/mod_file.py +++ b/wapitiCore/attack/mod_file.py @@ -16,6 +16,7 @@ # 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 datetime import datetime from os.path import join as path_join from collections import defaultdict, namedtuple import re @@ -23,7 +24,7 @@ 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 = datetime.utcnow() 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 (datetime.utcnow() - self.start).total_seconds() > 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..6d9aceb94 100644 --- a/wapitiCore/attack/mod_nikto.py +++ b/wapitiCore/attack/mod_nikto.py @@ -18,6 +18,7 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import asyncio import csv +from datetime import datetime import re import os import random @@ -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 = datetime.utcnow() 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 (datetime.utcnow() - self.start).total_seconds() > 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..f8fb7afb1 100644 --- a/wapitiCore/attack/mod_timesql.py +++ b/wapitiCore/attack/mod_timesql.py @@ -16,6 +16,7 @@ # 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 datetime import datetime from math import ceil from typing import Optional, Iterator from os.path import join as path_join @@ -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 = datetime.utcnow() 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 (datetime.utcnow() - self.start).total_seconds() > 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..f2b4ff5ae 100644 --- a/wapitiCore/attack/mod_wp_enum.py +++ b/wapitiCore/attack/mod_wp_enum.py @@ -2,6 +2,7 @@ import re import xml import xml.etree.ElementTree as ET +from datetime import datetime from os.path import join as path_join from typing import Match, Optional @@ -98,6 +99,12 @@ async def detect_plugin(self, url): if self._stop_event.is_set(): break + if (datetime.utcnow() - self.start).total_seconds() > 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 (datetime.utcnow() - self.start).total_seconds() > 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 = datetime.utcnow() 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: