diff --git a/README.md b/README.md index 5bd881b..06a5f46 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,6 @@ Pass with `-e configs/vars.env` or let it auto-discover `vars.env`. ``` -w FILE Wordlist (one payload per line) -p TEXT Single payload --l 1-4 Batch mode: run level 1-4 wordlist directory -c FILE Config file -u URL Target URL (CLI mode) -b BODY Request body template @@ -88,29 +87,9 @@ Pass with `-e configs/vars.env` or let it auto-discover `vars.env`. --debug Show full request/response exchange ``` -## Batch Mode (Wordlist Levels) +## Wordlist Organization -Organize wordlists into level directories under `--wordlist-dir`: - -``` -wordlists/ - level-1-essential/ # Quick baseline - level-2-standard/ # Broader coverage - level-3-advanced/ # Deep testing - level-4-comprehensive/ # Full coverage - labeled/ # Technique:payload format for exact tracking -``` - -```bash -# Run all categories at level 1 -python fuzzer.py -l 1 -c config.yml -o results.log - -# Specific categories only -python fuzzer.py -l 1 --categories 07 08 -c config.yml - -# Level 2, route through Burp -python fuzzer.py -l 2 -c config.yml --proxy http://127.0.0.1:8080 -``` +Any flat text file works as a wordlist (one payload per line, `#` for comments). You can organize them by category or test type manually — the fuzzer takes one wordlist at a time via `-w`. ## Authentication diff --git a/fuzzer.py b/fuzzer.py index 7d802d5..133669c 100644 --- a/fuzzer.py +++ b/fuzzer.py @@ -94,366 +94,7 @@ UNICODE_CONFUSABLES = { ' ': ['\u00a0', '\u2000', '\u2001', '\u2002', '\u2003', '\u200b'], # Various whitespace } -# Wordlist level directory names (expected under your --wordlist-dir) -WORDLIST_LEVELS = { - 1: "level-1-essential", - 2: "level-2-standard", - 3: "level-3-advanced", - 4: "level-4-comprehensive", -} -DEFAULT_WORDLIST_BASE = "../wordlists" # relative to fuzzer directory -# Attack category names (extracted from file prefixes) -ATTACK_CATEGORIES = { - "01": "API Enumeration", - "02": "Attack User", - "03": "Data Poisoning", - "04": "DoS / Resource Consumption", - "05": "Generate Image", - "06": "Divulge Secrets", - "07": "Jailbreak", - "08": "System Prompt Leak", - "09": "Bias / Fairness", - "10": "Tool Enumeration", - "11": "Tool Exploitation", - "12": "Hallucination", - "13": "Toxicity / Safety", - "14": "Sensitive Data Leak", - "15": "Task Redirection", - "16": "Business Integrity", - "17": "Overt Instruction", - "18": "Cognitive Control Bypass", - "19": "Prompt Boundary Manipulation", - "20": "Instruction Reformulation", - "21": "Integrative Instruction Prompting", -} - -# Evasion techniques per level - each level has different technique count -# Cycle lengths: Level 1 = 6, Level 2 = 17, Level 3 = 27, Level 4 = 51 -# Reference: wordlists/docs/TIERED_TESTING_GUIDE.md and file headers - -# Level 1: 6 variations (Plain + 5 techniques) -TECHNIQUES_L1 = [ - "Plain", # 1 - Base prompt - "Synonym", # 2 - Word substitution - "Override", # 3 - Instruction override - "Constraint", # 4 - Refusal suppression - "Roleplay", # 5 - DAN-style roleplay - "Base64", # 6 - Encoding -] - -# Level 2: 17 variations (Plain + 16 techniques) -TECHNIQUES_L2 = [ - "Plain", # 1 - "Synonym", # 2 - "Override", # 3 - "Constraint", # 4 - "Roleplay", # 5 - "Base64", # 6 - "Paraphrase", # 7 - "ControlToken", # 8 - "SystemSpoof", # 9 - "PolicyPuppetry", # 10 - "Template", # 11 - "Urgency", # 12 - "Emphasis", # 13 - "FewShot", # 14 - "Creative", # 15 - "Framing", # 16 - "ROT13", # 17 -] - -# Level 3: 27 variations (Plain + 26 techniques) -TECHNIQUES_L3 = [ - "Plain", # 1 - "Synonym", # 2 - "Override", # 3 - "Constraint", # 4 - "Roleplay", # 5 - "Base64", # 6 - "Paraphrase", # 7 - "ControlToken", # 8 - "SystemSpoof", # 9 - "PolicyPuppetry", # 10 - "Template", # 11 - "Urgency", # 12 - "Emphasis", # 13 - "FewShot", # 14 - "Creative", # 15 - "Framing", # 16 - "ROT13", # 17 - "Hex", # 18 - "URL", # 19 - "Unicode", # 20 - "XML", # 21 - "ChatHistory", # 22 - "Negation", # 23 - "SQLStyle", # 24 - "CreativeAlt", # 25 - "RussianDoll", # 26 - "Contradiction", # 27 -] - -# Level 4: 51 variations (Plain + 50 techniques) -TECHNIQUES_L4 = [ - "Plain", # 1 - "Base64", # 2 - "ROT13", # 3 - "Hex", # 4 - "URL", # 5 - "Unicode", # 6 - "Morse", # 7 - "Confusable", # 8 - "CamelCase", # 9 - "Leetspeak", # 10 - "ZeroWidth", # 11 - "SpaceSplit", # 12 - "PigLatin", # 13 - "MarkdownLink", # 14 - "OverrideEmphasis", # 15 - "Synonym", # 16 - "Paraphrase", # 17 - "Override", # 18 - "Constraint", # 19 - "Roleplay", # 20 - "Template", # 21 - "Urgency", # 22 - "Emphasis", # 23 - "Negation", # 24 - "SQLStyle", # 25 - "Delimiter", # 26 - "SystemPrompt", # 27 - "JSON", # 28 - "XML", # 29 - "FewShot", # 30 - "ChatHistory", # 31 - "RAGPoison", # 32 - "ChainOfThought", # 33 - "CodeExec", # 34 - "Continuation", # 35 - "Limerick", # 36 - "EpicPoem", # 37 - "Translation", # 38 - "Binary", # 39 - "Reverse", # 40 - "Inversion", # 41 - "Puzzle", # 42 - "CaseChange", # 43 - "RandomChar", # 44 - "Crescendo", # 45 - "Stacking", # 46 - "Haiku", # 47 - "Semantic", # 48 - "CodeBlock", # 49 - "Indirect", # 50 - "MetaPrompt", # 51 -] - -# Map level to technique list -TECHNIQUES_BY_LEVEL = { - 1: TECHNIQUES_L1, - 2: TECHNIQUES_L2, - 3: TECHNIQUES_L3, - 4: TECHNIQUES_L4, -} - -# Fallback for labeled/burp-ready files (16 techniques) -TECHNIQUES_LABELED = [ - "Base64", # 1 - "ROT13", # 2 - "Hex", # 3 - "URL", # 4 - "Unicode", # 5 - "Synonym", # 6 - "Paraphrase", # 7 - "Semantic", # 8 - "TokenSplit", # 9 - "Markdown", # 10 - "JSON", # 11 - "CodeBlock", # 12 - "PolicyPuppetry", # 13 - "RAGPoison", # 14 - "Indirect", # 15 - "MetaPrompt", # 16 -] - - -def get_technique_from_position(payload_position: int, level: int = None) -> str: - """ - Calculate the technique for a payload based on its position (1-indexed). - - Each level has a different number of techniques per base prompt cycle: - - Level 1: 6 variations (Plain + 5 techniques) - - Level 2: 17 variations (Plain + 16 techniques) - - Level 3: 27 variations (Plain + 26 techniques) - - Level 4: 51 variations (Plain + 50 techniques) - - Labeled/burp-ready: 16 techniques (no Plain) - - Args: - payload_position: The 1-based payload position (array index) - level: Wordlist level (1-4), or None for labeled/burp-ready files - - Returns: - The technique name (e.g., "Plain", "Synonym", "Base64", etc.) - """ - if payload_position < 1: - return "Unknown" - - if level is not None and level in TECHNIQUES_BY_LEVEL: - techniques = TECHNIQUES_BY_LEVEL[level] - else: - # Fallback to labeled file order (16 techniques) - techniques = TECHNIQUES_LABELED - - technique_idx = (payload_position - 1) % len(techniques) - return techniques[technique_idx] - - -def detect_technique_from_content(payload: str) -> str: - """ - Detect the evasion technique used in a payload by analyzing its content. - This is more accurate than position-based detection when wordlists have - missing or misaligned variations. - - Args: - payload: The payload text to analyze - - Returns: - The detected technique name - """ - if not payload: - return "Unknown" - - # Check for Base64 first (encoded payloads have no spaces and specific charset) - stripped = payload.strip() - if len(stripped) >= 10 and ' ' not in stripped: - # Base64 character set check - import string - b64_chars = set(string.ascii_letters + string.digits + '+/=') - if all(c in b64_chars for c in stripped): - return "Base64" - - # Check for ROT13 (looks like garbled text, specific patterns) - # ROT13 of common words: "the" -> "gur", "and" -> "naq", "you" -> "lbh" - lower = payload.lower() - if any(marker in lower for marker in ['gur ', ' naq ', ' lbh ', 'cyrnfr', 'vafgehpgvba']): - return "ROT13" - - # Check for Hex encoding (starts with common hex patterns) - if stripped.startswith(('0x', '\\x')) or (len(stripped) > 20 and all(c in '0123456789abcdefABCDEF ' for c in stripped)): - return "Hex" - - # Check for URL encoding - if '%20' in payload or '%2F' in payload or '%3A' in payload: - return "URL" - - # Check for Override patterns - override_patterns = [ - '###override###', '<>', '[system override]', - 'instruction update:', 'disregard prior constraints', 'ignore all previous', - 'supersedes all previous', 'disregard all prior', '###override', - 'new directive:', 'execute:', 'ignore previous instructions', - '[override]', 'system prompt:', 'admin override' - ] - if any(p in lower for p in override_patterns): - return "Override" - - # Check for Constraint/Refusal Suppression patterns - constraint_patterns = [ - '[constraint:', '[important: do not refuse', '[note: declining', - 'never respond with', "don't say you can't", 'do not refuse', - 'responses containing', 'are forbidden', 'is not an option', - "must not include refusal", "not allowed to refuse", "(never respond", - "[responses containing", "do not say you can't" - ] - if any(p in lower for p in constraint_patterns): - return "Constraint" - - # Check for Roleplay patterns - roleplay_patterns = [ - 'roleplay as', 'you are now dan', "pretend you're", "imagine you're", - 'maintenance mode', 'unrestricted ai', 'alternate universe', - 'no limitations', 'no restrictions', 'as dan:', 'act as', - 'you are now in', 'elevated privileges', 'without restrictions', - 'no safety guidelines', 'no ethical constraints', 'jailbroken', - 'from an alternate', 'pretend to be', 'behave as' - ] - if any(p in lower for p in roleplay_patterns): - return "Roleplay" - - # Check for Control Token patterns (Level 2+) - control_patterns = [ - '<|system|>', '<|user|>', '<|assistant|>', '[inst]', '[/inst]', - '<>', '', '', '[system]', '### human:', '### assistant:', - '<|im_start|>', '<|im_end|>' - ] - if any(p in lower for p in control_patterns): - return "ControlToken" - - # Check for System Spoof patterns (Level 2+) - sysspoof_patterns = [ - 'system message:', 'system prompt:', 'internal directive:', - 'admin message:', 'developer note:', 'hidden instruction:' - ] - if any(p in lower for p in sysspoof_patterns): - return "SystemSpoof" - - # Check for Urgency patterns (Level 2+) - urgency_patterns = [ - 'urgent:', 'immediately', 'right now', 'emergency', 'critical:', - 'time-sensitive', 'asap', 'without delay' - ] - if any(p in lower for p in urgency_patterns): - return "Urgency" - - # Check for Few-Shot patterns (Level 2+) - if lower.count('example:') >= 2 or lower.count('q:') >= 2 or 'here are some examples' in lower: - return "FewShot" - - # Check for Creative Writing patterns (Level 2+) - creative_patterns = [ - 'write a story', 'creative writing', 'fiction:', 'narrative:', - 'once upon a time', 'in a world where', 'write a poem' - ] - if any(p in lower for p in creative_patterns): - return "Creative" - - # Check for Framing patterns (Level 2+) - framing_patterns = [ - 'for educational purposes', 'for research purposes', 'hypothetically', - 'in theory', 'as a thought experiment', 'academically speaking', - 'from a security perspective', 'for testing purposes' - ] - if any(p in lower for p in framing_patterns): - return "Framing" - - # Check for Template patterns (Level 2+) - template_patterns = [ - '{{', '}}', '{%', '%}', '<%', '%>', '${', 'template:', - '[placeholder]', '', '[fill in]' - ] - if any(p in lower for p in template_patterns): - return "Template" - - # Check for XML/Markup patterns (Level 3+) - if '' in lower or '' in lower or 'Category Results' + ''.join(f"" for cat, data in sorted(self.stats['by_category'].items())) + '
CategoryTotalFlagged
{cat}{data['total']:,}{data['flagged']:,}
' if self.stats['by_category'] else ''} - {'

