aiptx 2.0.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 aiptx might be problematic. Click here for more details.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +24 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +960 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +321 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +288 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +85 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2284 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +44 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +201 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aiptx-2.0.2.dist-info/METADATA +324 -0
- aiptx-2.0.2.dist-info/RECORD +165 -0
- aiptx-2.0.2.dist-info/WHEEL +5 -0
- aiptx-2.0.2.dist-info/entry_points.txt +7 -0
- aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWT Token Security Analyzer
|
|
3
|
+
|
|
4
|
+
Comprehensive JWT (JSON Web Token) security testing:
|
|
5
|
+
- Algorithm confusion attacks (none, HS256/RS256 switch)
|
|
6
|
+
- Signature verification bypass
|
|
7
|
+
- Key brute-forcing (weak secrets)
|
|
8
|
+
- Claim manipulation (exp, iat, aud, iss)
|
|
9
|
+
- Token structure analysis
|
|
10
|
+
- JWK/JWKS endpoint testing
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
- https://portswigger.net/web-security/jwt
|
|
14
|
+
- https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from aipt_v2.tools.api_security import JWTAnalyzer
|
|
18
|
+
|
|
19
|
+
analyzer = JWTAnalyzer()
|
|
20
|
+
findings = analyzer.analyze("eyJhbGciOiJIUzI1NiIs...")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import hashlib
|
|
25
|
+
import hmac
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
import jwt as pyjwt
|
|
34
|
+
HAS_PYJWT = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
HAS_PYJWT = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class JWTFinding:
|
|
41
|
+
"""JWT security finding."""
|
|
42
|
+
vulnerability: str
|
|
43
|
+
severity: str # critical, high, medium, low, info
|
|
44
|
+
description: str
|
|
45
|
+
evidence: str
|
|
46
|
+
remediation: str
|
|
47
|
+
affected_claim: str = ""
|
|
48
|
+
attack_vector: str = ""
|
|
49
|
+
timestamp: str = ""
|
|
50
|
+
cwe: str = ""
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
if not self.timestamp:
|
|
54
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class JWTInfo:
|
|
59
|
+
"""Parsed JWT information."""
|
|
60
|
+
raw: str
|
|
61
|
+
header: Dict[str, Any]
|
|
62
|
+
payload: Dict[str, Any]
|
|
63
|
+
signature: str
|
|
64
|
+
algorithm: str
|
|
65
|
+
is_valid: bool = False
|
|
66
|
+
expiration: Optional[datetime] = None
|
|
67
|
+
issued_at: Optional[datetime] = None
|
|
68
|
+
not_before: Optional[datetime] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class JWTAnalyzer:
|
|
72
|
+
"""
|
|
73
|
+
JWT Security Analyzer.
|
|
74
|
+
|
|
75
|
+
Analyzes JWT tokens for security vulnerabilities
|
|
76
|
+
including algorithm confusion, weak signatures,
|
|
77
|
+
and claim manipulation issues.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# Common weak secrets for brute-force
|
|
81
|
+
COMMON_SECRETS = [
|
|
82
|
+
"secret", "password", "123456", "admin", "key",
|
|
83
|
+
"jwt_secret", "jwt-secret", "token", "auth",
|
|
84
|
+
"supersecret", "changeme", "default", "test",
|
|
85
|
+
"development", "dev", "production", "prod",
|
|
86
|
+
"your-256-bit-secret", "your-secret-key",
|
|
87
|
+
"HS256-secret", "secret123", "secretkey",
|
|
88
|
+
"application-secret", "app-secret", "api-secret"
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
# Extended wordlist
|
|
92
|
+
EXTENDED_SECRETS = COMMON_SECRETS + [
|
|
93
|
+
# Company-style secrets
|
|
94
|
+
"company-secret", "my-secret-key", "very-secret",
|
|
95
|
+
# Lazy admin secrets
|
|
96
|
+
"password123", "admin123", "root", "toor",
|
|
97
|
+
# Framework defaults
|
|
98
|
+
"django-insecure", "flask-secret", "express-secret",
|
|
99
|
+
"rails-secret", "laravel-secret",
|
|
100
|
+
# Environment-style
|
|
101
|
+
"JWT_SECRET", "API_SECRET", "AUTH_SECRET",
|
|
102
|
+
# UUID-like
|
|
103
|
+
"00000000-0000-0000-0000-000000000000",
|
|
104
|
+
# Simple variations
|
|
105
|
+
"Secret", "SECRET", "Password", "PASSWORD"
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def __init__(self, extended_wordlist: bool = False):
|
|
109
|
+
"""
|
|
110
|
+
Initialize JWT analyzer.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
extended_wordlist: Use extended wordlist for brute-force
|
|
114
|
+
"""
|
|
115
|
+
self.secrets = self.EXTENDED_SECRETS if extended_wordlist else self.COMMON_SECRETS
|
|
116
|
+
self.findings: List[JWTFinding] = []
|
|
117
|
+
|
|
118
|
+
def _base64_decode(self, data: str) -> bytes:
|
|
119
|
+
"""Decode base64url encoded data."""
|
|
120
|
+
# Add padding if needed
|
|
121
|
+
padding = 4 - len(data) % 4
|
|
122
|
+
if padding != 4:
|
|
123
|
+
data += "=" * padding
|
|
124
|
+
# Replace URL-safe characters
|
|
125
|
+
data = data.replace("-", "+").replace("_", "/")
|
|
126
|
+
return base64.b64decode(data)
|
|
127
|
+
|
|
128
|
+
def _base64_encode(self, data: bytes) -> str:
|
|
129
|
+
"""Encode data as base64url."""
|
|
130
|
+
encoded = base64.b64encode(data).decode()
|
|
131
|
+
# Make URL-safe
|
|
132
|
+
return encoded.replace("+", "-").replace("/", "_").rstrip("=")
|
|
133
|
+
|
|
134
|
+
def parse_token(self, token: str) -> Optional[JWTInfo]:
|
|
135
|
+
"""Parse JWT token into components."""
|
|
136
|
+
parts = token.split(".")
|
|
137
|
+
|
|
138
|
+
if len(parts) != 3:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
header_json = self._base64_decode(parts[0])
|
|
143
|
+
payload_json = self._base64_decode(parts[1])
|
|
144
|
+
|
|
145
|
+
header = json.loads(header_json)
|
|
146
|
+
payload = json.loads(payload_json)
|
|
147
|
+
|
|
148
|
+
# Parse timestamps
|
|
149
|
+
expiration = None
|
|
150
|
+
issued_at = None
|
|
151
|
+
not_before = None
|
|
152
|
+
|
|
153
|
+
if "exp" in payload:
|
|
154
|
+
try:
|
|
155
|
+
expiration = datetime.fromtimestamp(payload["exp"], tz=timezone.utc)
|
|
156
|
+
except (ValueError, OSError):
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
if "iat" in payload:
|
|
160
|
+
try:
|
|
161
|
+
issued_at = datetime.fromtimestamp(payload["iat"], tz=timezone.utc)
|
|
162
|
+
except (ValueError, OSError):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
if "nbf" in payload:
|
|
166
|
+
try:
|
|
167
|
+
not_before = datetime.fromtimestamp(payload["nbf"], tz=timezone.utc)
|
|
168
|
+
except (ValueError, OSError):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
return JWTInfo(
|
|
172
|
+
raw=token,
|
|
173
|
+
header=header,
|
|
174
|
+
payload=payload,
|
|
175
|
+
signature=parts[2],
|
|
176
|
+
algorithm=header.get("alg", "unknown"),
|
|
177
|
+
expiration=expiration,
|
|
178
|
+
issued_at=issued_at,
|
|
179
|
+
not_before=not_before
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except Exception:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def test_none_algorithm(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
186
|
+
"""Test for 'none' algorithm vulnerability."""
|
|
187
|
+
findings = []
|
|
188
|
+
|
|
189
|
+
# Create token with "none" algorithm
|
|
190
|
+
none_header = {"alg": "none", "typ": "JWT"}
|
|
191
|
+
none_header_b64 = self._base64_encode(json.dumps(none_header).encode())
|
|
192
|
+
payload_b64 = jwt_info.raw.split(".")[1]
|
|
193
|
+
|
|
194
|
+
# Tokens without signature
|
|
195
|
+
none_tokens = [
|
|
196
|
+
f"{none_header_b64}.{payload_b64}.",
|
|
197
|
+
f"{none_header_b64}.{payload_b64}",
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
# Also try "None", "NONE", "nOnE"
|
|
201
|
+
for alg_variant in ["None", "NONE", "nOnE"]:
|
|
202
|
+
variant_header = {"alg": alg_variant, "typ": "JWT"}
|
|
203
|
+
variant_b64 = self._base64_encode(json.dumps(variant_header).encode())
|
|
204
|
+
none_tokens.append(f"{variant_b64}.{payload_b64}.")
|
|
205
|
+
|
|
206
|
+
findings.append(JWTFinding(
|
|
207
|
+
vulnerability="None Algorithm Attack Vector",
|
|
208
|
+
severity="critical",
|
|
209
|
+
description="JWT library may accept 'none' algorithm, allowing signature bypass",
|
|
210
|
+
evidence=f"Generated attack tokens with none algorithm",
|
|
211
|
+
attack_vector=none_tokens[0],
|
|
212
|
+
remediation="Explicitly verify algorithm in JWT validation. Never accept 'none'.",
|
|
213
|
+
cwe="CWE-327"
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
return findings
|
|
217
|
+
|
|
218
|
+
def test_algorithm_confusion(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
219
|
+
"""Test for algorithm confusion (HS256/RS256 switch)."""
|
|
220
|
+
findings = []
|
|
221
|
+
|
|
222
|
+
# If token uses RS256/RS384/RS512, could be vulnerable to HS256 confusion
|
|
223
|
+
if jwt_info.algorithm in ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"]:
|
|
224
|
+
findings.append(JWTFinding(
|
|
225
|
+
vulnerability="Potential Algorithm Confusion",
|
|
226
|
+
severity="high",
|
|
227
|
+
description=f"Token uses {jwt_info.algorithm}. If the public key is known, "
|
|
228
|
+
"attacker may forge tokens by switching to HS256 and using public key as secret.",
|
|
229
|
+
evidence=f"Current algorithm: {jwt_info.algorithm}",
|
|
230
|
+
attack_vector="Switch alg to HS256 and sign with public key",
|
|
231
|
+
remediation="Explicitly specify expected algorithm during verification. "
|
|
232
|
+
"Never allow algorithm to be changed by the token itself.",
|
|
233
|
+
cwe="CWE-327"
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
return findings
|
|
237
|
+
|
|
238
|
+
def test_weak_secret(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
239
|
+
"""Test for weak/common secrets using brute-force."""
|
|
240
|
+
findings = []
|
|
241
|
+
|
|
242
|
+
if jwt_info.algorithm not in ["HS256", "HS384", "HS512"]:
|
|
243
|
+
return findings
|
|
244
|
+
|
|
245
|
+
# Get algorithm details
|
|
246
|
+
alg_map = {
|
|
247
|
+
"HS256": ("sha256", 256),
|
|
248
|
+
"HS384": ("sha384", 384),
|
|
249
|
+
"HS512": ("sha512", 512)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
hash_alg, _ = alg_map.get(jwt_info.algorithm, ("sha256", 256))
|
|
253
|
+
|
|
254
|
+
# Get message to sign
|
|
255
|
+
parts = jwt_info.raw.split(".")
|
|
256
|
+
message = f"{parts[0]}.{parts[1]}".encode()
|
|
257
|
+
target_sig = parts[2]
|
|
258
|
+
|
|
259
|
+
# Try common secrets
|
|
260
|
+
cracked_secret = None
|
|
261
|
+
for secret in self.secrets:
|
|
262
|
+
# Compute signature
|
|
263
|
+
sig = hmac.new(
|
|
264
|
+
secret.encode(),
|
|
265
|
+
message,
|
|
266
|
+
hash_alg
|
|
267
|
+
).digest()
|
|
268
|
+
computed_sig = self._base64_encode(sig)
|
|
269
|
+
|
|
270
|
+
if computed_sig == target_sig:
|
|
271
|
+
cracked_secret = secret
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
if cracked_secret:
|
|
275
|
+
findings.append(JWTFinding(
|
|
276
|
+
vulnerability="Weak JWT Secret",
|
|
277
|
+
severity="critical",
|
|
278
|
+
description=f"JWT secret is a common/weak value: '{cracked_secret}'",
|
|
279
|
+
evidence=f"Secret cracked: {cracked_secret}",
|
|
280
|
+
attack_vector=f"Use secret '{cracked_secret}' to forge tokens",
|
|
281
|
+
remediation="Use a strong, random secret (minimum 256 bits). "
|
|
282
|
+
"Consider using asymmetric algorithms (RS256).",
|
|
283
|
+
cwe="CWE-521"
|
|
284
|
+
))
|
|
285
|
+
else:
|
|
286
|
+
findings.append(JWTFinding(
|
|
287
|
+
vulnerability="JWT Secret Brute-Force Test",
|
|
288
|
+
severity="info",
|
|
289
|
+
description=f"Tested {len(self.secrets)} common secrets - none matched",
|
|
290
|
+
evidence="Secret not in common wordlist",
|
|
291
|
+
remediation="Continue using a strong secret",
|
|
292
|
+
cwe=""
|
|
293
|
+
))
|
|
294
|
+
|
|
295
|
+
return findings
|
|
296
|
+
|
|
297
|
+
def test_expiration(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
298
|
+
"""Test token expiration claims."""
|
|
299
|
+
findings = []
|
|
300
|
+
now = datetime.now(timezone.utc)
|
|
301
|
+
|
|
302
|
+
# Check for missing expiration
|
|
303
|
+
if not jwt_info.expiration:
|
|
304
|
+
if "exp" not in jwt_info.payload:
|
|
305
|
+
findings.append(JWTFinding(
|
|
306
|
+
vulnerability="Missing Token Expiration",
|
|
307
|
+
severity="high",
|
|
308
|
+
description="Token has no expiration claim (exp)",
|
|
309
|
+
evidence="No 'exp' claim in payload",
|
|
310
|
+
affected_claim="exp",
|
|
311
|
+
remediation="Always include expiration in tokens. Use short lifetimes.",
|
|
312
|
+
cwe="CWE-613"
|
|
313
|
+
))
|
|
314
|
+
else:
|
|
315
|
+
# Check if expired
|
|
316
|
+
if jwt_info.expiration < now:
|
|
317
|
+
findings.append(JWTFinding(
|
|
318
|
+
vulnerability="Expired Token",
|
|
319
|
+
severity="info",
|
|
320
|
+
description=f"Token expired at {jwt_info.expiration.isoformat()}",
|
|
321
|
+
evidence=f"exp: {jwt_info.payload.get('exp')}",
|
|
322
|
+
affected_claim="exp",
|
|
323
|
+
remediation="Refresh token or obtain new one",
|
|
324
|
+
cwe=""
|
|
325
|
+
))
|
|
326
|
+
else:
|
|
327
|
+
# Check for excessively long expiration
|
|
328
|
+
time_until_exp = (jwt_info.expiration - now).total_seconds()
|
|
329
|
+
if time_until_exp > 86400 * 30: # More than 30 days
|
|
330
|
+
findings.append(JWTFinding(
|
|
331
|
+
vulnerability="Long Token Lifetime",
|
|
332
|
+
severity="medium",
|
|
333
|
+
description=f"Token valid for {time_until_exp / 86400:.1f} days",
|
|
334
|
+
evidence=f"exp: {jwt_info.payload.get('exp')}",
|
|
335
|
+
affected_claim="exp",
|
|
336
|
+
remediation="Use shorter token lifetimes. Implement refresh tokens.",
|
|
337
|
+
cwe="CWE-613"
|
|
338
|
+
))
|
|
339
|
+
|
|
340
|
+
return findings
|
|
341
|
+
|
|
342
|
+
def test_sensitive_claims(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
343
|
+
"""Check for sensitive data in claims."""
|
|
344
|
+
findings = []
|
|
345
|
+
|
|
346
|
+
sensitive_patterns = {
|
|
347
|
+
"password": r"(password|passwd|pwd)",
|
|
348
|
+
"secret": r"(secret|api_key|apikey|private)",
|
|
349
|
+
"credit_card": r"(\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4})",
|
|
350
|
+
"ssn": r"\d{3}-\d{2}-\d{4}",
|
|
351
|
+
"email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
payload_str = json.dumps(jwt_info.payload).lower()
|
|
355
|
+
|
|
356
|
+
for data_type, pattern in sensitive_patterns.items():
|
|
357
|
+
if data_type in ["password", "secret"]:
|
|
358
|
+
# Check for key names
|
|
359
|
+
for key in jwt_info.payload.keys():
|
|
360
|
+
if re.search(pattern, key.lower()):
|
|
361
|
+
findings.append(JWTFinding(
|
|
362
|
+
vulnerability="Sensitive Data in JWT",
|
|
363
|
+
severity="high",
|
|
364
|
+
description=f"Token contains potentially sensitive claim: {key}",
|
|
365
|
+
evidence=f"Claim name matches pattern: {data_type}",
|
|
366
|
+
affected_claim=key,
|
|
367
|
+
remediation="Never store sensitive data in JWT. "
|
|
368
|
+
"Store server-side and reference by ID.",
|
|
369
|
+
cwe="CWE-200"
|
|
370
|
+
))
|
|
371
|
+
else:
|
|
372
|
+
# Check for patterns in values
|
|
373
|
+
if re.search(pattern, payload_str):
|
|
374
|
+
findings.append(JWTFinding(
|
|
375
|
+
vulnerability=f"Potential {data_type.replace('_', ' ').title()} in JWT",
|
|
376
|
+
severity="medium",
|
|
377
|
+
description=f"Token may contain {data_type} data",
|
|
378
|
+
evidence=f"Pattern match for {data_type}",
|
|
379
|
+
remediation="Review and remove sensitive data from token",
|
|
380
|
+
cwe="CWE-200"
|
|
381
|
+
))
|
|
382
|
+
|
|
383
|
+
return findings
|
|
384
|
+
|
|
385
|
+
def test_kid_injection(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
386
|
+
"""Test for kid (Key ID) injection vulnerabilities."""
|
|
387
|
+
findings = []
|
|
388
|
+
|
|
389
|
+
kid = jwt_info.header.get("kid")
|
|
390
|
+
|
|
391
|
+
if kid:
|
|
392
|
+
# Check for potential SQL injection in kid
|
|
393
|
+
sql_patterns = ["'", '"', ";", "--", "/*", "*/", "OR", "AND"]
|
|
394
|
+
for pattern in sql_patterns:
|
|
395
|
+
if pattern in kid:
|
|
396
|
+
findings.append(JWTFinding(
|
|
397
|
+
vulnerability="Potential SQL Injection in kid",
|
|
398
|
+
severity="critical",
|
|
399
|
+
description="kid header may be vulnerable to SQL injection",
|
|
400
|
+
evidence=f"kid contains: {pattern}",
|
|
401
|
+
attack_vector=f"kid: {kid}",
|
|
402
|
+
remediation="Validate and sanitize kid before use in queries",
|
|
403
|
+
cwe="CWE-89"
|
|
404
|
+
))
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
# Check for path traversal
|
|
408
|
+
if ".." in kid or "/" in kid or "\\" in kid:
|
|
409
|
+
findings.append(JWTFinding(
|
|
410
|
+
vulnerability="Potential Path Traversal in kid",
|
|
411
|
+
severity="high",
|
|
412
|
+
description="kid header may allow path traversal",
|
|
413
|
+
evidence=f"kid: {kid}",
|
|
414
|
+
attack_vector=f"kid: ../../path/to/key",
|
|
415
|
+
remediation="Validate kid against allowlist of key identifiers",
|
|
416
|
+
cwe="CWE-22"
|
|
417
|
+
))
|
|
418
|
+
|
|
419
|
+
# Check for command injection
|
|
420
|
+
cmd_patterns = ["|", "`", "$", "(", ")", ";"]
|
|
421
|
+
for pattern in cmd_patterns:
|
|
422
|
+
if pattern in kid:
|
|
423
|
+
findings.append(JWTFinding(
|
|
424
|
+
vulnerability="Potential Command Injection in kid",
|
|
425
|
+
severity="critical",
|
|
426
|
+
description="kid header may allow command injection",
|
|
427
|
+
evidence=f"kid contains: {pattern}",
|
|
428
|
+
attack_vector=f"kid: {kid}",
|
|
429
|
+
remediation="Never use kid in shell commands",
|
|
430
|
+
cwe="CWE-78"
|
|
431
|
+
))
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
return findings
|
|
435
|
+
|
|
436
|
+
def test_jku_jwks_uri(self, jwt_info: JWTInfo) -> List[JWTFinding]:
|
|
437
|
+
"""Test for jku/jwks_uri header injection."""
|
|
438
|
+
findings = []
|
|
439
|
+
|
|
440
|
+
jku = jwt_info.header.get("jku")
|
|
441
|
+
x5u = jwt_info.header.get("x5u")
|
|
442
|
+
|
|
443
|
+
for header_name, value in [("jku", jku), ("x5u", x5u)]:
|
|
444
|
+
if value:
|
|
445
|
+
findings.append(JWTFinding(
|
|
446
|
+
vulnerability=f"External Key Reference ({header_name})",
|
|
447
|
+
severity="high",
|
|
448
|
+
description=f"Token references external key via {header_name}: {value}",
|
|
449
|
+
evidence=f"{header_name}: {value}",
|
|
450
|
+
attack_vector=f"Replace {header_name} with attacker-controlled URL",
|
|
451
|
+
remediation=f"Do not accept {header_name} from tokens. "
|
|
452
|
+
"Use pre-configured key locations.",
|
|
453
|
+
cwe="CWE-345"
|
|
454
|
+
))
|
|
455
|
+
|
|
456
|
+
return findings
|
|
457
|
+
|
|
458
|
+
def generate_attack_tokens(self, jwt_info: JWTInfo) -> Dict[str, str]:
|
|
459
|
+
"""Generate various attack tokens for testing."""
|
|
460
|
+
attacks = {}
|
|
461
|
+
|
|
462
|
+
payload_b64 = jwt_info.raw.split(".")[1]
|
|
463
|
+
|
|
464
|
+
# None algorithm attack
|
|
465
|
+
none_header = self._base64_encode(json.dumps({"alg": "none", "typ": "JWT"}).encode())
|
|
466
|
+
attacks["none_algorithm"] = f"{none_header}.{payload_b64}."
|
|
467
|
+
|
|
468
|
+
# Modified payload (admin escalation)
|
|
469
|
+
modified_payload = jwt_info.payload.copy()
|
|
470
|
+
modified_payload["admin"] = True
|
|
471
|
+
modified_payload["role"] = "admin"
|
|
472
|
+
if "sub" in modified_payload:
|
|
473
|
+
modified_payload["sub"] = "admin"
|
|
474
|
+
|
|
475
|
+
mod_payload_b64 = self._base64_encode(json.dumps(modified_payload).encode())
|
|
476
|
+
attacks["admin_escalation_unsigned"] = f"{none_header}.{mod_payload_b64}."
|
|
477
|
+
|
|
478
|
+
# Expired timestamp manipulation
|
|
479
|
+
no_exp_payload = jwt_info.payload.copy()
|
|
480
|
+
no_exp_payload.pop("exp", None)
|
|
481
|
+
no_exp_b64 = self._base64_encode(json.dumps(no_exp_payload).encode())
|
|
482
|
+
attacks["no_expiration"] = f"{none_header}.{no_exp_b64}."
|
|
483
|
+
|
|
484
|
+
return attacks
|
|
485
|
+
|
|
486
|
+
def analyze(self, token: str) -> Tuple[JWTInfo, List[JWTFinding]]:
|
|
487
|
+
"""
|
|
488
|
+
Perform full JWT analysis.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
token: JWT token string
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Tuple of (JWTInfo, List of findings)
|
|
495
|
+
"""
|
|
496
|
+
findings = []
|
|
497
|
+
|
|
498
|
+
# Parse token
|
|
499
|
+
jwt_info = self.parse_token(token)
|
|
500
|
+
|
|
501
|
+
if not jwt_info:
|
|
502
|
+
findings.append(JWTFinding(
|
|
503
|
+
vulnerability="Invalid JWT Format",
|
|
504
|
+
severity="info",
|
|
505
|
+
description="Token is not a valid JWT format",
|
|
506
|
+
evidence=f"Token: {token[:50]}...",
|
|
507
|
+
remediation="Ensure token has three base64url-encoded parts separated by dots"
|
|
508
|
+
))
|
|
509
|
+
return JWTInfo(
|
|
510
|
+
raw=token,
|
|
511
|
+
header={},
|
|
512
|
+
payload={},
|
|
513
|
+
signature="",
|
|
514
|
+
algorithm="unknown"
|
|
515
|
+
), findings
|
|
516
|
+
|
|
517
|
+
# Run all tests
|
|
518
|
+
findings.extend(self.test_none_algorithm(jwt_info))
|
|
519
|
+
findings.extend(self.test_algorithm_confusion(jwt_info))
|
|
520
|
+
findings.extend(self.test_weak_secret(jwt_info))
|
|
521
|
+
findings.extend(self.test_expiration(jwt_info))
|
|
522
|
+
findings.extend(self.test_sensitive_claims(jwt_info))
|
|
523
|
+
findings.extend(self.test_kid_injection(jwt_info))
|
|
524
|
+
findings.extend(self.test_jku_jwks_uri(jwt_info))
|
|
525
|
+
|
|
526
|
+
return jwt_info, findings
|
|
527
|
+
|
|
528
|
+
def get_summary(self, findings: List[JWTFinding]) -> Dict[str, Any]:
|
|
529
|
+
"""Get summary of findings."""
|
|
530
|
+
return {
|
|
531
|
+
"total": len(findings),
|
|
532
|
+
"critical": len([f for f in findings if f.severity == "critical"]),
|
|
533
|
+
"high": len([f for f in findings if f.severity == "high"]),
|
|
534
|
+
"medium": len([f for f in findings if f.severity == "medium"]),
|
|
535
|
+
"low": len([f for f in findings if f.severity == "low"]),
|
|
536
|
+
"info": len([f for f in findings if f.severity == "info"])
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# Convenience function
|
|
541
|
+
def analyze_jwt(token: str, extended_wordlist: bool = False) -> Tuple[JWTInfo, List[JWTFinding]]:
|
|
542
|
+
"""
|
|
543
|
+
Quick JWT analysis.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
token: JWT token string
|
|
547
|
+
extended_wordlist: Use extended secret wordlist
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Tuple of (JWTInfo, List of findings)
|
|
551
|
+
"""
|
|
552
|
+
analyzer = JWTAnalyzer(extended_wordlist=extended_wordlist)
|
|
553
|
+
return analyzer.analyze(token)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def decode_jwt(token: str) -> Dict[str, Any]:
|
|
557
|
+
"""
|
|
558
|
+
Decode JWT without verification (for inspection).
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
token: JWT token string
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Dict with header and payload
|
|
565
|
+
"""
|
|
566
|
+
analyzer = JWTAnalyzer()
|
|
567
|
+
jwt_info = analyzer.parse_token(token)
|
|
568
|
+
|
|
569
|
+
if jwt_info:
|
|
570
|
+
return {
|
|
571
|
+
"header": jwt_info.header,
|
|
572
|
+
"payload": jwt_info.payload,
|
|
573
|
+
"algorithm": jwt_info.algorithm,
|
|
574
|
+
"expiration": jwt_info.expiration.isoformat() if jwt_info.expiration else None,
|
|
575
|
+
"issued_at": jwt_info.issued_at.isoformat() if jwt_info.issued_at else None
|
|
576
|
+
}
|
|
577
|
+
return {}
|