scythe-ttp 0.17.1__py3-none-any.whl → 0.17.2__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/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, authentication: Optional['Authentication'] = None):
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
@@ -439,81 +439,163 @@ class TTPAction(Action):
439
439
  True if TTP execution matches expected result, False otherwise
440
440
  """
441
441
  try:
442
- # Determine target URL
443
- if self.target_url:
444
- url = self.target_url
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
- url = driver.current_url
449
-
450
- # Navigate to URL if needed
451
- if url != driver.current_url:
452
- driver.get(url)
453
-
454
- # Execute TTP authentication if required
455
- if self.ttp.requires_authentication():
456
- auth_success = self.ttp.authenticate(driver, url)
457
- if not auth_success:
458
- self.store_result('error', 'TTP authentication failed')
459
- return False
460
-
461
- # Execute TTP payloads
462
- ttp_results = []
463
- success_count = 0
464
- total_count = 0
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
- for payload in self.ttp.get_payloads():
467
- total_count += 1
481
+ try:
482
+ # Execute step
483
+ self.ttp.execute_step(driver, payload)
468
484
 
469
- try:
470
- # Execute step
471
- self.ttp.execute_step(driver, payload)
472
-
473
- # Verify result
474
- result = self.ttp.verify_result(driver)
475
-
476
- ttp_results.append({
477
- 'payload': str(payload),
478
- 'success': result,
479
- 'url': driver.current_url
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
- if result:
483
- success_count += 1
484
-
485
- except Exception as e:
486
- ttp_results.append({
487
- 'payload': str(payload),
488
- 'success': False,
489
- 'error': str(e),
490
- 'url': driver.current_url
491
- })
492
-
493
- # Store results
494
- self.store_result('ttp_name', self.ttp.name)
495
- self.store_result('total_payloads', total_count)
496
- self.store_result('successful_payloads', success_count)
497
- self.store_result('ttp_results', ttp_results)
498
- self.store_result('success_rate', success_count / total_count if total_count > 0 else 0)
499
-
500
- # Update context
501
- context[f'ttp_results_{self.ttp.name}'] = ttp_results
502
- context['last_ttp_success_count'] = success_count
503
-
504
- # Determine action success based on expected result
505
- has_successes = success_count > 0
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
- if self.expected_result:
508
- # Expecting TTP to find vulnerabilities/succeed
509
- return has_successes
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
- except Exception as e:
515
- self.store_result('error', str(e))
516
- return False
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
@@ -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 URL manipulation",
18
- description="simulate an sql Injection by manipulation of url queries",
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="simulate an sql Injection by manipulation of url queries",
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
- driver.get(f"{self.target_url}?q={payload}")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.17.1
3
+ Version: 0.17.2
4
4
  Summary: An extensible framework for emulating attacker TTPs with Selenium.
5
5
  Author-email: EpykLab <cyber@epyklab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -15,9 +15,9 @@ scythe/cli/main.py,sha256=DFvOB39tX4FeiOxitJwXfq28J13GjewBZ9gOA_0HOjI,26003
15
15
  scythe/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  scythe/core/executor.py,sha256=C3FkW-DNugv82T0_ky-3zAvHV_hFwVSHrX2nzgAcmAI,10588
17
17
  scythe/core/headers.py,sha256=AokCQ3F5QGUcfoK7iO57hA1HHL4IznZeWV464_MqYcE,16670
18
- scythe/core/ttp.py,sha256=Xw9GgptYsjZ-pMLdyPv64bhiwGKobrXHdF32pjIY7OU,3102
18
+ scythe/core/ttp.py,sha256=tEYIhDdr8kcwQrlcfVmdeLFiAfOvc0BhPOVxPh8TiWo,5676
19
19
  scythe/journeys/__init__.py,sha256=Odi8NhRg7Hefmo1EJj1guakrCSPhsuus4i-_62uUUjs,654
20
- scythe/journeys/actions.py,sha256=k9WjfGR1nhJWyhDU_lHr7vFy5qAl7hyyV6kCL7ZQRMo,37479
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=D4G8zB_nU9LD5w3Vv2ABTuOl4XTeg2BgZwYMObt4JJw,2488
33
- scythe/ttps/web/sql_injection.py,sha256=aWk4DFePbtFDsieOOj03Ux-5OiykyOs2_d_3SvWMOVE,2910
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.1.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
36
- scythe_ttp-0.17.1.dist-info/METADATA,sha256=bZTf4pApqa3-NNPv6PkktiBPKSgiOOhjv0a7DJmn7To,30188
37
- scythe_ttp-0.17.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- scythe_ttp-0.17.1.dist-info/entry_points.txt,sha256=rAAsFBcCm0OX3I4uRyclfx4YJGoTuumZKY43HN7R5Ro,48
39
- scythe_ttp-0.17.1.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
40
- scythe_ttp-0.17.1.dist-info/RECORD,,
35
+ scythe_ttp-0.17.2.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
36
+ scythe_ttp-0.17.2.dist-info/METADATA,sha256=O71-_tq82UjkFpZ3rBxpb__7dYPoRTJ2xbINezSbaTs,30188
37
+ scythe_ttp-0.17.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ scythe_ttp-0.17.2.dist-info/entry_points.txt,sha256=rAAsFBcCm0OX3I4uRyclfx4YJGoTuumZKY43HN7R5Ro,48
39
+ scythe_ttp-0.17.2.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
40
+ scythe_ttp-0.17.2.dist-info/RECORD,,