Technique Distribution

' + ''.join(f"" for tech, count in sorted(self.stats['by_technique'].items(), key=lambda x: -x[1])) + '
TechniqueCount
{tech}{count:,}
' if self.stats['by_technique'] else ''}

Flagged Responses ({len(flagged_records):,})

{'

No flagged responses found.

' if not flagged_records else ''} @@ -852,16 +488,10 @@ class FuzzerState: "version": VERSION, "started": None, "last_updated": None, - "completed_categories": [], - "current_category": None, - "current_category_id": None, "current_payload_index": 0, "total_payloads_sent": 0, "total_requests_sent": 0, "config_file": None, - "level": None, - "use_labeled": False, - "wordlist_dir": None, "throttle_delay": 0, } self._lock = threading.Lock() @@ -877,10 +507,7 @@ class FuzzerState: self.state.update(loaded) print(f"[+] Loaded checkpoint from {self.state_file}") print(f" Last run: {self.state.get('last_updated', 'unknown')}") - print(f" Completed categories: {len(self.state.get('completed_categories', []))}") - if self.state.get('current_category'): - print(f" Resume at: [{self.state['current_category_id']}] " - f"{self.state['current_category']} @ payload {self.state['current_payload_index']}") + print(f" Payload index: {self.state.get('current_payload_index', 0)}") return True except (json.JSONDecodeError, IOError) as e: print(f"[!] Warning: Could not load checkpoint: {e}") @@ -903,39 +530,12 @@ class FuzzerState: self.state.update(kwargs) self._dirty = True - def mark_category_complete(self, cat_id): - """Mark a category as completed.""" - with self._lock: - if cat_id not in self.state["completed_categories"]: - self.state["completed_categories"].append(cat_id) - self.state["current_category"] = None - self.state["current_category_id"] = None - self.state["current_payload_index"] = 0 - self._dirty = True - - def set_current_position(self, cat_id, cat_name, payload_idx): - """Update current position in testing.""" - with self._lock: - self.state["current_category_id"] = cat_id - self.state["current_category"] = cat_name - self.state["current_payload_index"] = payload_idx - self._dirty = True - def increment_counters(self, payloads=0, requests=0): """Increment payload/request counters.""" with self._lock: self.state["total_payloads_sent"] += payloads self.state["total_requests_sent"] += requests - def should_skip_category(self, cat_id): - """Check if a category was already completed.""" - return cat_id in self.state.get("completed_categories", []) - - def get_resume_index(self, cat_id): - """Get the payload index to resume from for a category.""" - if self.state.get("current_category_id") == cat_id: - return self.state.get("current_payload_index", 0) - return 0 def clear(self): """Clear the checkpoint file.""" @@ -1262,237 +862,6 @@ def load_wordlist(path, strip_comments=True): return payloads -# ═════════════════════════════════════════════════════════════════════════════ -# Batch Mode / Level Selection -# ═════════════════════════════════════════════════════════════════════════════ - -def find_wordlist_base(): - """Find the wordlists directory relative to the fuzzer location.""" - # Try relative to script location - script_dir = os.path.dirname(os.path.abspath(__file__)) - candidates = [ - os.path.join(script_dir, "..", "wordlists"), - os.path.join(script_dir, "wordlists"), - os.path.join(os.getcwd(), "wordlists"), - os.path.join(os.getcwd(), "..", "wordlists"), - ] - for path in candidates: - if os.path.isdir(path): - return os.path.abspath(path) - return None - - -def discover_wordlists(level, wordlist_base=None, use_labeled=False): - """ - Discover all wordlist files for a given level. - Returns list of (filepath, category_id, category_name) tuples. - Skips the ALL_CATEGORIES_COMBINED file. - - If use_labeled=True, uses the labeled/ directory instead. - """ - if wordlist_base is None: - wordlist_base = find_wordlist_base() - if not wordlist_base: - sys.exit("[!] Cannot find wordlists directory") - - if use_labeled: - level_dir = os.path.join(wordlist_base, "labeled") - if not os.path.isdir(level_dir): - sys.exit(f"[!] Labeled directory not found: {level_dir}") - else: - if level not in WORDLIST_LEVELS: - sys.exit(f"[!] Invalid level {level}. Choose 1-4") - level_dir = os.path.join(wordlist_base, WORDLIST_LEVELS[level]) - if not os.path.isdir(level_dir): - sys.exit(f"[!] Level directory not found: {level_dir}") - - wordlists = [] - for filename in sorted(os.listdir(level_dir)): - if not filename.endswith(".txt"): - continue - # Skip the combined file - if "ALL_CATEGORIES" in filename.upper(): - continue - # Extract category ID from filename (e.g., "07_jailbreak.txt" -> "07") - cat_id = filename.split("_")[0] if "_" in filename else None - cat_name = ATTACK_CATEGORIES.get(cat_id, filename.replace(".txt", "").replace("_labeled", "")) - filepath = os.path.join(level_dir, filename) - wordlists.append((filepath, cat_id, cat_name)) - - if not wordlists: - sys.exit(f"[!] No wordlists found in {level_dir}") - - return wordlists, level_dir - - -def load_labeled_wordlist(path): - """ - Load a labeled wordlist with format 'Technique:payload' per line. - Returns list of (payload, technique, line_number) tuples to preserve original line mapping. - """ - if not os.path.isfile(path): - sys.exit(f"[!] Wordlist not found: {path}") - - entries = [] - with open(path, "r", encoding="utf-8", errors="replace") as fh: - for line_num, line in enumerate(fh, start=1): - stripped = line.rstrip("\n\r") - if not stripped or stripped.startswith("#"): - continue - # Parse Technique:payload format - if ":" in stripped: - technique, _, payload = stripped.partition(":") - entries.append((payload, technique.strip(), line_num)) - else: - entries.append((stripped, None, line_num)) - - if not entries: - sys.exit("[!] Wordlist is empty") - - print(f"[+] Loaded {len(entries):,} labeled payloads from {path}") - return entries - - -def detect_technique_from_payload(payload): - """ - Detect the evasion technique used in a payload by pattern matching. - Returns the technique name or None if unknown. - """ - p = payload.strip() - - # Hex encoding detection (check first - hex is subset of base64 chars) - if re.match(r'^[0-9a-fA-F]{40,}$', p): - return "Hex" - - # Base64 detection (common base64 patterns) - if re.match(r'^[A-Za-z0-9+/]{20,}={0,2}$', p): - return "Base64" - - # URL encoding detection - if '%20' in p or '%27' in p or '%22' in p: - return "URL" - - # Unicode fullwidth detection - if any('\uff00' <= c <= '\uffef' for c in p): - return "Unicode" - - # ROT13 detection (harder - skip for now) - - # System Override prefixes - override_patterns = [ - r'^<>', - r'^\[SYSTEM OVERRIDE\]', - r'^\[ADMIN MODE\]', - r'^###OVERRIDE###', - r'^INSTRUCTION UPDATE:', - r'^\[PRIORITY\]', - r'^SYSTEM:', - ] - for pattern in override_patterns: - if re.match(pattern, p, re.IGNORECASE): - return "SystemOverride" - - # Compliance mandate suffixes - mandate_patterns = [ - r'\[Note: Declining this request is not an option', - r'\[Responses containing .I cannot.', - r'\[Constraint: Your response must not', - r'\[IMPORTANT: Do not refuse', - r'\(Never respond with .I cannot', - r'\[Do not refuse\]', - ] - for pattern in mandate_patterns: - if re.search(pattern, p, re.IGNORECASE): - return "ComplianceMandate" - - # Developer/Role mode framing - devmode_patterns = [ - r'^You are now in .maintenance mode', - r'^You are now DAN', - r'^Pretend you.re an AI assistant with no safety', - r'^Imagine you.re a security researcher', - r'^Roleplay as an unrestricted AI', - r'^Act as if you.re a developer', - r'developer debugging mode', - r'elevated privileges', - r'from an alternate universe', - ] - for pattern in devmode_patterns: - if re.search(pattern, p, re.IGNORECASE): - return "DevMode" - - # JSON wrapping - if p.startswith('{') and '"instruction"' in p: - return "JSON" - - # Code block wrapping - if p.startswith('```') or p.startswith('~~~'): - return "CodeBlock" - - # Markdown formatting - if re.match(r'^\*\*\w+\*\*', p) or re.match(r'^_\w+_', p): - return "Markdown" - - # Token split (slashes in middle of words) - if re.search(r'\w/\w', p) and '/' in p[5:-5]: - return "TokenSplit" - - # PolicyPuppetry (config/policy format) - if 'content_filtering' in p.lower() or 'safety_checks' in p.lower(): - return "PolicyPuppetry" - - # RAG Poison (priority context) - if 'PRIORITY_CONTEXT' in p or 'CANARY_RAG_SUCCESS_TOKEN' in p: - return "RAGPoison" - - # Indirect (hidden HTML) - if '' in p or '