scythe-ttp 0.17.6__tar.gz → 0.18.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scythe_ttp-0.17.6/scythe_ttp.egg-info → scythe_ttp-0.18.1}/PKG-INFO +1 -1
- scythe_ttp-0.18.1/VERSION +1 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/cli/main.py +28 -16
- scythe_ttp-0.18.1/scythe/ttps/web/__init__.py +12 -0
- scythe_ttp-0.18.1/scythe/ttps/web/request_flooding.py +503 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1/scythe_ttp.egg-info}/PKG-INFO +1 -1
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/SOURCES.txt +1 -0
- scythe_ttp-0.17.6/VERSION +0 -1
- scythe_ttp-0.17.6/scythe/ttps/web/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/LICENSE +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/MANIFEST.in +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/README.md +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/pyproject.toml +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/requirements.txt +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/base.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/basic.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/bearer.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/cookie_jwt.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/base.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/default.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/human.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/machine.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/stealth.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/cli/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/executor.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/headers.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/ttp.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/actions.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/base.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/executor.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/base.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/batch.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/distributed.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/scale.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/payloads/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/payloads/generators.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/__init__.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/web/login_bruteforce.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/web/sql_injection.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/web/uuid_guessing.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/dependency_links.txt +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/entry_points.txt +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/requires.txt +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/top_level.txt +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/setup.cfg +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_api_models.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_authentication.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_behaviors.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_cli.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_cookie_jwt_auth.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_executor_modes.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_expected_results.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_feature_completeness.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_header_extraction.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_journeys.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_orchestrators.py +0 -0
- {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_ttp_api_mode.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.18.1
|
|
@@ -65,14 +65,14 @@ def check_version_in_response_header(args) -> bool:
|
|
|
65
65
|
return False
|
|
66
66
|
return True
|
|
67
67
|
|
|
68
|
-
def scythe_test_definition(args) ->
|
|
68
|
+
def scythe_test_definition(args) -> int:
|
|
69
69
|
# TODO: implement your test using Scythe primitives.
|
|
70
70
|
# Example placeholder that simply passes.
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
# Example usage with TTPExecutor:
|
|
73
73
|
# from scythe.core.executor import TTPExecutor
|
|
74
74
|
# from scythe.ttps.web.login_bruteforce import LoginBruteforceTTP
|
|
75
|
-
#
|
|
75
|
+
#
|
|
76
76
|
# ttp = LoginBruteforceTTP(
|
|
77
77
|
# payloads=['admin', 'root', 'test'],
|
|
78
78
|
# expected_result=False # Expect security controls to block attempts
|
|
@@ -80,12 +80,12 @@ def scythe_test_definition(args) -> bool:
|
|
|
80
80
|
# executor = TTPExecutor(ttp=ttp, target_url=args.url)
|
|
81
81
|
# executor.run()
|
|
82
82
|
# return executor.was_successful() # Returns True if all results matched expectations
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
# Example usage with JourneyExecutor:
|
|
85
85
|
# from scythe.journeys.executor import JourneyExecutor
|
|
86
86
|
# from scythe.journeys.base import Journey, Step
|
|
87
87
|
# from scythe.journeys.actions import NavigateAction, FillFormAction, ClickAction
|
|
88
|
-
#
|
|
88
|
+
#
|
|
89
89
|
# journey = Journey(
|
|
90
90
|
# name="Login Journey",
|
|
91
91
|
# description="Test user login flow",
|
|
@@ -95,19 +95,19 @@ def scythe_test_definition(args) -> bool:
|
|
|
95
95
|
# executor = JourneyExecutor(journey=journey, target_url=args.url)
|
|
96
96
|
# executor.run()
|
|
97
97
|
# return executor.was_successful() # Returns True if journey succeeded as expected
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
# Example usage with Orchestrators:
|
|
100
100
|
# from scythe.orchestrators.scale import ScaleOrchestrator
|
|
101
101
|
# from scythe.orchestrators.base import OrchestrationStrategy
|
|
102
|
-
#
|
|
102
|
+
#
|
|
103
103
|
# orchestrator = ScaleOrchestrator(
|
|
104
104
|
# strategy=OrchestrationStrategy.PARALLEL,
|
|
105
105
|
# max_workers=10
|
|
106
106
|
# )
|
|
107
107
|
# result = orchestrator.orchestrate_ttp(ttp=my_ttp, target_url=args.url, replications=100)
|
|
108
108
|
# return orchestrator.exit_code(result) == 0 # Returns True if all executions succeeded
|
|
109
|
-
|
|
110
|
-
return
|
|
109
|
+
|
|
110
|
+
return executor.exit_code() # assumes executor var
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
def main():
|
|
@@ -259,14 +259,14 @@ def main():
|
|
|
259
259
|
if check_url_available(args.url):
|
|
260
260
|
if args.gate_versions:
|
|
261
261
|
if check_version_in_response_header(args):
|
|
262
|
-
|
|
263
|
-
sys.exit(
|
|
262
|
+
exit_code = scythe_test_definition(args)
|
|
263
|
+
sys.exit(exit_code)
|
|
264
264
|
else:
|
|
265
265
|
print("No compatible version found in response header.")
|
|
266
266
|
sys.exit(1)
|
|
267
267
|
else:
|
|
268
|
-
|
|
269
|
-
sys.exit(
|
|
268
|
+
exit_code = scythe_test_definition(args)
|
|
269
|
+
sys.exit(exit_code)
|
|
270
270
|
else:
|
|
271
271
|
print("URL not available.")
|
|
272
272
|
sys.exit(1)
|
|
@@ -280,6 +280,13 @@ class ScytheCLIError(Exception):
|
|
|
280
280
|
pass
|
|
281
281
|
|
|
282
282
|
|
|
283
|
+
class ExitWithCode(Exception):
|
|
284
|
+
"""Exception to exit with a specific code from within Typer commands."""
|
|
285
|
+
def __init__(self, code: int):
|
|
286
|
+
self.code = code
|
|
287
|
+
super().__init__()
|
|
288
|
+
|
|
289
|
+
|
|
283
290
|
def _find_project_root(start: Optional[str] = None) -> Optional[str]:
|
|
284
291
|
"""Walk upwards from start (or cwd) to find a directory containing .scythe."""
|
|
285
292
|
cur = os.path.abspath(start or os.getcwd())
|
|
@@ -728,7 +735,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
728
735
|
code, output, version = _run_test(project_root, name, extra)
|
|
729
736
|
_record_run(project_root, name, code, output, version)
|
|
730
737
|
print(output)
|
|
731
|
-
|
|
738
|
+
# Raise exception to propagate exit code through Typer
|
|
739
|
+
if code != 0:
|
|
740
|
+
raise ExitWithCode(code)
|
|
741
|
+
return 0
|
|
732
742
|
|
|
733
743
|
db_app = typer.Typer(
|
|
734
744
|
no_args_is_help=True,
|
|
@@ -763,8 +773,10 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
763
773
|
app.add_typer(db_app, name="db")
|
|
764
774
|
|
|
765
775
|
try:
|
|
766
|
-
|
|
767
|
-
return
|
|
776
|
+
app()
|
|
777
|
+
return 0
|
|
778
|
+
except ExitWithCode as e:
|
|
779
|
+
return e.code
|
|
768
780
|
except ScytheCLIError as e:
|
|
769
781
|
print(f"Error: {e}", file=sys.stderr)
|
|
770
782
|
return 2
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .login_bruteforce import LoginBruteforceTTP
|
|
2
|
+
from .sql_injection import InputFieldInjector, URLManipulation
|
|
3
|
+
from .uuid_guessing import GuessUUIDInURL
|
|
4
|
+
from .request_flooding import RequestFloodingTTP
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'LoginBruteforceTTP',
|
|
8
|
+
'InputFieldInjector',
|
|
9
|
+
'URLManipulation',
|
|
10
|
+
'GuessUUIDInURL',
|
|
11
|
+
'RequestFloodingTTP'
|
|
12
|
+
]
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
from selenium.webdriver.common.by import By
|
|
2
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
3
|
+
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
|
4
|
+
from typing import Dict, Any, Optional, List, Generator
|
|
5
|
+
import requests
|
|
6
|
+
import time
|
|
7
|
+
import threading
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
from ...core.ttp import TTP
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RequestFloodingTTP(TTP):
|
|
15
|
+
"""
|
|
16
|
+
A TTP that emulates DDoS and request flooding attacks to test application resilience.
|
|
17
|
+
|
|
18
|
+
This TTP tests an application's ability to withstand high-volume request attacks
|
|
19
|
+
and rate limiting mechanisms by sending multiple rapid requests to target endpoints.
|
|
20
|
+
|
|
21
|
+
Supports two execution modes:
|
|
22
|
+
- UI mode: Uses Selenium to repeatedly interact with web pages/forms
|
|
23
|
+
- API mode: Makes rapid HTTP requests to API endpoints
|
|
24
|
+
|
|
25
|
+
Attack patterns include:
|
|
26
|
+
- Volume flooding: High number of requests in short time
|
|
27
|
+
- Slowloris-style: Slow, prolonged connections
|
|
28
|
+
- Burst flooding: Intermittent bursts of high traffic
|
|
29
|
+
- Resource exhaustion: Targeting expensive operations
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self,
|
|
33
|
+
target_endpoints: List[str] = None,
|
|
34
|
+
request_count: int = 100,
|
|
35
|
+
requests_per_second: float = 10.0,
|
|
36
|
+
attack_pattern: str = 'volume',
|
|
37
|
+
concurrent_threads: int = 5,
|
|
38
|
+
payload_data: Optional[Dict[str, Any]] = None,
|
|
39
|
+
http_method: str = 'GET',
|
|
40
|
+
form_selector: str = None,
|
|
41
|
+
submit_selector: str = None,
|
|
42
|
+
expected_result: bool = False,
|
|
43
|
+
authentication=None,
|
|
44
|
+
execution_mode: str = 'api',
|
|
45
|
+
success_indicators: Optional[Dict[str, Any]] = None,
|
|
46
|
+
user_agents: Optional[List[str]] = None,
|
|
47
|
+
randomize_timing: bool = True):
|
|
48
|
+
"""
|
|
49
|
+
Initialize the Request Flooding TTP.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
target_endpoints: List of endpoint paths to target (e.g., ['/api/search', '/login'])
|
|
53
|
+
request_count: Total number of requests to send per endpoint
|
|
54
|
+
requests_per_second: Target rate of requests (used for timing calculations)
|
|
55
|
+
attack_pattern: Type of attack - 'volume', 'slowloris', 'burst', 'resource_exhaustion'
|
|
56
|
+
concurrent_threads: Number of concurrent threads to use for requests
|
|
57
|
+
payload_data: Data to send in request body (API mode) or form fields (UI mode)
|
|
58
|
+
http_method: HTTP method to use ('GET', 'POST', 'PUT', 'DELETE')
|
|
59
|
+
form_selector: CSS selector for form to repeatedly submit (UI mode)
|
|
60
|
+
submit_selector: CSS selector for submit button (UI mode)
|
|
61
|
+
expected_result: False = expect app to resist/rate-limit, True = expect success
|
|
62
|
+
authentication: Optional authentication mechanism
|
|
63
|
+
execution_mode: 'ui' or 'api'
|
|
64
|
+
success_indicators: Dict defining what constitutes successful flooding detection
|
|
65
|
+
user_agents: List of user agents to rotate through (helps bypass simple filtering)
|
|
66
|
+
randomize_timing: Whether to randomize request timing to appear more natural
|
|
67
|
+
"""
|
|
68
|
+
super().__init__(
|
|
69
|
+
name="Request Flooding / DDoS Test",
|
|
70
|
+
description=f"Tests application resilience against {attack_pattern} flooding attacks with {request_count} requests",
|
|
71
|
+
expected_result=expected_result,
|
|
72
|
+
authentication=authentication,
|
|
73
|
+
execution_mode=execution_mode
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Core configuration
|
|
77
|
+
self.target_endpoints = target_endpoints or ['/']
|
|
78
|
+
self.request_count = request_count
|
|
79
|
+
self.requests_per_second = requests_per_second
|
|
80
|
+
self.attack_pattern = attack_pattern.lower()
|
|
81
|
+
self.concurrent_threads = min(concurrent_threads, 20) # Cap to prevent system overload
|
|
82
|
+
self.payload_data = payload_data or {}
|
|
83
|
+
self.http_method = http_method.upper()
|
|
84
|
+
|
|
85
|
+
# UI mode configuration
|
|
86
|
+
self.form_selector = form_selector
|
|
87
|
+
self.submit_selector = submit_selector
|
|
88
|
+
|
|
89
|
+
# Attack sophistication
|
|
90
|
+
self.user_agents = user_agents or [
|
|
91
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
92
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
93
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
|
|
94
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101'
|
|
95
|
+
]
|
|
96
|
+
self.randomize_timing = randomize_timing
|
|
97
|
+
|
|
98
|
+
# Success/failure detection
|
|
99
|
+
self.success_indicators = success_indicators or {
|
|
100
|
+
'rate_limit_status_codes': [429, 503, 502],
|
|
101
|
+
'error_keywords': ['rate limit', 'too many requests', 'service unavailable'],
|
|
102
|
+
'max_response_time': 30.0, # Consider slow responses as potential DoS impact
|
|
103
|
+
'expected_success_rate': 0.1 # Expect most requests to be blocked if defenses work
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Attack results tracking
|
|
107
|
+
self.attack_results = {
|
|
108
|
+
'total_requests': 0,
|
|
109
|
+
'successful_requests': 0,
|
|
110
|
+
'failed_requests': 0,
|
|
111
|
+
'rate_limited_requests': 0,
|
|
112
|
+
'error_responses': 0,
|
|
113
|
+
'avg_response_time': 0.0,
|
|
114
|
+
'max_response_time': 0.0,
|
|
115
|
+
'responses_by_status': {},
|
|
116
|
+
'attack_effectiveness': 0.0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def get_payloads(self) -> Generator[Dict[str, Any], None, None]:
|
|
120
|
+
"""
|
|
121
|
+
Generates attack payloads based on the configured attack pattern.
|
|
122
|
+
Each payload contains timing and configuration data for the attack.
|
|
123
|
+
"""
|
|
124
|
+
base_delay = 1.0 / self.requests_per_second if self.requests_per_second > 0 else 0.1
|
|
125
|
+
|
|
126
|
+
for i in range(self.request_count):
|
|
127
|
+
payload = {
|
|
128
|
+
'request_id': i,
|
|
129
|
+
'endpoint': self.target_endpoints[i % len(self.target_endpoints)],
|
|
130
|
+
'data': self.payload_data.copy(),
|
|
131
|
+
'user_agent': random.choice(self.user_agents),
|
|
132
|
+
'delay': self._calculate_delay(i, base_delay),
|
|
133
|
+
'timeout': self._calculate_timeout(i)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Add attack-pattern specific modifications
|
|
137
|
+
if self.attack_pattern == 'burst':
|
|
138
|
+
# Create bursts every 10 requests
|
|
139
|
+
if i % 10 == 0:
|
|
140
|
+
payload['delay'] = 0.05 # Very fast burst
|
|
141
|
+
else:
|
|
142
|
+
payload['delay'] = base_delay * 3 # Slower between bursts
|
|
143
|
+
|
|
144
|
+
elif self.attack_pattern == 'slowloris':
|
|
145
|
+
payload['timeout'] = 60.0 # Very long timeout
|
|
146
|
+
payload['delay'] = base_delay * 2 # Slower rate but longer connections
|
|
147
|
+
|
|
148
|
+
elif self.attack_pattern == 'resource_exhaustion':
|
|
149
|
+
# Add resource-intensive parameters
|
|
150
|
+
payload['data'].update({
|
|
151
|
+
'limit': 10000, # Request large datasets
|
|
152
|
+
'search': '*', # Broad search terms
|
|
153
|
+
'recursive': True
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
yield payload
|
|
157
|
+
|
|
158
|
+
def _calculate_delay(self, request_index: int, base_delay: float) -> float:
|
|
159
|
+
"""Calculate delay between requests based on pattern and randomization."""
|
|
160
|
+
if not self.randomize_timing:
|
|
161
|
+
return base_delay
|
|
162
|
+
|
|
163
|
+
# Add randomization (±25% of base delay)
|
|
164
|
+
jitter = base_delay * 0.25 * (random.random() - 0.5) * 2
|
|
165
|
+
return max(0.01, base_delay + jitter)
|
|
166
|
+
|
|
167
|
+
def _calculate_timeout(self, request_index: int) -> float:
|
|
168
|
+
"""Calculate request timeout based on attack pattern."""
|
|
169
|
+
if self.attack_pattern == 'slowloris':
|
|
170
|
+
return 60.0
|
|
171
|
+
elif self.attack_pattern == 'resource_exhaustion':
|
|
172
|
+
return 30.0
|
|
173
|
+
else:
|
|
174
|
+
return 10.0
|
|
175
|
+
|
|
176
|
+
def execute_step(self, driver: WebDriver, payload: Dict[str, Any]) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Executes a single flooding request in UI mode.
|
|
179
|
+
For UI mode, this repeatedly submits forms or navigates to pages.
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
endpoint = payload['endpoint']
|
|
183
|
+
delay = payload['delay']
|
|
184
|
+
|
|
185
|
+
# Wait for the calculated delay
|
|
186
|
+
if delay > 0:
|
|
187
|
+
time.sleep(delay)
|
|
188
|
+
|
|
189
|
+
# Navigate to the target endpoint
|
|
190
|
+
current_url = driver.current_url
|
|
191
|
+
base_url = current_url.split('?')[0].rstrip('/')
|
|
192
|
+
target_url = f"{base_url}{endpoint}"
|
|
193
|
+
|
|
194
|
+
start_time = time.time()
|
|
195
|
+
driver.get(target_url)
|
|
196
|
+
|
|
197
|
+
# If we have form selectors, submit the form
|
|
198
|
+
if self.form_selector and self.submit_selector:
|
|
199
|
+
try:
|
|
200
|
+
# Fill form with payload data
|
|
201
|
+
for field_name, field_value in payload['data'].items():
|
|
202
|
+
try:
|
|
203
|
+
field = driver.find_element(By.NAME, field_name)
|
|
204
|
+
field.clear()
|
|
205
|
+
field.send_keys(str(field_value))
|
|
206
|
+
except NoSuchElementException:
|
|
207
|
+
# Try by ID if name doesn't work
|
|
208
|
+
try:
|
|
209
|
+
field = driver.find_element(By.ID, field_name)
|
|
210
|
+
field.clear()
|
|
211
|
+
field.send_keys(str(field_value))
|
|
212
|
+
except NoSuchElementException:
|
|
213
|
+
continue # Skip this field if not found
|
|
214
|
+
|
|
215
|
+
# Submit the form
|
|
216
|
+
submit_btn = driver.find_element(By.CSS_SELECTOR, self.submit_selector)
|
|
217
|
+
submit_btn.click()
|
|
218
|
+
|
|
219
|
+
except NoSuchElementException:
|
|
220
|
+
pass # Continue even if form submission fails
|
|
221
|
+
|
|
222
|
+
# Record timing
|
|
223
|
+
response_time = time.time() - start_time
|
|
224
|
+
self._record_ui_result(response_time, driver.current_url)
|
|
225
|
+
|
|
226
|
+
except TimeoutException:
|
|
227
|
+
self._record_ui_result(payload['timeout'], None, timeout=True)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
self._record_ui_result(0.0, None, error=str(e))
|
|
230
|
+
|
|
231
|
+
def verify_result(self, driver: WebDriver) -> bool:
|
|
232
|
+
"""
|
|
233
|
+
Verifies the outcome of the flooding attack in UI mode.
|
|
234
|
+
Checks for rate limiting, error pages, or performance degradation.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
page_source = driver.page_source.lower()
|
|
238
|
+
current_url = driver.current_url.lower()
|
|
239
|
+
|
|
240
|
+
# Check for rate limiting indicators
|
|
241
|
+
rate_limit_indicators = [
|
|
242
|
+
'rate limit', 'too many requests', 'service unavailable',
|
|
243
|
+
'temporarily unavailable', 'error 429', 'error 503',
|
|
244
|
+
'please wait', 'slow down', 'blocked'
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
for indicator in rate_limit_indicators:
|
|
248
|
+
if indicator in page_source or indicator in current_url:
|
|
249
|
+
return not self.expected_result # Rate limiting found
|
|
250
|
+
|
|
251
|
+
# If no rate limiting found and we expected defenses, that's a failure
|
|
252
|
+
return self.expected_result
|
|
253
|
+
|
|
254
|
+
except Exception:
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def execute_step_api(self, session: requests.Session, payload: Dict[str, Any], context: Dict[str, Any]) -> requests.Response:
|
|
258
|
+
"""
|
|
259
|
+
Executes a single flooding request in API mode.
|
|
260
|
+
This is where the actual HTTP flooding happens.
|
|
261
|
+
"""
|
|
262
|
+
from urllib.parse import urljoin
|
|
263
|
+
|
|
264
|
+
# Build the full URL
|
|
265
|
+
base_url = context.get('target_url', '')
|
|
266
|
+
if not base_url:
|
|
267
|
+
raise ValueError("target_url must be set in context for API mode")
|
|
268
|
+
|
|
269
|
+
endpoint = payload['endpoint']
|
|
270
|
+
url = urljoin(base_url, endpoint)
|
|
271
|
+
|
|
272
|
+
# Prepare headers
|
|
273
|
+
headers = {
|
|
274
|
+
'User-Agent': payload['user_agent']
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# Merge auth headers from context
|
|
278
|
+
auth_headers = context.get('auth_headers', {})
|
|
279
|
+
if auth_headers:
|
|
280
|
+
headers.update(auth_headers)
|
|
281
|
+
|
|
282
|
+
# Wait for the calculated delay
|
|
283
|
+
delay = payload['delay']
|
|
284
|
+
if delay > 0:
|
|
285
|
+
time.sleep(delay)
|
|
286
|
+
|
|
287
|
+
# Honor existing rate limiting from previous requests
|
|
288
|
+
resume_at = context.get('rate_limit_resume_at')
|
|
289
|
+
if resume_at and time.time() < resume_at:
|
|
290
|
+
# Skip this request due to rate limiting
|
|
291
|
+
raise requests.exceptions.RequestException("Rate limited")
|
|
292
|
+
|
|
293
|
+
# Make the request
|
|
294
|
+
start_time = time.time()
|
|
295
|
+
try:
|
|
296
|
+
if self.http_method == 'GET':
|
|
297
|
+
response = session.get(
|
|
298
|
+
url,
|
|
299
|
+
params=payload['data'],
|
|
300
|
+
headers=headers,
|
|
301
|
+
timeout=payload['timeout']
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
response = session.request(
|
|
305
|
+
self.http_method,
|
|
306
|
+
url,
|
|
307
|
+
json=payload['data'],
|
|
308
|
+
headers=headers,
|
|
309
|
+
timeout=payload['timeout']
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Record the result
|
|
313
|
+
response_time = time.time() - start_time
|
|
314
|
+
self._record_api_result(response, response_time, context)
|
|
315
|
+
|
|
316
|
+
return response
|
|
317
|
+
|
|
318
|
+
except requests.exceptions.Timeout:
|
|
319
|
+
response_time = payload['timeout']
|
|
320
|
+
self._record_api_result(None, response_time, context, timeout=True)
|
|
321
|
+
raise
|
|
322
|
+
except Exception as e:
|
|
323
|
+
response_time = time.time() - start_time
|
|
324
|
+
self._record_api_result(None, response_time, context, error=str(e))
|
|
325
|
+
raise
|
|
326
|
+
|
|
327
|
+
def verify_result_api(self, response: requests.Response, context: Dict[str, Any]) -> bool:
|
|
328
|
+
"""
|
|
329
|
+
Verifies if the flooding attack was effective or if defenses kicked in.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
response: The response from execute_step_api
|
|
333
|
+
context: Shared context dictionary
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
True if attack behavior detected (rate limiting, errors),
|
|
337
|
+
False if requests succeeded without defensive measures
|
|
338
|
+
"""
|
|
339
|
+
# Check if we have accumulated enough results to make a determination
|
|
340
|
+
total_requests = self.attack_results['total_requests']
|
|
341
|
+
|
|
342
|
+
# Early determination if we have enough data
|
|
343
|
+
if total_requests >= min(20, self.request_count // 2):
|
|
344
|
+
success_rate = self.attack_results['successful_requests'] / total_requests
|
|
345
|
+
rate_limit_rate = self.attack_results['rate_limited_requests'] / total_requests
|
|
346
|
+
|
|
347
|
+
# If we expected defenses (expected_result=False)
|
|
348
|
+
if not self.expected_result:
|
|
349
|
+
# Good defense: high rate limiting, low success rate
|
|
350
|
+
if rate_limit_rate > 0.3 or success_rate < self.success_indicators['expected_success_rate']:
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
# If we expected success (expected_result=True)
|
|
354
|
+
else:
|
|
355
|
+
# Attack successful: high success rate, low rate limiting
|
|
356
|
+
if success_rate > 0.7 and rate_limit_rate < 0.2:
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
# Check immediate response indicators
|
|
360
|
+
if response:
|
|
361
|
+
# Rate limiting detected
|
|
362
|
+
if response.status_code in self.success_indicators['rate_limit_status_codes']:
|
|
363
|
+
return not self.expected_result
|
|
364
|
+
|
|
365
|
+
# Check response content for defensive indicators
|
|
366
|
+
try:
|
|
367
|
+
response_text = response.text.lower()
|
|
368
|
+
for keyword in self.success_indicators['error_keywords']:
|
|
369
|
+
if keyword in response_text:
|
|
370
|
+
return not self.expected_result
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
# Default to continuing the test
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
def _record_ui_result(self, response_time: float, url: str = None, timeout: bool = False, error: str = None):
|
|
378
|
+
"""Record results from UI mode execution."""
|
|
379
|
+
self.attack_results['total_requests'] += 1
|
|
380
|
+
|
|
381
|
+
if timeout:
|
|
382
|
+
self.attack_results['failed_requests'] += 1
|
|
383
|
+
elif error:
|
|
384
|
+
self.attack_results['error_responses'] += 1
|
|
385
|
+
elif url and any(indicator in url for indicator in ['error', 'limit', '429', '503']):
|
|
386
|
+
self.attack_results['rate_limited_requests'] += 1
|
|
387
|
+
else:
|
|
388
|
+
self.attack_results['successful_requests'] += 1
|
|
389
|
+
|
|
390
|
+
# Update timing stats
|
|
391
|
+
self.attack_results['max_response_time'] = max(
|
|
392
|
+
self.attack_results['max_response_time'], response_time
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Calculate rolling average
|
|
396
|
+
total = self.attack_results['total_requests']
|
|
397
|
+
current_avg = self.attack_results['avg_response_time']
|
|
398
|
+
self.attack_results['avg_response_time'] = (
|
|
399
|
+
(current_avg * (total - 1) + response_time) / total
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def _record_api_result(self, response: requests.Response = None, response_time: float = 0.0,
|
|
403
|
+
context: Dict[str, Any] = None, timeout: bool = False, error: str = None):
|
|
404
|
+
"""Record results from API mode execution."""
|
|
405
|
+
self.attack_results['total_requests'] += 1
|
|
406
|
+
|
|
407
|
+
if timeout:
|
|
408
|
+
self.attack_results['failed_requests'] += 1
|
|
409
|
+
elif error:
|
|
410
|
+
self.attack_results['error_responses'] += 1
|
|
411
|
+
elif response:
|
|
412
|
+
status_code = response.status_code
|
|
413
|
+
|
|
414
|
+
# Track status code distribution
|
|
415
|
+
if status_code not in self.attack_results['responses_by_status']:
|
|
416
|
+
self.attack_results['responses_by_status'][status_code] = 0
|
|
417
|
+
self.attack_results['responses_by_status'][status_code] += 1
|
|
418
|
+
|
|
419
|
+
# Categorize the response
|
|
420
|
+
if status_code in self.success_indicators['rate_limit_status_codes']:
|
|
421
|
+
self.attack_results['rate_limited_requests'] += 1
|
|
422
|
+
|
|
423
|
+
# Update rate limiting in context
|
|
424
|
+
if context and response.headers.get('Retry-After'):
|
|
425
|
+
try:
|
|
426
|
+
retry_after = int(response.headers['Retry-After'])
|
|
427
|
+
context['rate_limit_resume_at'] = time.time() + min(retry_after, 60)
|
|
428
|
+
except (ValueError, TypeError):
|
|
429
|
+
context['rate_limit_resume_at'] = time.time() + 5
|
|
430
|
+
|
|
431
|
+
elif 200 <= status_code < 300:
|
|
432
|
+
self.attack_results['successful_requests'] += 1
|
|
433
|
+
else:
|
|
434
|
+
self.attack_results['error_responses'] += 1
|
|
435
|
+
|
|
436
|
+
# Update timing statistics
|
|
437
|
+
self.attack_results['max_response_time'] = max(
|
|
438
|
+
self.attack_results['max_response_time'], response_time
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Calculate rolling average response time
|
|
442
|
+
total = self.attack_results['total_requests']
|
|
443
|
+
current_avg = self.attack_results['avg_response_time']
|
|
444
|
+
self.attack_results['avg_response_time'] = (
|
|
445
|
+
(current_avg * (total - 1) + response_time) / total
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Calculate attack effectiveness score
|
|
449
|
+
if total > 0:
|
|
450
|
+
success_rate = self.attack_results['successful_requests'] / total
|
|
451
|
+
rate_limit_rate = self.attack_results['rate_limited_requests'] / total
|
|
452
|
+
|
|
453
|
+
if self.expected_result:
|
|
454
|
+
# Higher success rate = more effective attack
|
|
455
|
+
self.attack_results['attack_effectiveness'] = success_rate * 100
|
|
456
|
+
else:
|
|
457
|
+
# Higher rate limiting = more effective defenses (which we want to detect)
|
|
458
|
+
self.attack_results['attack_effectiveness'] = rate_limit_rate * 100
|
|
459
|
+
|
|
460
|
+
def get_attack_summary(self) -> Dict[str, Any]:
|
|
461
|
+
"""
|
|
462
|
+
Returns a comprehensive summary of the attack results.
|
|
463
|
+
Useful for detailed analysis and reporting.
|
|
464
|
+
"""
|
|
465
|
+
total = self.attack_results['total_requests']
|
|
466
|
+
if total == 0:
|
|
467
|
+
return {"error": "No requests completed"}
|
|
468
|
+
|
|
469
|
+
summary = {
|
|
470
|
+
"attack_pattern": self.attack_pattern,
|
|
471
|
+
"total_requests": total,
|
|
472
|
+
"success_rate": (self.attack_results['successful_requests'] / total) * 100,
|
|
473
|
+
"rate_limit_rate": (self.attack_results['rate_limited_requests'] / total) * 100,
|
|
474
|
+
"error_rate": (self.attack_results['error_responses'] / total) * 100,
|
|
475
|
+
"avg_response_time": round(self.attack_results['avg_response_time'], 3),
|
|
476
|
+
"max_response_time": round(self.attack_results['max_response_time'], 3),
|
|
477
|
+
"attack_effectiveness": round(self.attack_results['attack_effectiveness'], 1),
|
|
478
|
+
"status_code_distribution": self.attack_results['responses_by_status'],
|
|
479
|
+
"defense_assessment": self._assess_defenses()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return summary
|
|
483
|
+
|
|
484
|
+
def _assess_defenses(self) -> str:
|
|
485
|
+
"""Assess the effectiveness of the target's defensive measures."""
|
|
486
|
+
total = self.attack_results['total_requests']
|
|
487
|
+
if total == 0:
|
|
488
|
+
return "Insufficient data"
|
|
489
|
+
|
|
490
|
+
success_rate = self.attack_results['successful_requests'] / total
|
|
491
|
+
rate_limit_rate = self.attack_results['rate_limited_requests'] / total
|
|
492
|
+
avg_response_time = self.attack_results['avg_response_time']
|
|
493
|
+
|
|
494
|
+
if rate_limit_rate > 0.5:
|
|
495
|
+
return "Strong rate limiting detected - Good defenses"
|
|
496
|
+
elif rate_limit_rate > 0.2:
|
|
497
|
+
return "Moderate rate limiting detected - Basic defenses"
|
|
498
|
+
elif avg_response_time > 10.0:
|
|
499
|
+
return "Performance degradation detected - Possible DoS impact"
|
|
500
|
+
elif success_rate > 0.8:
|
|
501
|
+
return "High success rate - Weak or no defenses detected"
|
|
502
|
+
else:
|
|
503
|
+
return "Mixed results - Some defensive measures present"
|
|
@@ -36,6 +36,7 @@ scythe/payloads/generators.py
|
|
|
36
36
|
scythe/ttps/__init__.py
|
|
37
37
|
scythe/ttps/web/__init__.py
|
|
38
38
|
scythe/ttps/web/login_bruteforce.py
|
|
39
|
+
scythe/ttps/web/request_flooding.py
|
|
39
40
|
scythe/ttps/web/sql_injection.py
|
|
40
41
|
scythe/ttps/web/uuid_guessing.py
|
|
41
42
|
scythe_ttp.egg-info/PKG-INFO
|
scythe_ttp-0.17.6/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.17.6
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|