scythe-ttp 0.17.1__py3-none-any.whl → 0.17.3__py3-none-any.whl
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.
Potentially problematic release.
This version of scythe-ttp might be problematic. Click here for more details.
- scythe/core/executor.py +116 -2
- scythe/core/ttp.py +61 -3
- scythe/journeys/actions.py +151 -69
- scythe/ttps/web/login_bruteforce.py +138 -7
- scythe/ttps/web/sql_injection.py +232 -15
- {scythe_ttp-0.17.1.dist-info → scythe_ttp-0.17.3.dist-info}/METADATA +1 -1
- {scythe_ttp-0.17.1.dist-info → scythe_ttp-0.17.3.dist-info}/RECORD +11 -11
- {scythe_ttp-0.17.1.dist-info → scythe_ttp-0.17.3.dist-info}/WHEEL +0 -0
- {scythe_ttp-0.17.1.dist-info → scythe_ttp-0.17.3.dist-info}/entry_points.txt +0 -0
- {scythe_ttp-0.17.1.dist-info → scythe_ttp-0.17.3.dist-info}/licenses/LICENSE +0 -0
- {scythe_ttp-0.17.1.dist-info → scythe_ttp-0.17.3.dist-info}/top_level.txt +0 -0
scythe/core/executor.py
CHANGED
|
@@ -3,9 +3,10 @@ import logging
|
|
|
3
3
|
from selenium import webdriver
|
|
4
4
|
from selenium.webdriver.chrome.options import Options
|
|
5
5
|
from .ttp import TTP
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
7
|
from ..behaviors.base import Behavior
|
|
8
8
|
from .headers import HeaderExtractor
|
|
9
|
+
import requests
|
|
9
10
|
|
|
10
11
|
# Configure logging
|
|
11
12
|
logging.basicConfig(
|
|
@@ -60,7 +61,18 @@ class TTPExecutor:
|
|
|
60
61
|
self.logger.info(f"Using behavior: {self.behavior.name}")
|
|
61
62
|
self.logger.info(f"Behavior description: {self.behavior.description}")
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
# Check execution mode
|
|
65
|
+
if self.ttp.execution_mode == 'api':
|
|
66
|
+
self.logger.info("Execution mode: API")
|
|
67
|
+
self._run_api_mode()
|
|
68
|
+
return
|
|
69
|
+
else:
|
|
70
|
+
self.logger.info("Execution mode: UI")
|
|
71
|
+
self._setup_driver()
|
|
72
|
+
self._run_ui_mode()
|
|
73
|
+
|
|
74
|
+
def _run_ui_mode(self):
|
|
75
|
+
"""Execute TTP in UI mode using Selenium."""
|
|
64
76
|
|
|
65
77
|
try:
|
|
66
78
|
# Handle authentication if required
|
|
@@ -172,6 +184,108 @@ class TTPExecutor:
|
|
|
172
184
|
finally:
|
|
173
185
|
self._cleanup()
|
|
174
186
|
|
|
187
|
+
def _run_api_mode(self):
|
|
188
|
+
"""Execute TTP in API mode using requests."""
|
|
189
|
+
session = requests.Session()
|
|
190
|
+
context: Dict[str, Any] = {
|
|
191
|
+
'target_url': self.target_url,
|
|
192
|
+
'auth_headers': {},
|
|
193
|
+
'rate_limit_resume_at': None
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# Handle authentication if required (API mode)
|
|
198
|
+
if self.ttp.requires_authentication():
|
|
199
|
+
auth_name = self.ttp.authentication.name if self.ttp.authentication else "Unknown"
|
|
200
|
+
self.logger.info(f"Authentication required for TTP: {auth_name}")
|
|
201
|
+
|
|
202
|
+
# Try to get auth headers directly
|
|
203
|
+
try:
|
|
204
|
+
if hasattr(self.ttp.authentication, 'get_auth_headers'):
|
|
205
|
+
auth_headers = self.ttp.authentication.get_auth_headers() or {}
|
|
206
|
+
context['auth_headers'] = auth_headers
|
|
207
|
+
session.headers.update(auth_headers)
|
|
208
|
+
self.logger.info("Authentication headers applied")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
self.logger.warning(f"Failed to get auth headers: {e}")
|
|
211
|
+
|
|
212
|
+
consecutive_failures = 0
|
|
213
|
+
|
|
214
|
+
for i, payload in enumerate(self.ttp.get_payloads(), 1):
|
|
215
|
+
# Check if behavior wants to continue
|
|
216
|
+
if self.behavior and not self.behavior.should_continue(i, consecutive_failures):
|
|
217
|
+
self.logger.info("Behavior requested to stop execution")
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
self.logger.info(f"Attempt {i}: Executing with payload -> '{payload}'")
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# Execute API request
|
|
224
|
+
response = self.ttp.execute_step_api(session, payload, context)
|
|
225
|
+
|
|
226
|
+
# Use behavior delay if available, otherwise use default
|
|
227
|
+
if self.behavior:
|
|
228
|
+
step_delay = self.behavior.get_step_delay(i)
|
|
229
|
+
else:
|
|
230
|
+
step_delay = self.delay
|
|
231
|
+
|
|
232
|
+
time.sleep(step_delay)
|
|
233
|
+
|
|
234
|
+
# Verify result
|
|
235
|
+
success = self.ttp.verify_result_api(response, context)
|
|
236
|
+
|
|
237
|
+
# Compare actual result with expected result
|
|
238
|
+
if success:
|
|
239
|
+
consecutive_failures = 0
|
|
240
|
+
|
|
241
|
+
# Extract target version from response headers
|
|
242
|
+
target_version = response.headers.get('X-SCYTHE-TARGET-VERSION') or response.headers.get('x-scythe-target-version')
|
|
243
|
+
|
|
244
|
+
result_entry = {
|
|
245
|
+
'payload': payload,
|
|
246
|
+
'url': response.url if hasattr(response, 'url') else self.target_url,
|
|
247
|
+
'expected': self.ttp.expected_result,
|
|
248
|
+
'actual': True,
|
|
249
|
+
'target_version': target_version
|
|
250
|
+
}
|
|
251
|
+
self.results.append(result_entry)
|
|
252
|
+
|
|
253
|
+
if self.ttp.expected_result:
|
|
254
|
+
version_info = f" | Version: {target_version}" if target_version else ""
|
|
255
|
+
self.logger.info(f"EXPECTED SUCCESS: '{payload}'{version_info}")
|
|
256
|
+
else:
|
|
257
|
+
version_info = f" | Version: {target_version}" if target_version else ""
|
|
258
|
+
self.logger.warning(f"UNEXPECTED SUCCESS: '{payload}' (expected to fail){version_info}")
|
|
259
|
+
self.has_test_failures = True
|
|
260
|
+
else:
|
|
261
|
+
consecutive_failures += 1
|
|
262
|
+
if self.ttp.expected_result:
|
|
263
|
+
self.logger.info(f"EXPECTED FAILURE: '{payload}' (security control working)")
|
|
264
|
+
self.has_test_failures = True
|
|
265
|
+
else:
|
|
266
|
+
self.logger.info(f"EXPECTED FAILURE: '{payload}'")
|
|
267
|
+
|
|
268
|
+
except Exception as step_error:
|
|
269
|
+
consecutive_failures += 1
|
|
270
|
+
self.logger.error(f"Error during step {i}: {step_error}")
|
|
271
|
+
|
|
272
|
+
# Let behavior handle the error
|
|
273
|
+
if self.behavior:
|
|
274
|
+
if not self.behavior.on_error(step_error, i):
|
|
275
|
+
self.logger.info("Behavior requested to stop due to error")
|
|
276
|
+
break
|
|
277
|
+
else:
|
|
278
|
+
# Default behavior: continue on most errors
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
except KeyboardInterrupt:
|
|
282
|
+
self.logger.info("Test interrupted by user.")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
self.logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
|
285
|
+
finally:
|
|
286
|
+
session.close()
|
|
287
|
+
self._cleanup()
|
|
288
|
+
|
|
175
289
|
def _cleanup(self):
|
|
176
290
|
"""Closes the WebDriver and prints a summary."""
|
|
177
291
|
if self.driver:
|
scythe/core/ttp.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Generator, Any, Optional, TYPE_CHECKING
|
|
2
|
+
from typing import Generator, Any, Optional, TYPE_CHECKING, Dict
|
|
3
3
|
from selenium.webdriver.remote.webdriver import WebDriver
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from ..auth.base import Authentication
|
|
7
|
+
import requests
|
|
7
8
|
|
|
8
9
|
class TTP(ABC):
|
|
9
10
|
"""
|
|
@@ -11,9 +12,14 @@ class TTP(ABC):
|
|
|
11
12
|
|
|
12
13
|
Each TTP implementation must define how to generate payloads, how to
|
|
13
14
|
execute a test step with a given payload, and how to verify the outcome.
|
|
15
|
+
|
|
16
|
+
TTPs can operate in two modes:
|
|
17
|
+
- 'ui': Uses Selenium WebDriver to interact with web UI (default)
|
|
18
|
+
- 'api': Uses requests library to interact directly with backend APIs
|
|
14
19
|
"""
|
|
15
20
|
|
|
16
|
-
def __init__(self, name: str, description: str, expected_result: bool = True,
|
|
21
|
+
def __init__(self, name: str, description: str, expected_result: bool = True,
|
|
22
|
+
authentication: Optional['Authentication'] = None, execution_mode: str = 'ui'):
|
|
17
23
|
"""
|
|
18
24
|
Initialize a TTP.
|
|
19
25
|
|
|
@@ -24,11 +30,13 @@ class TTP(ABC):
|
|
|
24
30
|
True means we expect to find vulnerabilities/success conditions.
|
|
25
31
|
False means we expect the security controls to prevent success.
|
|
26
32
|
authentication: Optional authentication mechanism to use before executing TTP
|
|
33
|
+
execution_mode: Execution mode - 'ui' for Selenium-based UI testing or 'api' for direct API testing
|
|
27
34
|
"""
|
|
28
35
|
self.name = name
|
|
29
36
|
self.description = description
|
|
30
37
|
self.expected_result = expected_result
|
|
31
38
|
self.authentication = authentication
|
|
39
|
+
self.execution_mode = execution_mode.lower()
|
|
32
40
|
|
|
33
41
|
@abstractmethod
|
|
34
42
|
def get_payloads(self) -> Generator[Any, None, None]:
|
|
@@ -80,9 +88,59 @@ class TTP(ABC):
|
|
|
80
88
|
@abstractmethod
|
|
81
89
|
def verify_result(self, driver: WebDriver) -> bool:
|
|
82
90
|
"""
|
|
83
|
-
Verifies the outcome of the executed step.
|
|
91
|
+
Verifies the outcome of the executed step in UI mode.
|
|
84
92
|
|
|
85
93
|
Returns:
|
|
86
94
|
True if the test indicates a potential success/vulnerability, False otherwise.
|
|
87
95
|
"""
|
|
88
96
|
pass
|
|
97
|
+
|
|
98
|
+
def execute_step_api(self, session: 'requests.Session', payload: Any, context: Dict[str, Any]) -> 'requests.Response':
|
|
99
|
+
"""
|
|
100
|
+
Executes a single test action using the provided payload via API request.
|
|
101
|
+
This method should be overridden by TTPs that support API mode.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
session: requests.Session instance for making HTTP requests
|
|
105
|
+
payload: The payload to use for this test iteration
|
|
106
|
+
context: Shared context dictionary for storing state and auth headers
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
requests.Response object from the API call
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
NotImplementedError: If the TTP does not support API mode
|
|
113
|
+
"""
|
|
114
|
+
raise NotImplementedError(f"{self.name} does not support API execution mode")
|
|
115
|
+
|
|
116
|
+
def verify_result_api(self, response: 'requests.Response', context: Dict[str, Any]) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Verifies the outcome of the executed step in API mode.
|
|
119
|
+
This method should be overridden by TTPs that support API mode.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
response: The requests.Response object from execute_step_api
|
|
123
|
+
context: Shared context dictionary for accessing state
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if the test indicates a potential success/vulnerability, False otherwise
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
NotImplementedError: If the TTP does not support API mode
|
|
130
|
+
"""
|
|
131
|
+
raise NotImplementedError(f"{self.name} does not support API result verification in API mode")
|
|
132
|
+
|
|
133
|
+
def supports_api_mode(self) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Check if this TTP implementation supports API execution mode.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if API mode is supported, False otherwise
|
|
139
|
+
"""
|
|
140
|
+
# Check if the TTP has overridden the API methods
|
|
141
|
+
try:
|
|
142
|
+
# Try to call the method on the class to see if it's been overridden
|
|
143
|
+
return (type(self).execute_step_api != TTP.execute_step_api or
|
|
144
|
+
type(self).verify_result_api != TTP.verify_result_api)
|
|
145
|
+
except Exception:
|
|
146
|
+
return False
|
scythe/journeys/actions.py
CHANGED
|
@@ -439,81 +439,163 @@ class TTPAction(Action):
|
|
|
439
439
|
True if TTP execution matches expected result, False otherwise
|
|
440
440
|
"""
|
|
441
441
|
try:
|
|
442
|
-
#
|
|
443
|
-
if self.
|
|
444
|
-
|
|
445
|
-
elif 'current_url' in context:
|
|
446
|
-
url = context['current_url']
|
|
442
|
+
# Check execution mode
|
|
443
|
+
if self.ttp.execution_mode == 'api':
|
|
444
|
+
return self._execute_api_mode(driver, context)
|
|
447
445
|
else:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
446
|
+
return self._execute_ui_mode(driver, context)
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
self.store_result('error', str(e))
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
def _execute_ui_mode(self, driver: WebDriver, context: Dict[str, Any]) -> bool:
|
|
453
|
+
"""Execute TTP in UI mode using Selenium."""
|
|
454
|
+
# Determine target URL
|
|
455
|
+
if self.target_url:
|
|
456
|
+
url = self.target_url
|
|
457
|
+
elif 'current_url' in context:
|
|
458
|
+
url = context['current_url']
|
|
459
|
+
else:
|
|
460
|
+
url = driver.current_url
|
|
461
|
+
|
|
462
|
+
# Navigate to URL if needed
|
|
463
|
+
if url != driver.current_url:
|
|
464
|
+
driver.get(url)
|
|
465
|
+
|
|
466
|
+
# Execute TTP authentication if required
|
|
467
|
+
if self.ttp.requires_authentication():
|
|
468
|
+
auth_success = self.ttp.authenticate(driver, url)
|
|
469
|
+
if not auth_success:
|
|
470
|
+
self.store_result('error', 'TTP authentication failed')
|
|
471
|
+
return False
|
|
472
|
+
|
|
473
|
+
# Execute TTP payloads
|
|
474
|
+
ttp_results = []
|
|
475
|
+
success_count = 0
|
|
476
|
+
total_count = 0
|
|
477
|
+
|
|
478
|
+
for payload in self.ttp.get_payloads():
|
|
479
|
+
total_count += 1
|
|
465
480
|
|
|
466
|
-
|
|
467
|
-
|
|
481
|
+
try:
|
|
482
|
+
# Execute step
|
|
483
|
+
self.ttp.execute_step(driver, payload)
|
|
468
484
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
result
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
})
|
|
485
|
+
# Verify result
|
|
486
|
+
result = self.ttp.verify_result(driver)
|
|
487
|
+
|
|
488
|
+
ttp_results.append({
|
|
489
|
+
'payload': str(payload),
|
|
490
|
+
'success': result,
|
|
491
|
+
'url': driver.current_url
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
if result:
|
|
495
|
+
success_count += 1
|
|
481
496
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
497
|
+
except Exception as e:
|
|
498
|
+
ttp_results.append({
|
|
499
|
+
'payload': str(payload),
|
|
500
|
+
'success': False,
|
|
501
|
+
'error': str(e),
|
|
502
|
+
'url': driver.current_url
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
# Store results
|
|
506
|
+
self.store_result('ttp_name', self.ttp.name)
|
|
507
|
+
self.store_result('execution_mode', 'ui')
|
|
508
|
+
self.store_result('total_payloads', total_count)
|
|
509
|
+
self.store_result('successful_payloads', success_count)
|
|
510
|
+
self.store_result('ttp_results', ttp_results)
|
|
511
|
+
self.store_result('success_rate', success_count / total_count if total_count > 0 else 0)
|
|
512
|
+
|
|
513
|
+
# Update context
|
|
514
|
+
context[f'ttp_results_{self.ttp.name}'] = ttp_results
|
|
515
|
+
context['last_ttp_success_count'] = success_count
|
|
516
|
+
|
|
517
|
+
# Determine action success based on expected result
|
|
518
|
+
has_successes = success_count > 0
|
|
519
|
+
|
|
520
|
+
if self.expected_result:
|
|
521
|
+
# Expecting TTP to find vulnerabilities/succeed
|
|
522
|
+
return has_successes
|
|
523
|
+
else:
|
|
524
|
+
# Expecting TTP to fail (security controls working)
|
|
525
|
+
return not has_successes
|
|
526
|
+
|
|
527
|
+
def _execute_api_mode(self, driver: WebDriver, context: Dict[str, Any]) -> bool:
|
|
528
|
+
"""Execute TTP in API mode using requests library."""
|
|
529
|
+
import requests
|
|
530
|
+
|
|
531
|
+
# Get or create requests session
|
|
532
|
+
session = context.get('requests_session')
|
|
533
|
+
if session is None:
|
|
534
|
+
session = requests.Session()
|
|
535
|
+
context['requests_session'] = session
|
|
536
|
+
|
|
537
|
+
# Set target URL in context if provided
|
|
538
|
+
if self.target_url:
|
|
539
|
+
context['target_url'] = self.target_url
|
|
540
|
+
|
|
541
|
+
# Verify TTP supports API mode
|
|
542
|
+
if not self.ttp.supports_api_mode():
|
|
543
|
+
self.store_result('error', f'TTP {self.ttp.name} does not support API execution mode')
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
# Execute TTP payloads via API
|
|
547
|
+
ttp_results = []
|
|
548
|
+
success_count = 0
|
|
549
|
+
total_count = 0
|
|
550
|
+
|
|
551
|
+
for payload in self.ttp.get_payloads():
|
|
552
|
+
total_count += 1
|
|
506
553
|
|
|
507
|
-
|
|
508
|
-
#
|
|
509
|
-
|
|
510
|
-
else:
|
|
511
|
-
# Expecting TTP to fail (security controls working)
|
|
512
|
-
return not has_successes
|
|
554
|
+
try:
|
|
555
|
+
# Execute step via API
|
|
556
|
+
response = self.ttp.execute_step_api(session, payload, context)
|
|
513
557
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
558
|
+
# Verify result
|
|
559
|
+
result = self.ttp.verify_result_api(response, context)
|
|
560
|
+
|
|
561
|
+
ttp_results.append({
|
|
562
|
+
'payload': str(payload),
|
|
563
|
+
'success': result,
|
|
564
|
+
'status_code': response.status_code,
|
|
565
|
+
'url': response.url
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
if result:
|
|
569
|
+
success_count += 1
|
|
570
|
+
|
|
571
|
+
except Exception as e:
|
|
572
|
+
ttp_results.append({
|
|
573
|
+
'payload': str(payload),
|
|
574
|
+
'success': False,
|
|
575
|
+
'error': str(e)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
# Store results
|
|
579
|
+
self.store_result('ttp_name', self.ttp.name)
|
|
580
|
+
self.store_result('execution_mode', 'api')
|
|
581
|
+
self.store_result('total_payloads', total_count)
|
|
582
|
+
self.store_result('successful_payloads', success_count)
|
|
583
|
+
self.store_result('ttp_results', ttp_results)
|
|
584
|
+
self.store_result('success_rate', success_count / total_count if total_count > 0 else 0)
|
|
585
|
+
|
|
586
|
+
# Update context
|
|
587
|
+
context[f'ttp_results_{self.ttp.name}'] = ttp_results
|
|
588
|
+
context['last_ttp_success_count'] = success_count
|
|
589
|
+
|
|
590
|
+
# Determine action success based on expected result
|
|
591
|
+
has_successes = success_count > 0
|
|
592
|
+
|
|
593
|
+
if self.expected_result:
|
|
594
|
+
# Expecting TTP to find vulnerabilities/succeed
|
|
595
|
+
return has_successes
|
|
596
|
+
else:
|
|
597
|
+
# Expecting TTP to fail (security controls working)
|
|
598
|
+
return not has_successes
|
|
517
599
|
|
|
518
600
|
|
|
519
601
|
class AssertAction(Action):
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from selenium.webdriver.common.by import By
|
|
2
2
|
from selenium.webdriver.remote.webdriver import WebDriver
|
|
3
3
|
from selenium.common.exceptions import NoSuchElementException
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
import requests
|
|
4
6
|
|
|
5
7
|
from ...core.ttp import TTP
|
|
6
8
|
from ...payloads.generators import PayloadGenerator
|
|
@@ -8,27 +10,65 @@ from ...payloads.generators import PayloadGenerator
|
|
|
8
10
|
class LoginBruteforceTTP(TTP):
|
|
9
11
|
"""
|
|
10
12
|
A TTP that emulates a login bruteforce attack.
|
|
13
|
+
|
|
14
|
+
Supports two execution modes:
|
|
15
|
+
- UI mode: Uses Selenium to fill login forms
|
|
16
|
+
- API mode: Makes direct HTTP POST requests to login endpoints
|
|
11
17
|
"""
|
|
12
18
|
def __init__(self,
|
|
13
19
|
payload_generator: PayloadGenerator,
|
|
14
20
|
username: str,
|
|
15
|
-
username_selector: str,
|
|
16
|
-
password_selector: str,
|
|
17
|
-
submit_selector: str,
|
|
21
|
+
username_selector: str = None,
|
|
22
|
+
password_selector: str = None,
|
|
23
|
+
submit_selector: str = None,
|
|
18
24
|
expected_result: bool = True,
|
|
19
|
-
authentication=None
|
|
20
|
-
|
|
25
|
+
authentication=None,
|
|
26
|
+
execution_mode: str = 'ui',
|
|
27
|
+
api_endpoint: Optional[str] = None,
|
|
28
|
+
username_field: str = 'username',
|
|
29
|
+
password_field: str = 'password',
|
|
30
|
+
success_indicators: Optional[Dict[str, Any]] = None):
|
|
31
|
+
"""
|
|
32
|
+
Initialize the Login Bruteforce TTP.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
payload_generator: Generator that yields password payloads
|
|
36
|
+
username: Username to attempt login with
|
|
37
|
+
username_selector: CSS selector for username field (UI mode)
|
|
38
|
+
password_selector: CSS selector for password field (UI mode)
|
|
39
|
+
submit_selector: CSS selector for submit button (UI mode)
|
|
40
|
+
expected_result: Whether we expect to find a valid password
|
|
41
|
+
authentication: Optional authentication to perform before testing
|
|
42
|
+
execution_mode: 'ui' or 'api'
|
|
43
|
+
api_endpoint: API endpoint path for login (API mode, e.g., '/api/auth/login')
|
|
44
|
+
username_field: Field name for username in API request body (API mode)
|
|
45
|
+
password_field: Field name for password in API request body (API mode)
|
|
46
|
+
success_indicators: Dict with keys 'status_code' (int), 'response_contains' (str),
|
|
47
|
+
'response_not_contains' (str) to determine successful login in API mode
|
|
48
|
+
"""
|
|
21
49
|
super().__init__(
|
|
22
50
|
name="Login Bruteforce",
|
|
23
51
|
description="Attempts to guess a user's password using a list of payloads.",
|
|
24
52
|
expected_result=expected_result,
|
|
25
|
-
authentication=authentication
|
|
53
|
+
authentication=authentication,
|
|
54
|
+
execution_mode=execution_mode
|
|
26
55
|
)
|
|
27
56
|
self.payload_generator = payload_generator
|
|
28
57
|
self.username = username
|
|
58
|
+
|
|
59
|
+
# UI mode fields
|
|
29
60
|
self.username_selector = username_selector
|
|
30
61
|
self.password_selector = password_selector
|
|
31
62
|
self.submit_selector = submit_selector
|
|
63
|
+
|
|
64
|
+
# API mode fields
|
|
65
|
+
self.api_endpoint = api_endpoint
|
|
66
|
+
self.username_field = username_field
|
|
67
|
+
self.password_field = password_field
|
|
68
|
+
self.success_indicators = success_indicators or {
|
|
69
|
+
'status_code': 200,
|
|
70
|
+
'response_not_contains': 'invalid'
|
|
71
|
+
}
|
|
32
72
|
|
|
33
73
|
def get_payloads(self):
|
|
34
74
|
"""Yields passwords from the configured generator."""
|
|
@@ -58,7 +98,98 @@ class LoginBruteforceTTP(TTP):
|
|
|
58
98
|
|
|
59
99
|
def verify_result(self, driver: WebDriver) -> bool:
|
|
60
100
|
"""
|
|
61
|
-
Checks for indicators of a successful login.
|
|
101
|
+
Checks for indicators of a successful login in UI mode.
|
|
62
102
|
A simple check is if the URL no longer contains 'login'.
|
|
63
103
|
"""
|
|
64
104
|
return "login" not in driver.current_url.lower()
|
|
105
|
+
|
|
106
|
+
def execute_step_api(self, session: requests.Session, payload: str, context: Dict[str, Any]) -> requests.Response:
|
|
107
|
+
"""
|
|
108
|
+
Executes a login attempt via API request.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
session: requests.Session for making HTTP requests
|
|
112
|
+
payload: The password to attempt
|
|
113
|
+
context: Shared context dictionary
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
requests.Response from the login attempt
|
|
117
|
+
"""
|
|
118
|
+
from urllib.parse import urljoin
|
|
119
|
+
|
|
120
|
+
# Build the full URL
|
|
121
|
+
base_url = context.get('target_url', '')
|
|
122
|
+
if not base_url:
|
|
123
|
+
raise ValueError("target_url must be set in context for API mode")
|
|
124
|
+
|
|
125
|
+
url = urljoin(base_url, self.api_endpoint or '/login')
|
|
126
|
+
|
|
127
|
+
# Build request body
|
|
128
|
+
body = {
|
|
129
|
+
self.username_field: self.username,
|
|
130
|
+
self.password_field: payload
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Merge auth headers from context
|
|
134
|
+
headers = {}
|
|
135
|
+
auth_headers = context.get('auth_headers', {})
|
|
136
|
+
if auth_headers:
|
|
137
|
+
headers.update(auth_headers)
|
|
138
|
+
|
|
139
|
+
# Honor rate limiting
|
|
140
|
+
import time
|
|
141
|
+
resume_at = context.get('rate_limit_resume_at')
|
|
142
|
+
now = time.time()
|
|
143
|
+
if isinstance(resume_at, (int, float)) and resume_at > now:
|
|
144
|
+
wait_s = min(resume_at - now, 30)
|
|
145
|
+
if wait_s > 0:
|
|
146
|
+
time.sleep(wait_s)
|
|
147
|
+
|
|
148
|
+
# Make the request
|
|
149
|
+
response = session.post(url, json=body, headers=headers or None, timeout=10.0)
|
|
150
|
+
|
|
151
|
+
# Handle rate limiting
|
|
152
|
+
if response.status_code == 429:
|
|
153
|
+
retry_after = response.headers.get('Retry-After', '1')
|
|
154
|
+
try:
|
|
155
|
+
wait_s = int(retry_after)
|
|
156
|
+
except (ValueError, TypeError):
|
|
157
|
+
wait_s = 1
|
|
158
|
+
context['rate_limit_resume_at'] = time.time() + min(wait_s, 30)
|
|
159
|
+
|
|
160
|
+
return response
|
|
161
|
+
|
|
162
|
+
def verify_result_api(self, response: requests.Response, context: Dict[str, Any]) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Verifies if the login attempt was successful based on the API response.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
response: The response from execute_step_api
|
|
168
|
+
context: Shared context dictionary
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if login appears successful, False otherwise
|
|
172
|
+
"""
|
|
173
|
+
# Check status code
|
|
174
|
+
expected_status = self.success_indicators.get('status_code')
|
|
175
|
+
if expected_status is not None and response.status_code != expected_status:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
# Check response body contains/not contains strings
|
|
179
|
+
try:
|
|
180
|
+
response_text = response.text.lower()
|
|
181
|
+
|
|
182
|
+
# Check if response should contain certain text
|
|
183
|
+
contains = self.success_indicators.get('response_contains')
|
|
184
|
+
if contains and contains.lower() not in response_text:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
# Check if response should NOT contain certain text
|
|
188
|
+
not_contains = self.success_indicators.get('response_not_contains')
|
|
189
|
+
if not_contains and not_contains.lower() in response_text:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
return True
|
|
193
|
+
except Exception:
|
|
194
|
+
# If we can't read the response, consider it a failure
|
|
195
|
+
return False
|
scythe/ttps/web/sql_injection.py
CHANGED
|
@@ -1,28 +1,65 @@
|
|
|
1
1
|
from selenium.webdriver.common.by import By
|
|
2
2
|
from selenium.webdriver.remote.webdriver import WebDriver
|
|
3
3
|
from selenium.common.exceptions import NoSuchElementException
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
import requests
|
|
6
|
+
|
|
4
7
|
from ...core.ttp import TTP
|
|
5
8
|
from ...payloads.generators import PayloadGenerator
|
|
6
9
|
|
|
7
10
|
class InputFieldInjector(TTP):
|
|
11
|
+
"""
|
|
12
|
+
SQL Injection TTP that tests input fields for SQL injection vulnerabilities.
|
|
13
|
+
|
|
14
|
+
Supports two execution modes:
|
|
15
|
+
- UI mode: Fills form fields with SQL payloads
|
|
16
|
+
- API mode: Sends SQL payloads in API request body fields
|
|
17
|
+
"""
|
|
8
18
|
def __init__(self,
|
|
9
|
-
target_url: str,
|
|
10
|
-
field_selector: str,
|
|
11
|
-
submit_selector: str,
|
|
12
|
-
payload_generator: PayloadGenerator,
|
|
19
|
+
target_url: str = None,
|
|
20
|
+
field_selector: str = None,
|
|
21
|
+
submit_selector: str = None,
|
|
22
|
+
payload_generator: PayloadGenerator = None,
|
|
13
23
|
expected_result: bool = True,
|
|
14
|
-
authentication=None
|
|
15
|
-
|
|
24
|
+
authentication=None,
|
|
25
|
+
execution_mode: str = 'ui',
|
|
26
|
+
api_endpoint: Optional[str] = None,
|
|
27
|
+
injection_field: str = 'query',
|
|
28
|
+
http_method: str = 'POST'):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the SQL Injection TTP.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
target_url: Target URL (UI mode)
|
|
34
|
+
field_selector: CSS/tag selector for input field (UI mode)
|
|
35
|
+
submit_selector: CSS selector for submit button (UI mode)
|
|
36
|
+
payload_generator: Generator that yields SQL injection payloads
|
|
37
|
+
expected_result: Whether we expect to find SQL injection vulnerabilities
|
|
38
|
+
authentication: Optional authentication
|
|
39
|
+
execution_mode: 'ui' or 'api'
|
|
40
|
+
api_endpoint: API endpoint path (API mode, e.g., '/api/search')
|
|
41
|
+
injection_field: Field name to inject SQL payload into (API mode)
|
|
42
|
+
http_method: HTTP method to use (API mode) - 'POST' or 'GET'
|
|
43
|
+
"""
|
|
16
44
|
super().__init__(
|
|
17
|
-
name="SQL Injection via
|
|
18
|
-
description="
|
|
45
|
+
name="SQL Injection via Input Field",
|
|
46
|
+
description="Simulate SQL injection by injecting payloads into input fields",
|
|
19
47
|
expected_result=expected_result,
|
|
20
|
-
authentication=authentication
|
|
48
|
+
authentication=authentication,
|
|
49
|
+
execution_mode=execution_mode)
|
|
21
50
|
|
|
51
|
+
# UI mode fields
|
|
22
52
|
self.target_url = target_url
|
|
23
53
|
self.field_selector = field_selector
|
|
24
|
-
self.payload_generator = payload_generator
|
|
25
54
|
self.submit_selector = submit_selector
|
|
55
|
+
|
|
56
|
+
# Common fields
|
|
57
|
+
self.payload_generator = payload_generator
|
|
58
|
+
|
|
59
|
+
# API mode fields
|
|
60
|
+
self.api_endpoint = api_endpoint
|
|
61
|
+
self.injection_field = injection_field
|
|
62
|
+
self.http_method = http_method.upper()
|
|
26
63
|
|
|
27
64
|
def get_payloads(self):
|
|
28
65
|
"""yields queries from the configured generator"""
|
|
@@ -51,30 +88,210 @@ class InputFieldInjector(TTP):
|
|
|
51
88
|
|
|
52
89
|
|
|
53
90
|
def verify_result(self, driver: WebDriver) -> bool:
|
|
91
|
+
"""Checks for SQL error indicators in the page source (UI mode)."""
|
|
54
92
|
return "sql" in driver.page_source.lower() or \
|
|
55
93
|
"source" in driver.page_source.lower()
|
|
94
|
+
|
|
95
|
+
def execute_step_api(self, session: requests.Session, payload: str, context: Dict[str, Any]) -> requests.Response:
|
|
96
|
+
"""
|
|
97
|
+
Executes a SQL injection attempt via API request.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
session: requests.Session for making HTTP requests
|
|
101
|
+
payload: The SQL injection payload to test
|
|
102
|
+
context: Shared context dictionary
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
requests.Response from the injection attempt
|
|
106
|
+
"""
|
|
107
|
+
from urllib.parse import urljoin
|
|
108
|
+
|
|
109
|
+
# Build the full URL
|
|
110
|
+
base_url = context.get('target_url', '')
|
|
111
|
+
if not base_url:
|
|
112
|
+
raise ValueError("target_url must be set in context for API mode")
|
|
113
|
+
|
|
114
|
+
url = urljoin(base_url, self.api_endpoint or '/search')
|
|
115
|
+
|
|
116
|
+
# Merge auth headers from context
|
|
117
|
+
headers = {}
|
|
118
|
+
auth_headers = context.get('auth_headers', {})
|
|
119
|
+
if auth_headers:
|
|
120
|
+
headers.update(auth_headers)
|
|
121
|
+
|
|
122
|
+
# Honor rate limiting
|
|
123
|
+
import time
|
|
124
|
+
resume_at = context.get('rate_limit_resume_at')
|
|
125
|
+
now = time.time()
|
|
126
|
+
if isinstance(resume_at, (int, float)) and resume_at > now:
|
|
127
|
+
wait_s = min(resume_at - now, 30)
|
|
128
|
+
if wait_s > 0:
|
|
129
|
+
time.sleep(wait_s)
|
|
130
|
+
|
|
131
|
+
# Make the request based on HTTP method
|
|
132
|
+
if self.http_method == 'GET':
|
|
133
|
+
# For GET, put payload in query params
|
|
134
|
+
response = session.get(url, params={self.injection_field: payload}, headers=headers or None, timeout=10.0)
|
|
135
|
+
else:
|
|
136
|
+
# For POST/PUT/etc, put payload in JSON body
|
|
137
|
+
body = {self.injection_field: payload}
|
|
138
|
+
response = session.request(self.http_method, url, json=body, headers=headers or None, timeout=10.0)
|
|
139
|
+
|
|
140
|
+
# Handle rate limiting
|
|
141
|
+
if response.status_code == 429:
|
|
142
|
+
retry_after = response.headers.get('Retry-After', '1')
|
|
143
|
+
try:
|
|
144
|
+
wait_s = int(retry_after)
|
|
145
|
+
except (ValueError, TypeError):
|
|
146
|
+
wait_s = 1
|
|
147
|
+
context['rate_limit_resume_at'] = time.time() + min(wait_s, 30)
|
|
148
|
+
|
|
149
|
+
return response
|
|
150
|
+
|
|
151
|
+
def verify_result_api(self, response: requests.Response, context: Dict[str, Any]) -> bool:
|
|
152
|
+
"""
|
|
153
|
+
Verifies if the SQL injection attempt triggered a vulnerability.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
response: The response from execute_step_api
|
|
157
|
+
context: Shared context dictionary
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if SQL error indicators found, False otherwise
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
response_text = response.text.lower()
|
|
164
|
+
# Common SQL error indicators
|
|
165
|
+
sql_indicators = [
|
|
166
|
+
'sql', 'syntax', 'mysql', 'sqlite', 'postgresql', 'oracle',
|
|
167
|
+
'odbc', 'jdbc', 'driver', 'database', 'query', 'syntax error',
|
|
168
|
+
'unterminated', 'unexpected', 'warning: mysql'
|
|
169
|
+
]
|
|
170
|
+
return any(indicator in response_text for indicator in sql_indicators)
|
|
171
|
+
except Exception:
|
|
172
|
+
return False
|
|
56
173
|
|
|
57
174
|
|
|
58
175
|
class URLManipulation(TTP):
|
|
176
|
+
"""
|
|
177
|
+
SQL Injection TTP that tests URL query parameters for SQL injection vulnerabilities.
|
|
178
|
+
|
|
179
|
+
Supports two execution modes:
|
|
180
|
+
- UI mode: Navigates to URLs with SQL payloads in query parameters
|
|
181
|
+
- API mode: Sends GET requests with SQL payloads in query parameters
|
|
182
|
+
"""
|
|
59
183
|
def __init__(self,
|
|
60
184
|
payload_generator: PayloadGenerator,
|
|
61
|
-
target_url: str,
|
|
185
|
+
target_url: str = None,
|
|
62
186
|
expected_result: bool = True,
|
|
63
|
-
authentication=None
|
|
187
|
+
authentication=None,
|
|
188
|
+
execution_mode: str = 'ui',
|
|
189
|
+
api_endpoint: Optional[str] = None,
|
|
190
|
+
query_param: str = 'q'):
|
|
191
|
+
"""
|
|
192
|
+
Initialize the URL Manipulation SQL Injection TTP.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
payload_generator: Generator that yields SQL injection payloads
|
|
196
|
+
target_url: Target URL (UI mode)
|
|
197
|
+
expected_result: Whether we expect to find SQL injection vulnerabilities
|
|
198
|
+
authentication: Optional authentication
|
|
199
|
+
execution_mode: 'ui' or 'api'
|
|
200
|
+
api_endpoint: API endpoint path (API mode, e.g., '/api/search')
|
|
201
|
+
query_param: Query parameter name to inject into (default: 'q')
|
|
202
|
+
"""
|
|
64
203
|
super().__init__(
|
|
65
204
|
name="SQL Injection via URL manipulation",
|
|
66
|
-
description="
|
|
205
|
+
description="Simulate SQL injection by manipulating URL query parameters",
|
|
67
206
|
expected_result=expected_result,
|
|
68
|
-
authentication=authentication
|
|
207
|
+
authentication=authentication,
|
|
208
|
+
execution_mode=execution_mode)
|
|
69
209
|
self.target_url = target_url
|
|
70
210
|
self.payload_generator = payload_generator
|
|
211
|
+
self.api_endpoint = api_endpoint
|
|
212
|
+
self.query_param = query_param
|
|
71
213
|
|
|
72
214
|
def get_payloads(self):
|
|
73
215
|
yield from self.payload_generator()
|
|
74
216
|
|
|
75
217
|
def execute_step(self, driver: WebDriver, payload: str):
|
|
76
|
-
|
|
218
|
+
"""Execute SQL injection via URL manipulation in UI mode."""
|
|
219
|
+
driver.get(f"{self.target_url}?{self.query_param}={payload}")
|
|
77
220
|
|
|
78
221
|
def verify_result(self, driver: WebDriver) -> bool:
|
|
222
|
+
"""Check for SQL error indicators in UI mode."""
|
|
79
223
|
return "sql" in driver.page_source.lower() or \
|
|
80
224
|
"source" in driver.page_source.lower()
|
|
225
|
+
|
|
226
|
+
def execute_step_api(self, session: requests.Session, payload: str, context: Dict[str, Any]) -> requests.Response:
|
|
227
|
+
"""
|
|
228
|
+
Executes a SQL injection attempt via API request with query parameters.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
session: requests.Session for making HTTP requests
|
|
232
|
+
payload: The SQL injection payload to test
|
|
233
|
+
context: Shared context dictionary
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
requests.Response from the injection attempt
|
|
237
|
+
"""
|
|
238
|
+
from urllib.parse import urljoin
|
|
239
|
+
|
|
240
|
+
# Build the full URL
|
|
241
|
+
base_url = context.get('target_url', '')
|
|
242
|
+
if not base_url:
|
|
243
|
+
raise ValueError("target_url must be set in context for API mode")
|
|
244
|
+
|
|
245
|
+
url = urljoin(base_url, self.api_endpoint or self.target_url or '/')
|
|
246
|
+
|
|
247
|
+
# Merge auth headers from context
|
|
248
|
+
headers = {}
|
|
249
|
+
auth_headers = context.get('auth_headers', {})
|
|
250
|
+
if auth_headers:
|
|
251
|
+
headers.update(auth_headers)
|
|
252
|
+
|
|
253
|
+
# Honor rate limiting
|
|
254
|
+
import time
|
|
255
|
+
resume_at = context.get('rate_limit_resume_at')
|
|
256
|
+
now = time.time()
|
|
257
|
+
if isinstance(resume_at, (int, float)) and resume_at > now:
|
|
258
|
+
wait_s = min(resume_at - now, 30)
|
|
259
|
+
if wait_s > 0:
|
|
260
|
+
time.sleep(wait_s)
|
|
261
|
+
|
|
262
|
+
# Make GET request with payload in query param
|
|
263
|
+
response = session.get(url, params={self.query_param: payload}, headers=headers or None, timeout=10.0)
|
|
264
|
+
|
|
265
|
+
# Handle rate limiting
|
|
266
|
+
if response.status_code == 429:
|
|
267
|
+
retry_after = response.headers.get('Retry-After', '1')
|
|
268
|
+
try:
|
|
269
|
+
wait_s = int(retry_after)
|
|
270
|
+
except (ValueError, TypeError):
|
|
271
|
+
wait_s = 1
|
|
272
|
+
context['rate_limit_resume_at'] = time.time() + min(wait_s, 30)
|
|
273
|
+
|
|
274
|
+
return response
|
|
275
|
+
|
|
276
|
+
def verify_result_api(self, response: requests.Response, context: Dict[str, Any]) -> bool:
|
|
277
|
+
"""
|
|
278
|
+
Verifies if the SQL injection attempt triggered a vulnerability.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
response: The response from execute_step_api
|
|
282
|
+
context: Shared context dictionary
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True if SQL error indicators found, False otherwise
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
response_text = response.text.lower()
|
|
289
|
+
# Common SQL error indicators
|
|
290
|
+
sql_indicators = [
|
|
291
|
+
'sql', 'syntax', 'mysql', 'sqlite', 'postgresql', 'oracle',
|
|
292
|
+
'odbc', 'jdbc', 'driver', 'database', 'query', 'syntax error',
|
|
293
|
+
'unterminated', 'unexpected', 'warning: mysql'
|
|
294
|
+
]
|
|
295
|
+
return any(indicator in response_text for indicator in sql_indicators)
|
|
296
|
+
except Exception:
|
|
297
|
+
return False
|
|
@@ -13,11 +13,11 @@ scythe/behaviors/stealth.py,sha256=xv7MrPQgRCdCUJyYTcXV2aasWZoAw8rAQWg-AuQVb7U,1
|
|
|
13
13
|
scythe/cli/__init__.py,sha256=9EVxmFiWsAoqWJ6br1bc3BxlA71JyOQP28fUHhX2k7E,43
|
|
14
14
|
scythe/cli/main.py,sha256=DFvOB39tX4FeiOxitJwXfq28J13GjewBZ9gOA_0HOjI,26003
|
|
15
15
|
scythe/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
scythe/core/executor.py,sha256=
|
|
16
|
+
scythe/core/executor.py,sha256=FTuhvWiNOsN1LZ4scEsVGDXx9D3yF3tiAoDt-HW2QRI,16038
|
|
17
17
|
scythe/core/headers.py,sha256=AokCQ3F5QGUcfoK7iO57hA1HHL4IznZeWV464_MqYcE,16670
|
|
18
|
-
scythe/core/ttp.py,sha256=
|
|
18
|
+
scythe/core/ttp.py,sha256=tEYIhDdr8kcwQrlcfVmdeLFiAfOvc0BhPOVxPh8TiWo,5676
|
|
19
19
|
scythe/journeys/__init__.py,sha256=Odi8NhRg7Hefmo1EJj1guakrCSPhsuus4i-_62uUUjs,654
|
|
20
|
-
scythe/journeys/actions.py,sha256=
|
|
20
|
+
scythe/journeys/actions.py,sha256=URr53p1GQxSIBZo0IubchQ1dlfvnPHgCtmkRfLSoi7A,40333
|
|
21
21
|
scythe/journeys/base.py,sha256=vXIgEnSW__iYTriBbuMG4l_XCM96xojJH_fyFScKoBY,24969
|
|
22
22
|
scythe/journeys/executor.py,sha256=uJkjO3PALSLZh3IOSxgX18gRJX_Bck3gW9OClusiQeE,24949
|
|
23
23
|
scythe/orchestrators/__init__.py,sha256=_vemcXjKbB1jI0F2dPA0F1zNsyUekjcXImLDUDhWDN0,560
|
|
@@ -29,12 +29,12 @@ scythe/payloads/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
29
29
|
scythe/payloads/generators.py,sha256=tCcJULoFnUppgaiFhYq5f20OoQxTdKKIb2O-Ntby9ZM,914
|
|
30
30
|
scythe/ttps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
scythe/ttps/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
-
scythe/ttps/web/login_bruteforce.py,sha256=
|
|
33
|
-
scythe/ttps/web/sql_injection.py,sha256=
|
|
32
|
+
scythe/ttps/web/login_bruteforce.py,sha256=ybmN2Vl9-p58YbOchirY1193GvlaTmUTW1qlluN_l3I,7816
|
|
33
|
+
scythe/ttps/web/sql_injection.py,sha256=rIRHaRUilSrMA5q5MO1RqR6-TM_fRIiCanPaFz5wKKs,11712
|
|
34
34
|
scythe/ttps/web/uuid_guessing.py,sha256=JwNt_9HVynMWFPPU6UGJFcpxvMVDsvc_wAnJVtcYbps,1235
|
|
35
|
-
scythe_ttp-0.17.
|
|
36
|
-
scythe_ttp-0.17.
|
|
37
|
-
scythe_ttp-0.17.
|
|
38
|
-
scythe_ttp-0.17.
|
|
39
|
-
scythe_ttp-0.17.
|
|
40
|
-
scythe_ttp-0.17.
|
|
35
|
+
scythe_ttp-0.17.3.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
|
|
36
|
+
scythe_ttp-0.17.3.dist-info/METADATA,sha256=VEyrEx7ZfhrAx3hn_F1lXV5XkYjxkLE4bV-M65inP1s,30188
|
|
37
|
+
scythe_ttp-0.17.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
38
|
+
scythe_ttp-0.17.3.dist-info/entry_points.txt,sha256=rAAsFBcCm0OX3I4uRyclfx4YJGoTuumZKY43HN7R5Ro,48
|
|
39
|
+
scythe_ttp-0.17.3.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
|
|
40
|
+
scythe_ttp-0.17.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|