Last active
May 23, 2025 16:06
-
-
Save nullenc0de/88e02e641ddd99bd1c49dcbd4f71c30a to your computer and use it in GitHub Desktop.
Revisions
-
nullenc0de revised this gist
May 23, 2025 . 1 changed file with 286 additions and 212 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """ Authentication Bypass Automation Tool v4 Further false positive reduction - Better HTML vs API response detection - Improved JavaScript code filtering - Skip logout endpoints - Enhanced credential pattern validation """ import subprocess @@ -43,46 +43,46 @@ class AuthBypassTester: }) # Disable SSL verification to avoid certificate warnings self.session.verify = False # Expanded response indicators for any API data that might control access self.bypass_indicators = [ # Authentication status 'authenticated', 'logged_in', 'isAuthenticated', 'user_authenticated', 'auth_status', 'login_status', 'authorized', 'session_valid', 'loggedIn', # Access control 'access_granted', 'has_access', 'allowed', 'permitted', 'can_access', 'is_authorized', 'access_level', 'permission_granted', # User status 'active', 'enabled', 'valid', 'verified', 'approved', 'confirmed', 'is_active', 'is_enabled', 'is_valid', 'is_verified', # Subscription/License status 'subscribed', 'premium', 'licensed', 'paid', 'trial_expired', 'subscription_active', 'license_valid', 'account_active', # Feature flags 'feature_enabled', 'beta_access', 'admin_access', 'demo_mode', 'maintenance_mode', 'read_only', 'restricted' ] self.found_urls = set() self.vulnerable_endpoints = [] def run_urlfinder(self) -> List[str]: """Run URLfinder to discover URLs for the target domain""" logger.info(f"Running URLfinder for domain: {self.domain}") try: # Run urlfinder with JSON output cmd = ['urlfinder', '-d', self.domain, '-j', '-silent'] result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) if result.returncode != 0: logger.error(f"URLfinder failed: {result.stderr}") return [] urls = [] for line in result.stdout.strip().split('\n'): if line.strip(): @@ -93,10 +93,10 @@ class AuthBypassTester: # Handle non-JSON lines (might be regular output) if line.startswith('http'): urls.append(line.strip()) logger.info(f"Found {len(urls)} URLs from URLfinder") return urls except subprocess.TimeoutExpired: logger.error("URLfinder timed out after 10 minutes") return [] @@ -107,7 +107,7 @@ class AuthBypassTester: def filter_api_endpoints(self, urls: List[str]) -> List[str]: """Filter URLs that are API endpoints (prioritized and filtered)""" api_urls = [] # Skip common false positive patterns (ENHANCED) skip_patterns = [ r'/wp-json/wp/v2/', @@ -152,8 +152,15 @@ class AuthBypassTester: r'/signin\?', r'/account/login', r'/user/login', # Skip logout endpoints r'/logout', r'/signout', r'/log-out', r'/sign-out', r'/account/logout', r'/user/logout', ] # High priority patterns (test these first) priority_patterns = [ r'/api/v\d+/', @@ -171,7 +178,7 @@ class AuthBypassTester: r'/session/', r'/token/', ] # Regular API patterns regular_patterns = [ r'/ajax/', @@ -182,37 +189,37 @@ class AuthBypassTester: r'/backend/', r'/secure/', ] # First, collect high priority endpoints priority_urls = [] regular_urls = [] for url in urls: # Skip known false positive patterns if any(re.search(pattern, url, re.IGNORECASE) for pattern in skip_patterns): continue # Additional filter: Skip if it looks like a static asset with parameters if re.search(r'\.(jpg|png|gif|svg|ttf|woff|css)\?sw=|sh=|v=', url, re.IGNORECASE): continue # Check for high priority patterns is_priority = any(re.search(pattern, url, re.IGNORECASE) for pattern in priority_patterns) is_regular = any(re.search(pattern, url, re.IGNORECASE) for pattern in regular_patterns) # Special handling for .well-known URLs if '.well-known/' in url: # Quick test to see if this returns HTML (indicating 404/error page) try: test_response = requests.get(url, timeout=5, verify=False) if (test_response.status_code == 200 and 'application/json' in test_response.headers.get('content-type', '')): is_priority = True else: continue # Skip - not JSON content except: continue # Skip if we can't test it # Special handling for JSON files - only include if they're actually config files elif url.endswith('.json'): # Skip manifest.json files as they're meant to be public @@ -225,23 +232,23 @@ class AuthBypassTester: is_priority = True else: continue # Skip other JSON files if is_priority: priority_urls.append(url) elif is_regular: regular_urls.append(url) # Combine with priority first, test all relevant endpoints api_urls = priority_urls + regular_urls # Remove duplicates while preserving order seen = set() filtered_api_urls = [] for url in api_urls: if url not in seen: seen.add(url) filtered_api_urls.append(url) logger.info(f"Found {len(filtered_api_urls)} potential API endpoints (Priority: {len(priority_urls)}, Regular: {len(regular_urls)})") logger.info(f"Filtered out static assets and non-API endpoints") return filtered_api_urls @@ -250,79 +257,79 @@ class AuthBypassTester: """Check if response contains meaningful content (not just empty or error pages)""" if not response or response.status_code >= 400: return False content = response.text.strip() content_length = len(content) # Empty or very short responses aren't meaningful if content_length < 10: return False # Check if it's just an HTML error page if content.lower().startswith('<!doctype html') or '<html' in content.lower()[:100]: # Check for common error page indicators error_indicators = ['404', 'not found', 'error', 'forbidden', 'unauthorized'] if any(indicator in content.lower()[:500] for indicator in error_indicators): return False return True def test_endpoint_bypass(self, url: str) -> Dict: """Test a single endpoint for multiple authentication bypass techniques""" results = [] try: # Test 1: Response Manipulation (original technique) response_manip = self.test_response_manipulation(url) if response_manip['vulnerable']: results.append(response_manip) # Test 2: Missing Authentication missing_auth = self.test_missing_authentication(url) if missing_auth['vulnerable']: results.append(missing_auth) # Test 3: IDOR (parameter manipulation) idor_test = self.test_idor_vulnerability(url) if idor_test['vulnerable']: results.append(idor_test) # Test 4: Weak Token Validation (skip for public files) if not any(public_file in url for public_file in ['manifest.json', '.well-known/', 'robots.txt']): weak_token = self.test_weak_token_validation(url) if weak_token['vulnerable']: results.append(weak_token) # Test 5: Privilege Escalation priv_esc = self.test_privilege_escalation(url) if priv_esc['vulnerable']: results.append(priv_esc) # Test 6: HTTP Method Access Control method_bypass = self.test_http_method_bypass(url) if method_bypass['vulnerable']: results.append(method_bypass) # Test 7: Information Disclosure info_disclosure = self.test_information_disclosure(url) if info_disclosure['vulnerable']: results.append(info_disclosure) # Test 8: CORS Misconfiguration cors_test = self.test_cors_misconfiguration(url) if cors_test['vulnerable']: results.append(cors_test) # Test 9: Error Message Analysis error_analysis = self.test_error_message_disclosure(url) if error_analysis['vulnerable']: results.append(error_analysis) if results: return {'vulnerable': True, 'url': url, 'vulnerabilities': results} else: return {'vulnerable': False, 'url': url} except Exception as e: logger.debug(f"Error testing {url}: {e}") return {'vulnerable': False, 'url': url, 'error': str(e)} @@ -333,13 +340,13 @@ class AuthBypassTester: # Skip OAuth endpoints - they're supposed to redirect if any(oauth_pattern in url.lower() for oauth_pattern in ['/oauth/authorize', '/auth/authorize']): return {'vulnerable': False} response = self.session.get(url, timeout=self.timeout, allow_redirects=False) # Check if it's a JSON API endpoint if 'application/json' in response.headers.get('content-type', ''): return self.test_json_manipulation(url, response) # Check for redirect to SSO/login if response.status_code in [301, 302, 303, 307, 308]: location = response.headers.get('location', '') @@ -352,9 +359,9 @@ class AuthBypassTester: 'details': f'Protected endpoint redirects to authentication: {location}', 'exploitation': 'Intercept response and remove Location header or change status code to 200' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -366,48 +373,95 @@ class AuthBypassTester: 'wp-json/wp/v2/', '/job/', '/events/', 'manifest.json', '/configuration/modals/', 'clientlibs', '/content/', '/login', '/signin', '/oauth/', '/auth/authorize', 'support.', 'help.', '/help_center/', '/support/', '/logout', '/signout', '/log-out', '/sign-out' ]): return {'vulnerable': False} # Test completely unauthenticated request clean_session = requests.Session() clean_session.verify = False response = clean_session.get(url, timeout=self.timeout) if response.status_code == 200 and self.is_meaningful_response(response): content = response.text.lower() # Skip if this is an HTML page (not an API response) content_type = response.headers.get('content-type', '').lower() if 'text/html' in content_type: # For HTML pages, only check for very specific exposed secrets in script tags or meta tags # Skip generic JavaScript code patterns specific_html_patterns = [ r'<meta[^>]+content=["\']([a-zA-Z0-9]{40,})["\']', # Meta tag with long token r'window\.API_KEY\s*=\s*["\']([a-zA-Z0-9]{20,})["\']', # Explicitly set API key r'data-api-key=["\']([a-zA-Z0-9]{20,})["\']', # Data attribute with API key ] for pattern in specific_html_patterns: match = re.search(pattern, response.text, re.IGNORECASE) if match: token = match.group(1) # Validate it's not a placeholder if not any(placeholder in token.lower() for placeholder in ['your-api-key', 'example', 'test', 'demo', 'xxx']): return { 'vulnerable': True, 'method': 'Missing Authentication - Exposed API Key in HTML', 'details': f'Found exposed API key in HTML page', 'exploitation': 'Extract API key from HTML source', 'poc': f'curl -X GET "{url}" | grep -i "api[_-]key"', 'severity': 'HIGH' } return {'vulnerable': False} # Skip other checks for HTML pages # For non-HTML responses (JSON, XML, etc.), use the original patterns critical_patterns = [ r'["\']?api[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9\-_]{20,})["\']?', r'["\']?secret[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9\-_]{20,})["\']?', r'["\']?access[_-]?token["\']?\s*[:=]\s*["\']?([a-zA-Z0-9\-_]{20,})["\']?', r'["\']?private[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9\-_]{20,})["\']?', r'["\']?password["\']?\s*[:=]\s*["\']?([^"\']{8,})["\']?', r'["\']?jwt["\']?\s*[:=]\s*["\']?(eyJ[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]*)["\']?', r'["\']?bearer["\']?\s*[:=]\s*["\']?([a-zA-Z0-9\-_]{20,})["\']?' ] # Check for actual pattern matches (not just keywords) for pattern in critical_patterns: matches = re.findall(pattern, response.text, re.IGNORECASE) if matches: for match in matches: # Extract the actual secret value secret_value = match if isinstance(match, str) else match[-1] # Validate it's not a placeholder or JavaScript variable if any(placeholder in secret_value.lower() for placeholder in ['your-api-key', 'example', 'test', 'demo', 'xxx', '...', 'placeholder', 'sample', 'dummy', 'fake', 'mock']): continue # Skip if it's too short or doesn't look like a real secret if len(secret_value) < 10 or secret_value.isdigit() or secret_value.isalpha(): continue # Additional check: ensure we're not in a JavaScript function definition # Get context around the match match_pos = response.text.find(secret_value) if match_pos > 0: context_before = response.text[max(0, match_pos-50):match_pos] if any(js_pattern in context_before for js_pattern in ['function', 'const ', 'let ', 'var ', '${', 'querySelector']): continue return { 'vulnerable': True, 'method': 'Missing Authentication - Critical Secret Exposure', 'details': f'Exposed secret in API response', 'exploitation': 'Direct access to sensitive credentials without authentication', 'poc': f'curl -X GET "{url}"', 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text, 'severity': 'CRITICAL' } # Check for API documentation exposure if any(keyword in content for keyword in ['swagger', 'openapi', 'api-docs']) and len(content) > 1000: return { @@ -418,9 +472,9 @@ class AuthBypassTester: 'poc': f'curl -X GET "{url}"', 'severity': 'HIGH' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -439,35 +493,35 @@ class AuthBypassTester: (r'[?&]user=(\d+)', 'user'), (r'[?&]uid=(\d+)', 'uid') ] for pattern, param_type in id_patterns: match = re.search(pattern, url) if match: original_id = match.group(1) # Try different IDs test_ids = [str(int(original_id) + 1), str(int(original_id) - 1), '1', '999'] # Get baseline response baseline_response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(baseline_response): continue for test_id in test_ids: if '?' in pattern or '&' in pattern: test_url = re.sub(pattern, f'{pattern.split("=")[0]}={test_id}', url) else: test_url = url.replace(f'/{original_id}', f'/{test_id}') response = self.session.get(test_url, timeout=self.timeout) # Check if we got a different, meaningful response if (response.status_code == 200 and self.is_meaningful_response(response) and response.text != baseline_response.text and len(response.text) > 100): # Additional check: response should contain user/account data if any(indicator in response.text.lower() for indicator in ['email', 'name', 'phone', 'address', 'account', 'profile']): return { 'vulnerable': True, @@ -476,9 +530,9 @@ class AuthBypassTester: 'exploitation': f'Change {param_type} ID to access other users\' data', 'poc': f'curl -X GET "{test_url}"' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -488,48 +542,54 @@ class AuthBypassTester: # Skip static assets and public files static_extensions = r'\.(jpg|jpeg|png|gif|svg|ico|webp|ttf|eot|woff|woff2|css|js)(\?.*)?$' public_files = ['manifest.json', 'robots.txt', '.well-known/', 'sitemap.xml'] if (re.search(static_extensions, url, re.IGNORECASE) or any(public_file in url for public_file in public_files)): return {'vulnerable': False} # Skip login/auth pages - they don't validate tokens if any(pattern in url.lower() for pattern in ['/login', '/signin', '/oauth/', '/auth/', '/account/login', '/user/login', '/logout', '/signout', '/customer/account', '/account/orders']): return {'vulnerable': False} # First test normal request to establish baseline normal_response = self.session.get(url, timeout=self.timeout) # Skip if baseline response is not meaningful or is HTML if not self.is_meaningful_response(normal_response): return {'vulnerable': False} # Skip HTML pages - they often return same content regardless of auth content_type = normal_response.headers.get('content-type', '').lower() if 'text/html' in content_type: return {'vulnerable': False} # Test with invalid/tampered tokens invalid_tokens = [ 'Bearer invalid_token_12345', 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.invalid_signature', 'Bearer admin_bypass_token', 'Basic YWRtaW46YWRtaW4=' # admin:admin in base64 ] for token in invalid_tokens: headers = {'Authorization': token} response = self.session.get(url, headers=headers, timeout=self.timeout) # Only flag as vulnerable if: # 1. Returns 200 status # 2. Response is meaningful # 3. Response suggests successful authentication if (response.status_code == 200 and self.is_meaningful_response(response) and len(response.text) > 50): # Check if response indicates successful auth auth_success_indicators = [ '"authenticated":true', '"status":"success"', '"loggedIn":true', '"user":{', '"profile":{', '"permissions":[', '"role":"' ] if any(indicator in response.text for indicator in auth_success_indicators): return { 'vulnerable': True, @@ -540,27 +600,27 @@ class AuthBypassTester: 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text, 'severity': 'CRITICAL' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_privilege_escalation(self, url: str) -> Dict: """Test 5: Privilege Escalation via Parameter Tampering""" try: # Skip if URL doesn't look like it handles user data if not any(keyword in url.lower() for keyword in ['user', 'account', 'profile', 'admin', 'dashboard', 'panel', 'manage']): return {'vulnerable': False} # First get baseline response baseline_response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(baseline_response): return {'vulnerable': False} baseline_content = baseline_response.text.lower() # Test with privilege escalation parameters priv_params = [ {'role': 'admin'}, @@ -571,27 +631,27 @@ class AuthBypassTester: {'privilege': 'root'}, {'access_level': '999'} ] for params in priv_params: response = self.session.get(url, params=params, timeout=self.timeout) if response.status_code == 200 and self.is_meaningful_response(response): content = response.text.lower() # Check if response suggests elevated privileges admin_gained_indicators = [ 'admin panel', 'administrator dashboard', 'system settings', 'user management', 'delete user', 'modify permissions', 'root access', 'superuser', '"role":"admin"', '"isAdmin":true' ] # Check if we gained admin content that wasn't in baseline admin_gained = any(indicator in content and indicator not in baseline_content for indicator in admin_gained_indicators) # Also check for significant content change content_significantly_different = abs(len(content) - len(baseline_content)) > 500 if admin_gained or (content_significantly_different and 'admin' in content): return { 'vulnerable': True, @@ -601,9 +661,9 @@ class AuthBypassTester: 'poc': f'curl -X GET "{url}?" + "&".join([f"{k}={v}" for k,v in params.items()])', 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -616,23 +676,23 @@ class AuthBypassTester: r'/images/', r'/fonts/', r'/css/', r'/static/', r'/assets/', r'/configuration/', r'/modals/', r'/content/' ] if any(re.search(pattern, url, re.IGNORECASE) if pattern.endswith('$') else pattern in url.lower() for pattern in static_patterns): return {'vulnerable': False} # Get baseline with GET request get_response = self.session.get(url, timeout=self.timeout) # Only test if GET returns meaningful content if not self.is_meaningful_response(get_response): return {'vulnerable': False} get_content_length = len(get_response.text) # Test dangerous methods dangerous_methods = ['PUT', 'DELETE', 'PATCH'] for method in dangerous_methods: # Add test data for methods that accept body test_data = None @@ -641,35 +701,35 @@ class AuthBypassTester: headers = {'Content-Type': 'application/json'} else: headers = {} response = self.session.request( method, url, data=test_data, headers=headers, timeout=self.timeout ) # Only flag as vulnerable if: # 1. Method returns success status # 2. Response indicates actual processing (not just error page) # 3. Response is different from GET (suggests method was processed) if response.status_code in [200, 201, 202, 204]: content = response.text.lower() # Check for indicators of successful operation success_indicators = [ 'success', 'updated', 'deleted', 'created', 'modified', 'saved', 'removed', '"status":200', '"ok":true' ] error_indicators = [ 'error', 'forbidden', 'unauthorized', 'not allowed', 'access denied', 'permission denied', 'rejected' ] has_success = any(indicator in content for indicator in success_indicators) has_errors = any(indicator in content for indicator in error_indicators) # For DELETE, 204 with no content is actually a success indicator if method == 'DELETE' and response.status_code == 204: return { @@ -680,7 +740,7 @@ class AuthBypassTester: 'poc': f'curl -X {method} "{url}"', 'severity': 'HIGH' } # For other methods, check for success without errors elif has_success and not has_errors: return { @@ -691,9 +751,9 @@ class AuthBypassTester: 'poc': f'curl -X {method} "{url}"' + (f' -d \'{test_data}\' -H "Content-Type: application/json"' if test_data else ''), 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -703,12 +763,12 @@ class AuthBypassTester: # Skip login pages if any(pattern in url.lower() for pattern in ['/login', '/signin', '/oauth/', '/auth/']): return {'vulnerable': False} response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(response): return {'vulnerable': False} # Look for exposed secrets with better patterns secret_patterns = [ (r'["\']?api[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9\-_]{20,})["\']', 'API Key'), @@ -723,7 +783,7 @@ class AuthBypassTester: (r'["\']?github[_-]?token["\']?\s*[:=]\s*["\']([a-zA-Z0-9]{40})["\']', 'GitHub Token'), (r'["\']?stripe[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9_]{24,})["\']', 'Stripe API Key') ] found_secrets = [] for pattern, secret_type in secret_patterns: matches = re.findall(pattern, response.text, re.IGNORECASE) @@ -732,8 +792,8 @@ class AuthBypassTester: real_secrets = [] for secret in matches: # Skip placeholders if any(placeholder in secret.lower() for placeholder in ['your-api-key', 'example', 'test', 'demo', 'xxx', '...', 'placeholder', 'sample', 'dummy', 'fake', 'mock']): continue # Skip repeated characters @@ -743,10 +803,10 @@ class AuthBypassTester: if secret.isdigit() or secret.isalpha(): continue real_secrets.append(secret) if real_secrets: found_secrets.extend([(secret_type, secret[:20] + '...') for secret in real_secrets]) if found_secrets: return { 'vulnerable': True, @@ -758,9 +818,9 @@ class AuthBypassTester: 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text, 'severity': 'CRITICAL' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -770,29 +830,29 @@ class AuthBypassTester: # Skip non-API endpoints and support pages if not any(pattern in url.lower() for pattern in ['api', 'ajax', 'json', 'rest', 'graphql']): return {'vulnerable': False} # Skip support/help endpoints which are meant to be public if any(pattern in url.lower() for pattern in ['support.', 'help.', '/help_center/', '/support/']): return {'vulnerable': False} headers = {'Origin': 'https://evil-attacker.com'} response = self.session.get(url, headers=headers, timeout=self.timeout) # Only check successful responses with meaningful content if not (response.status_code == 200 and self.is_meaningful_response(response)): return {'vulnerable': False} cors_header = response.headers.get('Access-Control-Allow-Origin', '') cors_credentials = response.headers.get('Access-Control-Allow-Credentials', '') # Check for overly permissive CORS if cors_header == '*' or cors_header == 'https://evil-attacker.com': # Additional check: is this actually sensitive data? sensitive_content = any(indicator in response.text.lower() for indicator in [ 'password', 'token', 'secret', 'api_key', 'private', 'confidential', 'credit', 'ssn', 'email', 'phone', 'address', 'user_id', 'session' ]) # Also check if response contains user-specific data patterns user_data_patterns = [ r'"user":\s*{', @@ -801,24 +861,24 @@ class AuthBypassTester: r'"account":\s*{', r'"session":\s*"[^"]+"' ] has_user_data = any(re.search(pattern, response.text) for pattern in user_data_patterns) if sensitive_content or has_user_data or ('true' in cors_credentials.lower() and cors_header != '*'): severity = 'CRITICAL' if cors_credentials.lower() == 'true' else 'HIGH' return { 'vulnerable': True, 'method': 'CORS Misconfiguration', 'details': f'Allows unauthorized origin: {cors_header}' + (f' with credentials' if cors_credentials.lower() == 'true' else ''), 'exploitation': 'Cross-origin data theft possible from malicious websites', 'poc': f'curl -X GET "{url}" -H "Origin: https://evil-attacker.com"', 'cors_header': cors_header, 'severity': severity } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -828,11 +888,11 @@ class AuthBypassTester: # Skip if URL doesn't look like an API endpoint if not any(pattern in url.lower() for pattern in ['api', 'ajax', 'rest', 'service']): return {'vulnerable': False} # Skip support/help endpoints if any(pattern in url.lower() for pattern in ['support.', 'help.', '/help_center/', '/support/']): return {'vulnerable': False} # Trigger errors with malformed requests error_triggers = [ {'method': 'POST', 'data': 'invalid json', 'headers': {'Content-Type': 'application/json'}}, @@ -841,15 +901,15 @@ class AuthBypassTester: {'method': 'GET', 'params': {'debug': 'true', 'test': '../../../etc/passwd'}}, {'method': 'POST', 'data': '<invalid>xml</xml>', 'headers': {'Content-Type': 'application/xml'}} ] for trigger in error_triggers: method = trigger.pop('method', 'GET') response = self.session.request(method, url, timeout=self.timeout, **trigger) # Look for real error disclosures (not just HTML pages) if self.is_meaningful_response(response) or response.status_code >= 400: content = response.text # Enhanced patterns for real security issues critical_patterns = [ (r'(com\.[\w\.]+Exception)', 'Java Exception'), @@ -861,14 +921,14 @@ class AuthBypassTester: (r'(\/home\/[\w\/]+\.[\w]+|\/var\/www\/[\w\/]+\.[\w]+|C:\\[\w\\]+\.[\w]+)', 'File Path Disclosure'), (r'(SELECT\s+\*?\s*FROM\s+[\w\.]+|INSERT\s+INTO\s+[\w\.]+|UPDATE\s+[\w\.]+\s+SET|DELETE\s+FROM\s+[\w\.]+)', 'SQL Query Disclosure'), (r'(mysql_fetch_|mysqli_|pg_query|oci_parse)\s*\(', 'Database Function'), (r'(root|admin|password|pwd)\s*[:=]\s*["\']?([a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:,.<>?]{8,})["\']?(?!\w)', 'Credential Disclosure'), (r'(MongoDB|Redis|PostgreSQL|MySQL|Oracle|MSSQL)\s+(error|Error|ERROR)', 'Database Error'), (r'"stack":\s*"[^"]+Error[^"]+"', 'JSON Stack Trace'), (r'Traceback\s+\(most\s+recent\s+call\s+last\)', 'Python Traceback'), (r'Warning:\s+[\w_]+\(\)\s+\[[\w\.]+\]:', 'PHP Warning'), (r'Fatal error:\s+[\w\s]+in\s+[\w\/\.]+\s+on\s+line\s+\d+', 'PHP Fatal Error') ] for pattern, disclosure_type in critical_patterns: match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE) if match: @@ -878,17 +938,30 @@ class AuthBypassTester: # Must have actual SQL structure, not just keywords if not any(sql_struct in query_text.upper() for sql_struct in ['SELECT', 'FROM', 'INSERT INTO', 'UPDATE', 'DELETE FROM']): continue # For file paths, ensure it's an actual file path if disclosure_type == 'File Path Disclosure': path_text = match.group(1) # Must have file extension or be clearly a path file_extensions = r'\.(php|py|java|cs|rb|js)$' if not (re.search(file_extensions, path_text) or '/' in path_text or '\\' in path_text): continue # For credential disclosure, do extra validation if disclosure_type == 'Credential Disclosure': # Skip if it's part of JavaScript code match_text = match.group(0) if any(js_term in match_text for js_term in ['shadowRoot', 'formToroRoot', 'documentRoot', 'querySelector']): continue # Check if there's an actual value after the = if match.lastindex >= 2: cred_value = match.group(2) if len(cred_value) < 6 or cred_value in ['true', 'false', 'null', 'undefined', 'this', 'self']: continue # Extract relevant error context error_context = content[max(0, match.start()-100):min(len(content), match.end()+100)] return { 'vulnerable': True, 'method': f'Information Disclosure - {disclosure_type}', @@ -898,9 +971,9 @@ class AuthBypassTester: 'error_sample': error_context, 'severity': 'HIGH' if 'Stack Trace' in disclosure_type or 'SQL' in disclosure_type else 'MEDIUM' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} @@ -921,16 +994,16 @@ class AuthBypassTester: try: if original_response.status_code != 200: return {'vulnerable': False} try: data = original_response.json() except: return {'vulnerable': False} # Look for authentication/authorization fields bypass_found = False potential_bypasses = [] for key, value in self.flatten_dict(data).items(): # Check for auth-related fields with negative values if any(indicator in key.lower() for indicator in self.bypass_indicators): @@ -942,10 +1015,10 @@ class AuthBypassTester: 'suggested_value': self.get_bypass_value(key, value), 'impact': self.assess_bypass_impact(key) }) # Only report if we found high-impact bypasses high_impact_bypasses = [b for b in potential_bypasses if b['impact'] == 'HIGH'] if high_impact_bypasses: return { 'vulnerable': True, @@ -956,25 +1029,25 @@ class AuthBypassTester: 'poc': f'curl -X GET "{url}" # Intercept with Burp Suite and modify response', 'response_sample': json.dumps(data, indent=2)[:300] + '...' if len(json.dumps(data)) > 300 else json.dumps(data, indent=2) } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def assess_bypass_impact(self, field_name: str) -> str: """Assess the impact of bypassing a specific field""" field_lower = field_name.lower() high_impact_keywords = [ 'authenticated', 'authorized', 'logged_in', 'isadmin', 'is_admin', 'admin', 'root', 'superuser', 'premium', 'paid', 'licensed' ] medium_impact_keywords = [ 'enabled', 'active', 'valid', 'verified', 'confirmed' ] if any(keyword in field_lower for keyword in high_impact_keywords): return 'HIGH' elif any(keyword in field_lower for keyword in medium_impact_keywords): @@ -985,22 +1058,22 @@ class AuthBypassTester: def get_bypass_value(self, key: str, current_value) -> str: """Suggest appropriate bypass value based on the field name and current value""" key_lower = key.lower() if any(word in key_lower for word in ['expired', 'disabled', 'restricted']): return 'false' if current_value else 'true' elif 'level' in key_lower or 'role' in key_lower: return 'admin' if isinstance(current_value, str) else '999' else: return 'true' if current_value is False else 'false' def check_for_auth_errors(self, data: dict) -> bool: """Check if response contains authentication/authorization errors""" error_indicators = [ 'unauthorized', 'forbidden', 'access_denied', 'insufficient_privileges', 'login_required', 'authentication_required', 'invalid_token', 'permission_denied', 'not_authorized', 'access_forbidden' ] # Convert entire response to lowercase string for searching response_str = json.dumps(data).lower() return any(indicator in response_str for indicator in error_indicators) @@ -1019,11 +1092,11 @@ class AuthBypassTester: def test_all_endpoints(self, urls: List[str]) -> List[Dict]: """Test all API endpoints concurrently for multiple bypass techniques""" logger.info(f"Testing {len(urls)} endpoints for authentication bypasses using 9 different techniques") results = [] with ThreadPoolExecutor(max_workers=self.threads) as executor: future_to_url = {executor.submit(self.test_endpoint_bypass, url): url for url in urls} for future in as_completed(future_to_url): result = future.result() if result['vulnerable']: @@ -1041,24 +1114,24 @@ class AuthBypassTester: self.vulnerable_endpoints.append(result) logger.info(f"🚨 VULNERABLE: {result['url']} - {result.get('method', 'Unknown')}") results.append(result) return results def generate_report(self, results: List[Dict]) -> str: """Generate a detailed report of findings""" total_tested = len(results) vulnerable = len(self.vulnerable_endpoints) report = f""" === Enhanced API Security Test Results (v4) === Domain: {self.domain} Total URLs discovered: {len(self.found_urls)} API endpoints tested: {total_tested} Vulnerable endpoints found: {vulnerable} TESTS PERFORMED: 1. Response Manipulation (Boolean/Redirect Bypass) 2. Missing Authentication on Sensitive Endpoints 3. Insecure Direct Object Reference (IDOR) 4. Weak Session Token/Cookie Validation 5. Privilege Escalation via Parameter Tampering @@ -1068,15 +1141,15 @@ TESTS PERFORMED: 9. Error Message Information Disclosure """ if vulnerable > 0: # Sort vulnerabilities by severity critical_vulns = [v for v in self.vulnerable_endpoints if v.get('severity') == 'CRITICAL'] high_vulns = [v for v in self.vulnerable_endpoints if v.get('severity') == 'HIGH'] other_vulns = [v for v in self.vulnerable_endpoints if v.get('severity') not in ['CRITICAL', 'HIGH']] sorted_vulns = critical_vulns + high_vulns + other_vulns report += "=== VULNERABLE API ENDPOINTS ===\n" for vuln in sorted_vulns: severity = vuln.get('severity', 'MEDIUM') @@ -1095,23 +1168,23 @@ Exploitation: {vuln.get('exploitation', 'Manual testing required')} 📄 Response Sample: {vuln['response_sample']} """ if 'bypass_opportunities' in vuln: report += "\n🎯 Specific Bypass Opportunities:\n" for bypass in vuln['bypass_opportunities']: report += f" - Change '{bypass['field']}' from '{bypass['current_value']}' to '{bypass['suggested_value']}' (Impact: {bypass.get('impact', 'Unknown')})\n" if 'secrets_found' in vuln: report += f"\n🔑 Secrets Found: {', '.join(vuln['secrets_found'])}\n" if 'cors_header' in vuln: report += f"\n🌐 CORS Header: {vuln['cors_header']}\n" if 'error_sample' in vuln: report += f"\n⚠️ Error Sample:\n{vuln['error_sample']}\n" report += "\n" + "="*60 + "\n" report += f""" === SUMMARY BY VULNERABILITY TYPE === """ @@ -1120,71 +1193,72 @@ Exploitation: {vuln.get('exploitation', 'Manual testing required')} for vuln in self.vulnerable_endpoints: vuln_type = vuln.get('method', 'Unknown') vuln_types[vuln_type] = vuln_types.get(vuln_type, 0) + 1 for vuln_type, count in sorted(vuln_types.items()): report += f"{vuln_type}: {count} endpoints\n" # Add severity summary report += f""" === SUMMARY BY SEVERITY === CRITICAL: {len(critical_vulns)} vulnerabilities HIGH: {len(high_vulns)} vulnerabilities MEDIUM/LOW: {len(other_vulns)} vulnerabilities """ return report def run_full_scan(self) -> str: """Run the complete authentication bypass scan""" logger.info(f"Starting enhanced API security scan for {self.domain}") # Step 1: Discover URLs all_urls = self.run_urlfinder() if not all_urls: return "No URLs discovered. Check URLfinder installation and domain validity." self.found_urls = set(all_urls) # Step 2: Filter API endpoints api_urls = self.filter_api_endpoints(all_urls) if not api_urls: return "No API endpoints found in discovered URLs." # Step 3: Test for bypasses results = self.test_all_endpoints(api_urls) # Step 4: Generate report return self.generate_report(results) def main(): parser = argparse.ArgumentParser(description='Enhanced Authentication Bypass Tester v4') parser.add_argument('domain', help='Target domain to test') parser.add_argument('-t', '--threads', type=int, default=10, help='Number of threads (default: 10)') parser.add_argument('-o', '--output', help='Output file for results') parser.add_argument('--timeout', type=int, default=10, help='Request timeout in seconds (default: 10)') args = parser.parse_args() # Validate domain if not args.domain or '.' not in args.domain: logger.error("Please provide a valid domain") return # Warning message print("⚠️ WARNING: Only use this tool on domains you own or have explicit permission to test!") print("Unauthorized testing may be illegal and could result in serious consequences.") print("\n📌 Enhanced Version 4.0 - Further False Positive Reduction") print(" - Better HTML vs API response detection") print(" - Improved JavaScript code filtering") print(" - Skip logout endpoints") print(" - Enhanced credential validation\n") # Run the scan tester = AuthBypassTester(args.domain, threads=args.threads, timeout=args.timeout) report = tester.run_full_scan() # Output results print(report) if args.output: with open(args.output, 'w') as f: f.write(report) -
nullenc0de revised this gist
May 23, 2025 . 1 changed file with 121 additions and 29 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,11 @@ #!/usr/bin/env python3 """ Authentication Bypass Automation Tool v3 Major false positive reduction update - Skips support/help center endpoints - Better OAuth/login page handling - Improved SQL/file path detection - Enhanced secret validation """ import subprocess @@ -131,6 +135,23 @@ class AuthBypassTester: r'/en_in/.*configuration/modals', # Localized modal configs r'\.html?(\?.*)?$', # HTML files r'/content/.*/configuration/', # CMS configuration paths # Skip support/help center endpoints r'support\.', # Support subdomains r'help\.', # Help subdomains r'/help_center/', # Help center paths r'/support/', # Support paths r'/hc/', # Help center abbreviated r'/knowledge/', # Knowledge base r'/kb/', # Knowledge base abbreviated r'/faq/', # FAQ sections r'/docs/', # Documentation # Skip OAuth/auth endpoints (handled separately) r'/oauth/authorize', r'/auth/authorize', r'/login\?', r'/signin\?', r'/account/login', r'/user/login', ] # High priority patterns (test these first) @@ -309,6 +330,10 @@ class AuthBypassTester: def test_response_manipulation(self, url: str) -> Dict: """Test 1: Response Manipulation Bypass""" try: # Skip OAuth endpoints - they're supposed to redirect if any(oauth_pattern in url.lower() for oauth_pattern in ['/oauth/authorize', '/auth/authorize']): return {'vulnerable': False} response = self.session.get(url, timeout=self.timeout, allow_redirects=False) # Check if it's a JSON API endpoint @@ -318,11 +343,13 @@ class AuthBypassTester: # Check for redirect to SSO/login if response.status_code in [301, 302, 303, 307, 308]: location = response.headers.get('location', '') # Only flag if it's protecting actual API/data endpoints, not OAuth flows if (any(keyword in location.lower() for keyword in ['login', 'auth', 'sso', 'signin']) and not any(skip in url.lower() for skip in ['/login', '/signin', '/auth/', '/oauth/'])): return { 'vulnerable': True, 'method': 'Response Manipulation - Redirect Bypass', 'details': f'Protected endpoint redirects to authentication: {location}', 'exploitation': 'Intercept response and remove Location header or change status code to 200' } @@ -337,7 +364,9 @@ class AuthBypassTester: # Skip common false positives if any(pattern in url.lower() for pattern in [ 'wp-json/wp/v2/', '/job/', '/events/', 'manifest.json', '/configuration/modals/', 'clientlibs', '/content/', '/login', '/signin', '/oauth/', '/auth/authorize', 'support.', 'help.', '/help_center/', '/support/' ]): return {'vulnerable': False} @@ -375,7 +404,8 @@ class AuthBypassTester: 'details': f'Exposed secret matching pattern: {pattern}', 'exploitation': 'Direct access to sensitive credentials without authentication', 'poc': f'curl -X GET "{url}"', 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text, 'severity': 'CRITICAL' } # Check for API documentation exposure @@ -385,7 +415,8 @@ class AuthBypassTester: 'method': 'Missing Authentication - API Documentation Exposure', 'details': 'API documentation accessible without authentication', 'exploitation': 'Access to complete API structure and endpoints', 'poc': f'curl -X GET "{url}"', 'severity': 'HIGH' } return {'vulnerable': False} @@ -462,6 +493,10 @@ class AuthBypassTester: any(public_file in url for public_file in public_files)): return {'vulnerable': False} # Skip login/auth pages - they don't validate tokens if any(pattern in url.lower() for pattern in ['/login', '/signin', '/oauth/', '/auth/', '/account/login', '/user/login']): return {'vulnerable': False} # First test normal request to establish baseline normal_response = self.session.get(url, timeout=self.timeout) @@ -502,7 +537,8 @@ class AuthBypassTester: 'details': f'Server accepts invalid token and returns authenticated response', 'exploitation': 'Use any invalid token to bypass authentication', 'poc': f'curl -X GET "{url}" -H "Authorization: {token}"', 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text, 'severity': 'CRITICAL' } return {'vulnerable': False} @@ -664,6 +700,10 @@ class AuthBypassTester: def test_information_disclosure(self, url: str) -> Dict: """Test 7: Exposed API Key or Token in Unauthenticated Requests""" try: # Skip login pages if any(pattern in url.lower() for pattern in ['/login', '/signin', '/oauth/', '/auth/']): return {'vulnerable': False} response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(response): @@ -678,18 +718,31 @@ class AuthBypassTester: (r'["\']?aws[_-]?access[_-]?key[_-]?id["\']?\s*[:=]\s*["\']([A-Z0-9]{20})["\']', 'AWS Access Key'), (r'["\']?aws[_-]?secret[_-]?access[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9/+=]{40})["\']', 'AWS Secret Key'), (r'Bearer\s+([a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]*)', 'JWT Token'), (r'Basic\s+([a-zA-Z0-9+/=]{20,})', 'Basic Auth Credentials'), (r'["\']?client[_-]?secret["\']?\s*[:=]\s*["\']([a-zA-Z0-9\-_]{20,})["\']', 'OAuth Client Secret'), (r'["\']?github[_-]?token["\']?\s*[:=]\s*["\']([a-zA-Z0-9]{40})["\']', 'GitHub Token'), (r'["\']?stripe[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9_]{24,})["\']', 'Stripe API Key') ] found_secrets = [] for pattern, secret_type in secret_patterns: matches = re.findall(pattern, response.text, re.IGNORECASE) if matches: # Filter out obvious placeholders and common false positives real_secrets = [] for secret in matches: # Skip placeholders if any(placeholder in secret.lower() for placeholder in ['your-api-key', 'example', 'test', 'demo', 'xxx', '...', 'placeholder', 'sample', 'dummy', 'fake', 'mock']): continue # Skip repeated characters if len(set(secret)) < 5: continue # Skip if it's just numbers or just letters (likely not a real key) if secret.isdigit() or secret.isalpha(): continue real_secrets.append(secret) if real_secrets: found_secrets.extend([(secret_type, secret[:20] + '...') for secret in real_secrets]) @@ -702,7 +755,8 @@ class AuthBypassTester: 'exploitation': 'Extract and use exposed API keys/tokens', 'poc': f'curl -X GET "{url}"', 'secrets_found': [f'{stype}: {svalue}' for stype, svalue in found_secrets[:3]], 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text, 'severity': 'CRITICAL' } return {'vulnerable': False} @@ -713,10 +767,14 @@ class AuthBypassTester: def test_cors_misconfiguration(self, url: str) -> Dict: """Test 8: CORS Misconfiguration""" try: # Skip non-API endpoints and support pages if not any(pattern in url.lower() for pattern in ['api', 'ajax', 'json', 'rest', 'graphql']): return {'vulnerable': False} # Skip support/help endpoints which are meant to be public if any(pattern in url.lower() for pattern in ['support.', 'help.', '/help_center/', '/support/']): return {'vulnerable': False} headers = {'Origin': 'https://evil-attacker.com'} response = self.session.get(url, headers=headers, timeout=self.timeout) @@ -732,10 +790,21 @@ class AuthBypassTester: # Additional check: is this actually sensitive data? sensitive_content = any(indicator in response.text.lower() for indicator in [ 'password', 'token', 'secret', 'api_key', 'private', 'confidential', 'credit', 'ssn', 'email', 'phone', 'address', 'user_id', 'session' ]) # Also check if response contains user-specific data patterns user_data_patterns = [ r'"user":\s*{', r'"email":\s*"[^"]+@[^"]+"', r'"profile":\s*{', r'"account":\s*{', r'"session":\s*"[^"]+"' ] has_user_data = any(re.search(pattern, response.text) for pattern in user_data_patterns) if sensitive_content or has_user_data or ('true' in cors_credentials.lower() and cors_header != '*'): severity = 'CRITICAL' if cors_credentials.lower() == 'true' else 'HIGH' return { 'vulnerable': True, @@ -760,6 +829,10 @@ class AuthBypassTester: if not any(pattern in url.lower() for pattern in ['api', 'ajax', 'rest', 'service']): return {'vulnerable': False} # Skip support/help endpoints if any(pattern in url.lower() for pattern in ['support.', 'help.', '/help_center/', '/support/']): return {'vulnerable': False} # Trigger errors with malformed requests error_triggers = [ {'method': 'POST', 'data': 'invalid json', 'headers': {'Content-Type': 'application/json'}}, @@ -782,21 +855,37 @@ class AuthBypassTester: (r'(com\.[\w\.]+Exception)', 'Java Exception'), (r'(org\.[\w\.]+Exception)', 'Java Framework Exception'), (r'at\s+[\w\.$]+\([\w\.]+:\d+\)', 'Java Stack Trace'), (r'File\s+"([^"]+\.py)"\,?\s+line\s+\d+', 'Python Stack Trace'), (r'(Microsoft\.[\w\.]+Exception)', '.NET Exception'), (r'(System\.[\w\.]+Exception)', '.NET System Exception'), (r'(\/home\/[\w\/]+\.[\w]+|\/var\/www\/[\w\/]+\.[\w]+|C:\\[\w\\]+\.[\w]+)', 'File Path Disclosure'), (r'(SELECT\s+\*?\s*FROM\s+[\w\.]+|INSERT\s+INTO\s+[\w\.]+|UPDATE\s+[\w\.]+\s+SET|DELETE\s+FROM\s+[\w\.]+)', 'SQL Query Disclosure'), (r'(mysql_fetch_|mysqli_|pg_query|oci_parse)\s*\(', 'Database Function'), (r'(root|admin|password|pwd)\s*[:=]\s*["\']?([a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:,.<>?]+)["\']?', 'Credential Disclosure'), (r'(MongoDB|Redis|PostgreSQL|MySQL|Oracle|MSSQL)\s+(error|Error|ERROR)', 'Database Error'), (r'"stack":\s*"[^"]+Error[^"]+"', 'JSON Stack Trace'), (r'Traceback\s+\(most\s+recent\s+call\s+last\)', 'Python Traceback'), (r'Warning:\s+[\w_]+\(\)\s+\[[\w\.]+\]:', 'PHP Warning'), (r'Fatal error:\s+[\w\s]+in\s+[\w\/\.]+\s+on\s+line\s+\d+', 'PHP Fatal Error') ] for pattern, disclosure_type in critical_patterns: match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE) if match: # For SQL queries, ensure it's a real query not just words if disclosure_type == 'SQL Query Disclosure': query_text = match.group(0) # Must have actual SQL structure, not just keywords if not any(sql_struct in query_text.upper() for sql_struct in ['SELECT', 'FROM', 'INSERT INTO', 'UPDATE', 'DELETE FROM']): continue # For file paths, ensure it's an actual file path if disclosure_type == 'File Path Disclosure': path_text = match.group(1) # Must have file extension or be clearly a path if not (re.search(r'\.(php|py|java|cs|rb|js)$', path_text) or '/' in path_text or '\\' in path_text): continue # Extract relevant error context error_context = content[max(0, match.start()-100):min(len(content), match.end()+100)] @@ -807,7 +896,7 @@ class AuthBypassTester: 'exploitation': 'Craft malformed requests to extract system information', 'poc': self._generate_error_poc(url, trigger, method), 'error_sample': error_context, 'severity': 'HIGH' if 'Stack Trace' in disclosure_type or 'SQL' in disclosure_type else 'MEDIUM' } return {'vulnerable': False} @@ -961,7 +1050,7 @@ class AuthBypassTester: vulnerable = len(self.vulnerable_endpoints) report = f""" === Enhanced API Security Test Results (v3) === Domain: {self.domain} Total URLs discovered: {len(self.found_urls)} API endpoints tested: {total_tested} @@ -1068,7 +1157,7 @@ MEDIUM/LOW: {len(other_vulns)} vulnerabilities return self.generate_report(results) def main(): parser = argparse.ArgumentParser(description='Enhanced Authentication Bypass Tester v3') parser.add_argument('domain', help='Target domain to test') parser.add_argument('-t', '--threads', type=int, default=10, help='Number of threads (default: 10)') parser.add_argument('-o', '--output', help='Output file for results') @@ -1084,7 +1173,10 @@ def main(): # Warning message print("⚠️ WARNING: Only use this tool on domains you own or have explicit permission to test!") print("Unauthorized testing may be illegal and could result in serious consequences.") print("\n📌 Enhanced Version 3.0 - Major False Positive Reduction") print(" - Skips support/help center endpoints") print(" - Better OAuth and login page handling") print(" - Improved error detection patterns\n") # Run the scan tester = AuthBypassTester(args.domain, threads=args.threads, timeout=args.timeout) -
nullenc0de revised this gist
May 22, 2025 . 1 changed file with 423 additions and 224 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ Authentication Bypass Automation Tool v2 Enhanced version with reduced false positives """ import subprocess @@ -104,7 +104,7 @@ class AuthBypassTester: """Filter URLs that are API endpoints (prioritized and filtered)""" api_urls = [] # Skip common false positive patterns (ENHANCED) skip_patterns = [ r'/wp-json/wp/v2/', r'/job/\d+/', @@ -126,18 +126,29 @@ class AuthBypassTester: r'\.min\.(js|css)(\?.*)?$', r'/demandware\.static/', r'\.gif(\?.*)?$', # Add patterns for common false positives r'/configuration/modals/', # Modal configuration endpoints r'/en_in/.*configuration/modals', # Localized modal configs r'\.html?(\?.*)?$', # HTML files r'/content/.*/configuration/', # CMS configuration paths ] # High priority patterns (test these first) priority_patterns = [ r'/api/v\d+/', r'/api/(?!.*/(gtm|noticeError))', r'/v\d+/(?!.*wp)', r'developers\.', r'/apispecs/', r'/graphql', r'/rest/(?!.*wp)', r'/oauth/', r'/auth/', r'/login/', r'/user/', r'/account/', r'/session/', r'/token/', ] # Regular API patterns @@ -147,8 +158,8 @@ class AuthBypassTester: r'/internal/', r'/private/', r'/admin/api/', r'/backend/', r'/secure/', ] # First, collect high priority endpoints @@ -174,20 +185,22 @@ class AuthBypassTester: try: test_response = requests.get(url, timeout=5, verify=False) if (test_response.status_code == 200 and 'application/json' in test_response.headers.get('content-type', '')): is_priority = True else: continue # Skip - not JSON content except: continue # Skip if we can't test it # Special handling for JSON files - only include if they're actually config files elif url.endswith('.json'): # Skip manifest.json files as they're meant to be public if 'manifest.json' in url: continue # Include if it's in config or API directories if any(keyword in url.lower() for keyword in ['config', 'api', 'jwks', 'settings']): # But exclude if it's clearly in a static directory if not any(static_dir in url.lower() for static_dir in ['static', 'assets', 'images', 'css', 'clientlibs']): is_priority = True else: continue # Skip other JSON files @@ -212,6 +225,27 @@ class AuthBypassTester: logger.info(f"Filtered out static assets and non-API endpoints") return filtered_api_urls def is_meaningful_response(self, response: requests.Response) -> bool: """Check if response contains meaningful content (not just empty or error pages)""" if not response or response.status_code >= 400: return False content = response.text.strip() content_length = len(content) # Empty or very short responses aren't meaningful if content_length < 10: return False # Check if it's just an HTML error page if content.lower().startswith('<!doctype html') or '<html' in content.lower()[:100]: # Check for common error page indicators error_indicators = ['404', 'not found', 'error', 'forbidden', 'unauthorized'] if any(indicator in content.lower()[:500] for indicator in error_indicators): return False return True def test_endpoint_bypass(self, url: str) -> Dict: """Test a single endpoint for multiple authentication bypass techniques""" results = [] @@ -232,10 +266,11 @@ class AuthBypassTester: if idor_test['vulnerable']: results.append(idor_test) # Test 4: Weak Token Validation (skip for public files) if not any(public_file in url for public_file in ['manifest.json', '.well-known/', 'robots.txt']): weak_token = self.test_weak_token_validation(url) if weak_token['vulnerable']: results.append(weak_token) # Test 5: Privilege Escalation priv_esc = self.test_privilege_escalation(url) @@ -283,12 +318,12 @@ class AuthBypassTester: # Check for redirect to SSO/login if response.status_code in [301, 302, 303, 307, 308]: location = response.headers.get('location', '') if any(keyword in location.lower() for keyword in ['login', 'auth', 'sso', 'signin', 'oauth']): return { 'vulnerable': True, 'method': 'Response Manipulation - Redirect Bypass', 'details': f'Authentication redirect detected to: {location}', 'exploitation': 'Intercept response and remove Location header or change status code to 200' } return {'vulnerable': False} @@ -301,8 +336,8 @@ class AuthBypassTester: try: # Skip common false positives if any(pattern in url.lower() for pattern in [ 'wp-json/wp/v2/', '/job/', '/events/', 'manifest.json', '/configuration/modals/', 'clientlibs', '/content/' ]): return {'vulnerable': False} @@ -311,53 +346,46 @@ class AuthBypassTester: clean_session.verify = False response = clean_session.get(url, timeout=self.timeout) if response.status_code == 200 and self.is_meaningful_response(response): content = response.text.lower() # High-value sensitive patterns (ENHANCED) critical_patterns = [ r'api[_-]?key["\']?\s*[:=]\s*["\']?[a-zA-Z0-9]{20,}', r'secret[_-]?key["\']?\s*[:=]\s*["\']?[a-zA-Z0-9]{20,}', r'access[_-]?token["\']?\s*[:=]\s*["\']?[a-zA-Z0-9]{20,}', r'private[_-]?key["\']?\s*[:=]\s*["\']?[a-zA-Z0-9]{20,}', r'password["\']?\s*[:=]\s*["\']?[^"\']{8,}', r'connection[_-]?string["\']?\s*[:=]\s*["\']?[^"\']+', r'database[_-]?url["\']?\s*[:=]\s*["\']?[^"\']+', r'jwt["\']?\s*[:=]\s*["\']?eyJ[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]+', r'bearer["\']?\s*[:=]\s*["\']?[a-zA-Z0-9]{20,}', r'oauth[_-]?secret["\']?\s*[:=]\s*["\']?[a-zA-Z0-9]{20,}' ] # Check for actual pattern matches (not just keywords) for pattern in critical_patterns: if re.search(pattern, response.text, re.IGNORECASE): # Extract the matched secret for validation match = re.search(pattern, response.text, re.IGNORECASE) if match: return { 'vulnerable': True, 'method': 'Missing Authentication - Critical Secret Exposure', 'details': f'Exposed secret matching pattern: {pattern}', 'exploitation': 'Direct access to sensitive credentials without authentication', 'poc': f'curl -X GET "{url}"', 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text } # Check for API documentation exposure if any(keyword in content for keyword in ['swagger', 'openapi', 'api-docs']) and len(content) > 1000: return { 'vulnerable': True, 'method': 'Missing Authentication - API Documentation Exposure', 'details': 'API documentation accessible without authentication', 'exploitation': 'Access to complete API structure and endpoints', 'poc': f'curl -X GET "{url}"' } return {'vulnerable': False} @@ -369,26 +397,54 @@ class AuthBypassTester: """Test 3: Insecure Direct Object Reference (IDOR)""" try: # Look for numeric IDs in URL id_patterns = [ (r'/user/(\d+)', 'user'), (r'/account/(\d+)', 'account'), (r'/profile/(\d+)', 'profile'), (r'/order/(\d+)', 'order'), (r'/invoice/(\d+)', 'invoice'), (r'/document/(\d+)', 'document'), (r'[?&]id=(\d+)', 'id'), (r'[?&]user=(\d+)', 'user'), (r'[?&]uid=(\d+)', 'uid') ] for pattern, param_type in id_patterns: match = re.search(pattern, url) if match: original_id = match.group(1) # Try different IDs test_ids = [str(int(original_id) + 1), str(int(original_id) - 1), '1', '999'] # Get baseline response baseline_response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(baseline_response): continue for test_id in test_ids: if '?' in pattern or '&' in pattern: test_url = re.sub(pattern, f'{pattern.split("=")[0]}={test_id}', url) else: test_url = url.replace(f'/{original_id}', f'/{test_id}') response = self.session.get(test_url, timeout=self.timeout) # Check if we got a different, meaningful response if (response.status_code == 200 and self.is_meaningful_response(response) and response.text != baseline_response.text and len(response.text) > 100): # Additional check: response should contain user/account data if any(indicator in response.text.lower() for indicator in ['email', 'name', 'phone', 'address', 'account', 'profile']): return { 'vulnerable': True, 'method': f'IDOR (Insecure Direct Object Reference) - {param_type} parameter', 'details': f'Successfully accessed {param_type} ID {test_id} (original: {original_id})', 'exploitation': f'Change {param_type} ID to access other users\' data', 'poc': f'curl -X GET "{test_url}"' } return {'vulnerable': False} @@ -398,49 +454,56 @@ class AuthBypassTester: def test_weak_token_validation(self, url: str) -> Dict: """Test 4: Weak Session Token or Cookie Validation""" try: # Skip static assets and public files static_extensions = r'\.(jpg|jpeg|png|gif|svg|ico|webp|ttf|eot|woff|woff2|css|js)(\?.*)?$' public_files = ['manifest.json', 'robots.txt', '.well-known/', 'sitemap.xml'] if (re.search(static_extensions, url, re.IGNORECASE) or any(public_file in url for public_file in public_files)): return {'vulnerable': False} # First test normal request to establish baseline normal_response = self.session.get(url, timeout=self.timeout) # Skip if baseline response is not meaningful if not self.is_meaningful_response(normal_response): return {'vulnerable': False} # Test with invalid/tampered tokens invalid_tokens = [ 'Bearer invalid_token_12345', 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.invalid_signature', 'Bearer admin_bypass_token', 'Basic YWRtaW46YWRtaW4=' # admin:admin in base64 ] for token in invalid_tokens: headers = {'Authorization': token} response = self.session.get(url, headers=headers, timeout=self.timeout) # Only flag as vulnerable if: # 1. Returns 200 status # 2. Response is meaningful # 3. Response suggests successful authentication if (response.status_code == 200 and self.is_meaningful_response(response) and len(response.text) > 50): # Check if response indicates successful auth auth_success_indicators = [ '"authenticated":true', '"status":"success"', '"loggedIn":true', '"user":{', '"profile":{', '"permissions":[', '"role":"' ] if any(indicator in response.text for indicator in auth_success_indicators): return { 'vulnerable': True, 'method': 'Weak Token Validation - Authentication Bypass', 'details': f'Server accepts invalid token and returns authenticated response', 'exploitation': 'Use any invalid token to bypass authentication', 'poc': f'curl -X GET "{url}" -H "Authorization: {token}"', 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} @@ -450,39 +513,57 @@ class AuthBypassTester: def test_privilege_escalation(self, url: str) -> Dict: """Test 5: Privilege Escalation via Parameter Tampering""" try: # Skip if URL doesn't look like it handles user data if not any(keyword in url.lower() for keyword in ['user', 'account', 'profile', 'admin', 'dashboard', 'panel', 'manage']): return {'vulnerable': False} # First get baseline response baseline_response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(baseline_response): return {'vulnerable': False} baseline_content = baseline_response.text.lower() # Test with privilege escalation parameters priv_params = [ {'role': 'admin'}, {'isAdmin': 'true'}, {'is_admin': '1'}, {'admin': '1'}, {'userRole': 'administrator'}, {'privilege': 'root'}, {'access_level': '999'} ] for params in priv_params: response = self.session.get(url, params=params, timeout=self.timeout) if response.status_code == 200 and self.is_meaningful_response(response): content = response.text.lower() # Check if response suggests elevated privileges admin_gained_indicators = [ 'admin panel', 'administrator dashboard', 'system settings', 'user management', 'delete user', 'modify permissions', 'root access', 'superuser', '"role":"admin"', '"isAdmin":true' ] # Check if we gained admin content that wasn't in baseline admin_gained = any(indicator in content and indicator not in baseline_content for indicator in admin_gained_indicators) # Also check for significant content change content_significantly_different = abs(len(content) - len(baseline_content)) > 500 if admin_gained or (content_significantly_different and 'admin' in content): return { 'vulnerable': True, 'method': 'Privilege Escalation via Parameter Tampering', 'details': f'Successfully escalated privileges using parameters: {params}', 'exploitation': 'Add privilege parameters to gain admin access', 'poc': f'curl -X GET "{url}?" + "&".join([f"{k}={v}" for k,v in params.items()])', 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text } return {'vulnerable': False} @@ -493,57 +574,85 @@ class AuthBypassTester: def test_http_method_bypass(self, url: str) -> Dict: """Test 6: Broken Access Control on HTTP Methods""" try: # Skip static assets and configuration endpoints static_patterns = [ r'\.(jpg|jpeg|png|gif|svg|ico|webp|ttf|eot|woff|woff2|css|js)(\?.*)?$', r'/images/', r'/fonts/', r'/css/', r'/static/', r'/assets/', r'/configuration/', r'/modals/', r'/content/' ] if any(re.search(pattern, url, re.IGNORECASE) if pattern.endswith('$') else pattern in url.lower() for pattern in static_patterns): return {'vulnerable': False} # Get baseline with GET request get_response = self.session.get(url, timeout=self.timeout) # Only test if GET returns meaningful content if not self.is_meaningful_response(get_response): return {'vulnerable': False} get_content_length = len(get_response.text) # Test dangerous methods dangerous_methods = ['PUT', 'DELETE', 'PATCH'] for method in dangerous_methods: # Add test data for methods that accept body test_data = None if method in ['PUT', 'PATCH']: test_data = json.dumps({'test': 'data'}) headers = {'Content-Type': 'application/json'} else: headers = {} response = self.session.request( method, url, data=test_data, headers=headers, timeout=self.timeout ) # Only flag as vulnerable if: # 1. Method returns success status # 2. Response indicates actual processing (not just error page) # 3. Response is different from GET (suggests method was processed) if response.status_code in [200, 201, 202, 204]: content = response.text.lower() # Check for indicators of successful operation success_indicators = [ 'success', 'updated', 'deleted', 'created', 'modified', 'saved', 'removed', '"status":200', '"ok":true' ] error_indicators = [ 'error', 'forbidden', 'unauthorized', 'not allowed', 'access denied', 'permission denied', 'rejected' ] has_success = any(indicator in content for indicator in success_indicators) has_errors = any(indicator in content for indicator in error_indicators) # For DELETE, 204 with no content is actually a success indicator if method == 'DELETE' and response.status_code == 204: return { 'vulnerable': True, 'method': f'HTTP Method Bypass - {method} (Destructive Operation)', 'details': f'{method} operation completed successfully (Status: 204 No Content)', 'exploitation': f'Use {method} method to delete resources without authorization', 'poc': f'curl -X {method} "{url}"', 'severity': 'HIGH' } # For other methods, check for success without errors elif has_success and not has_errors: return { 'vulnerable': True, 'method': f'HTTP Method Bypass - {method}', 'details': f'{method} method processed successfully (Status: {response.status_code})', 'exploitation': f'Use {method} method to modify resources without proper authorization', 'poc': f'curl -X {method} "{url}"' + (f' -d \'{test_data}\' -H "Content-Type: application/json"' if test_data else ''), 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } @@ -557,27 +666,44 @@ class AuthBypassTester: try: response = self.session.get(url, timeout=self.timeout) if not self.is_meaningful_response(response): return {'vulnerable': False} # Look for exposed secrets with better patterns secret_patterns = [ (r'["\']?api[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9\-_]{20,})["\']', 'API Key'), (r'["\']?secret[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9\-_]{20,})["\']', 'Secret Key'), (r'["\']?access[_-]?token["\']?\s*[:=]\s*["\']([a-zA-Z0-9\-_]{20,})["\']', 'Access Token'), (r'["\']?private[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9\-_]{20,})["\']', 'Private Key'), (r'["\']?aws[_-]?access[_-]?key[_-]?id["\']?\s*[:=]\s*["\']([A-Z0-9]{20})["\']', 'AWS Access Key'), (r'["\']?aws[_-]?secret[_-]?access[_-]?key["\']?\s*[:=]\s*["\']([a-zA-Z0-9/+=]{40})["\']', 'AWS Secret Key'), (r'Bearer\s+([a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]*)', 'JWT Token'), (r'Basic\s+([a-zA-Z0-9+/=]{20,})', 'Basic Auth Credentials') ] found_secrets = [] for pattern, secret_type in secret_patterns: matches = re.findall(pattern, response.text, re.IGNORECASE) if matches: # Filter out obvious placeholders real_secrets = [m for m in matches if not any( placeholder in m.lower() for placeholder in ['your-api-key', 'example', 'test', 'demo', 'xxx', '...', 'placeholder'] )] if real_secrets: found_secrets.extend([(secret_type, secret[:20] + '...') for secret in real_secrets]) if found_secrets: return { 'vulnerable': True, 'method': 'Information Disclosure - Exposed Secrets', 'details': f'Found {len(found_secrets)} exposed secrets in response', 'exploitation': 'Extract and use exposed API keys/tokens', 'poc': f'curl -X GET "{url}"', 'secrets_found': [f'{stype}: {svalue}' for stype, svalue in found_secrets[:3]], 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text } return {'vulnerable': False} @@ -587,31 +713,39 @@ class AuthBypassTester: def test_cors_misconfiguration(self, url: str) -> Dict: """Test 8: CORS Misconfiguration""" try: # Skip non-API endpoints if not any(pattern in url.lower() for pattern in ['api', 'ajax', 'json', 'rest', 'graphql']): return {'vulnerable': False} headers = {'Origin': 'https://evil-attacker.com'} response = self.session.get(url, headers=headers, timeout=self.timeout) # Only check successful responses with meaningful content if not (response.status_code == 200 and self.is_meaningful_response(response)): return {'vulnerable': False} cors_header = response.headers.get('Access-Control-Allow-Origin', '') cors_credentials = response.headers.get('Access-Control-Allow-Credentials', '') # Check for overly permissive CORS if cors_header == '*' or cors_header == 'https://evil-attacker.com': # Additional check: is this actually sensitive data? sensitive_content = any(indicator in response.text.lower() for indicator in [ 'password', 'token', 'secret', 'api_key', 'private', 'confidential', 'credit', 'ssn', 'email', 'phone', 'address' ]) if sensitive_content or ('true' in cors_credentials.lower() and cors_header != '*'): severity = 'CRITICAL' if cors_credentials.lower() == 'true' else 'HIGH' return { 'vulnerable': True, 'method': 'CORS Misconfiguration', 'details': f'Allows unauthorized origin: {cors_header}' + (f' with credentials' if cors_credentials.lower() == 'true' else ''), 'exploitation': 'Cross-origin data theft possible from malicious websites', 'poc': f'curl -X GET "{url}" -H "Origin: https://evil-attacker.com"', 'cors_header': cors_header, 'severity': severity } return {'vulnerable': False} @@ -622,46 +756,77 @@ class AuthBypassTester: def test_error_message_disclosure(self, url: str) -> Dict: """Test 9: Information Disclosure in Error Messages""" try: # Skip if URL doesn't look like an API endpoint if not any(pattern in url.lower() for pattern in ['api', 'ajax', 'rest', 'service']): return {'vulnerable': False} # Trigger errors with malformed requests error_triggers = [ {'method': 'POST', 'data': 'invalid json', 'headers': {'Content-Type': 'application/json'}}, {'method': 'POST', 'data': '{"test": }', 'headers': {'Content-Type': 'application/json'}}, {'method': 'GET', 'params': {'id': "' OR 1=1 --"}}, {'method': 'GET', 'params': {'debug': 'true', 'test': '../../../etc/passwd'}}, {'method': 'POST', 'data': '<invalid>xml</xml>', 'headers': {'Content-Type': 'application/xml'}} ] for trigger in error_triggers: method = trigger.pop('method', 'GET') response = self.session.request(method, url, timeout=self.timeout, **trigger) # Look for real error disclosures (not just HTML pages) if self.is_meaningful_response(response) or response.status_code >= 400: content = response.text # Enhanced patterns for real security issues critical_patterns = [ (r'(com\.[\w\.]+Exception)', 'Java Exception'), (r'(org\.[\w\.]+Exception)', 'Java Framework Exception'), (r'at\s+[\w\.$]+\([\w\.]+:\d+\)', 'Java Stack Trace'), (r'File\s+"([^"]+)"\,?\s+line\s+\d+', 'Python Stack Trace'), (r'(Microsoft\.[\w\.]+Exception)', '.NET Exception'), (r'(System\.[\w\.]+Exception)', '.NET System Exception'), (r'(\/home\/[\w\/]+|\/var\/www\/[\w\/]+|C:\\[\w\\]+)', 'File Path Disclosure'), (r'(SELECT\s+.*\s+FROM\s+|INSERT\s+INTO\s+|UPDATE\s+.*\s+SET)', 'SQL Query Disclosure'), (r'(mysql_fetch_|mysqli_|pg_query|oci_parse)', 'Database Function'), (r'(root|admin|password|pwd)\s*[:=]\s*["\']?[\w]+', 'Credential Disclosure'), (r'(MongoDB|Redis|PostgreSQL|MySQL|Oracle|MSSQL)\s+Error', 'Database Error'), (r'"stack":\s*"[^"]+Error[^"]+"', 'JSON Stack Trace'), (r'Traceback\s+\(most\s+recent\s+call\s+last\)', 'Python Traceback') ] for pattern, disclosure_type in critical_patterns: match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE) if match: # Extract relevant error context error_context = content[max(0, match.start()-100):min(len(content), match.end()+100)] return { 'vulnerable': True, 'method': f'Information Disclosure - {disclosure_type}', 'details': f'Error message reveals sensitive information: {disclosure_type}', 'exploitation': 'Craft malformed requests to extract system information', 'poc': self._generate_error_poc(url, trigger, method), 'error_sample': error_context, 'severity': 'HIGH' if 'Stack Trace' in disclosure_type else 'MEDIUM' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def _generate_error_poc(self, url: str, trigger: dict, method: str) -> str: """Generate POC command for error triggering""" if method == 'POST': data = trigger.get('data', '') headers = trigger.get('headers', {}) header_str = ' '.join([f'-H "{k}: {v}"' for k, v in headers.items()]) return f'curl -X POST "{url}" -d \'{data}\' {header_str}' else: params = trigger.get('params', {}) param_str = '&'.join([f'{k}={v}' for k, v in params.items()]) return f'curl -X GET "{url}?{param_str}"' def test_json_manipulation(self, url: str, original_response: requests.Response) -> Dict: """Test JSON response manipulation bypass""" try: @@ -673,45 +838,61 @@ class AuthBypassTester: except: return {'vulnerable': False} # Look for authentication/authorization fields bypass_found = False potential_bypasses = [] for key, value in self.flatten_dict(data).items(): # Check for auth-related fields with negative values if any(indicator in key.lower() for indicator in self.bypass_indicators): if value is False or str(value).lower() in ['false', '0', 'no', 'disabled', 'expired', 'unauthorized']: bypass_found = True potential_bypasses.append({ 'field': key, 'current_value': value, 'suggested_value': self.get_bypass_value(key, value), 'impact': self.assess_bypass_impact(key) }) # Only report if we found high-impact bypasses high_impact_bypasses = [b for b in potential_bypasses if b['impact'] == 'HIGH'] if high_impact_bypasses: return { 'vulnerable': True, 'method': 'JSON Response Manipulation - Authentication Bypass', 'details': f'Found {len(high_impact_bypasses)} high-impact bypass opportunities', 'bypass_opportunities': high_impact_bypasses[:3], # Limit to top 3 'exploitation': 'Intercept response and modify authentication fields', 'poc': f'curl -X GET "{url}" # Intercept with Burp Suite and modify response', 'response_sample': json.dumps(data, indent=2)[:300] + '...' if len(json.dumps(data)) > 300 else json.dumps(data, indent=2) } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def assess_bypass_impact(self, field_name: str) -> str: """Assess the impact of bypassing a specific field""" field_lower = field_name.lower() high_impact_keywords = [ 'authenticated', 'authorized', 'logged_in', 'isadmin', 'is_admin', 'admin', 'root', 'superuser', 'premium', 'paid', 'licensed' ] medium_impact_keywords = [ 'enabled', 'active', 'valid', 'verified', 'confirmed' ] if any(keyword in field_lower for keyword in high_impact_keywords): return 'HIGH' elif any(keyword in field_lower for keyword in medium_impact_keywords): return 'MEDIUM' else: return 'LOW' def get_bypass_value(self, key: str, current_value) -> str: """Suggest appropriate bypass value based on the field name and current value""" key_lower = key.lower() @@ -780,7 +961,7 @@ class AuthBypassTester: vulnerable = len(self.vulnerable_endpoints) report = f""" === Enhanced API Security Test Results (v2) === Domain: {self.domain} Total URLs discovered: {len(self.found_urls)} API endpoints tested: {total_tested} @@ -800,10 +981,19 @@ TESTS PERFORMED: """ if vulnerable > 0: # Sort vulnerabilities by severity critical_vulns = [v for v in self.vulnerable_endpoints if v.get('severity') == 'CRITICAL'] high_vulns = [v for v in self.vulnerable_endpoints if v.get('severity') == 'HIGH'] other_vulns = [v for v in self.vulnerable_endpoints if v.get('severity') not in ['CRITICAL', 'HIGH']] sorted_vulns = critical_vulns + high_vulns + other_vulns report += "=== VULNERABLE API ENDPOINTS ===\n" for vuln in sorted_vulns: severity = vuln.get('severity', 'MEDIUM') report += f""" URL: {vuln['url']} Severity: {severity} Vulnerability: {vuln.get('method', 'Unknown')} Details: {vuln.get('details', 'No additional details')} Exploitation: {vuln.get('exploitation', 'Manual testing required')} @@ -820,7 +1010,7 @@ Exploitation: {vuln.get('exploitation', 'Manual testing required')} if 'bypass_opportunities' in vuln: report += "\n🎯 Specific Bypass Opportunities:\n" for bypass in vuln['bypass_opportunities']: report += f" - Change '{bypass['field']}' from '{bypass['current_value']}' to '{bypass['suggested_value']}' (Impact: {bypass.get('impact', 'Unknown')})\n" if 'secrets_found' in vuln: report += f"\n🔑 Secrets Found: {', '.join(vuln['secrets_found'])}\n" @@ -829,7 +1019,7 @@ Exploitation: {vuln.get('exploitation', 'Manual testing required')} report += f"\n🌐 CORS Header: {vuln['cors_header']}\n" if 'error_sample' in vuln: report += f"\n⚠️ Error Sample:\n{vuln['error_sample']}\n" report += "\n" + "="*60 + "\n" @@ -844,12 +1034,20 @@ Exploitation: {vuln.get('exploitation', 'Manual testing required')} for vuln_type, count in sorted(vuln_types.items()): report += f"{vuln_type}: {count} endpoints\n" # Add severity summary report += f""" === SUMMARY BY SEVERITY === CRITICAL: {len(critical_vulns)} vulnerabilities HIGH: {len(high_vulns)} vulnerabilities MEDIUM/LOW: {len(other_vulns)} vulnerabilities """ return report def run_full_scan(self) -> str: """Run the complete authentication bypass scan""" logger.info(f"Starting enhanced API security scan for {self.domain}") # Step 1: Discover URLs all_urls = self.run_urlfinder() @@ -870,7 +1068,7 @@ Exploitation: {vuln.get('exploitation', 'Manual testing required')} return self.generate_report(results) def main(): parser = argparse.ArgumentParser(description='Enhanced Authentication Bypass Tester v2') parser.add_argument('domain', help='Target domain to test') parser.add_argument('-t', '--threads', type=int, default=10, help='Number of threads (default: 10)') parser.add_argument('-o', '--output', help='Output file for results') @@ -886,6 +1084,7 @@ def main(): # Warning message print("⚠️ WARNING: Only use this tool on domains you own or have explicit permission to test!") print("Unauthorized testing may be illegal and could result in serious consequences.") print("\n📌 Enhanced Version 2.0 - Reduced False Positives") # Run the scan tester = AuthBypassTester(args.domain, threads=args.threads, timeout=args.timeout) -
nullenc0de created this gist
May 22, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,903 @@ #!/usr/bin/env python3 """ Authentication Bypass Automation Tool Combines URLfinder with automated authentication bypass testing """ import subprocess import requests import json import re import time import argparse from urllib.parse import urlparse, urljoin from concurrent.futures import ThreadPoolExecutor, as_completed import logging from typing import List, Dict, Set import urllib3 # Disable SSL warnings and certificate verification warnings urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) requests.packages.urllib3.disable_warnings() # Configure logging to suppress certificate warnings logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Suppress specific certificate warnings logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR) logging.getLogger("requests.packages.urllib3").setLevel(logging.ERROR) class AuthBypassTester: def __init__(self, domain: str, threads: int = 10, timeout: int = 10): self.domain = domain self.threads = threads self.timeout = timeout self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) # Disable SSL verification to avoid certificate warnings self.session.verify = False # Expanded response indicators for any API data that might control access self.bypass_indicators = [ # Authentication status 'authenticated', 'logged_in', 'isAuthenticated', 'user_authenticated', 'auth_status', 'login_status', 'authorized', 'session_valid', 'loggedIn', # Access control 'access_granted', 'has_access', 'allowed', 'permitted', 'can_access', 'is_authorized', 'access_level', 'permission_granted', # User status 'active', 'enabled', 'valid', 'verified', 'approved', 'confirmed', 'is_active', 'is_enabled', 'is_valid', 'is_verified', # Subscription/License status 'subscribed', 'premium', 'licensed', 'paid', 'trial_expired', 'subscription_active', 'license_valid', 'account_active', # Feature flags 'feature_enabled', 'beta_access', 'admin_access', 'demo_mode', 'maintenance_mode', 'read_only', 'restricted' ] self.found_urls = set() self.vulnerable_endpoints = [] def run_urlfinder(self) -> List[str]: """Run URLfinder to discover URLs for the target domain""" logger.info(f"Running URLfinder for domain: {self.domain}") try: # Run urlfinder with JSON output cmd = ['urlfinder', '-d', self.domain, '-j', '-silent'] result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) if result.returncode != 0: logger.error(f"URLfinder failed: {result.stderr}") return [] urls = [] for line in result.stdout.strip().split('\n'): if line.strip(): try: data = json.loads(line) urls.append(data['url']) except json.JSONDecodeError: # Handle non-JSON lines (might be regular output) if line.startswith('http'): urls.append(line.strip()) logger.info(f"Found {len(urls)} URLs from URLfinder") return urls except subprocess.TimeoutExpired: logger.error("URLfinder timed out after 10 minutes") return [] except FileNotFoundError: logger.error("URLfinder not found. Make sure it's installed and in PATH") return [] def filter_api_endpoints(self, urls: List[str]) -> List[str]: """Filter URLs that are API endpoints (prioritized and filtered)""" api_urls = [] # Skip common false positive patterns skip_patterns = [ r'/wp-json/wp/v2/', r'/job/\d+/', r'\.js$', r'\.css$', r'/404\?', r'/gtm\.js', r'/gtm\.start', r'noticeError/called', r'\.(jpg|jpeg|png|gif|svg|ico|webp)(\?.*)?$', r'\.(ttf|eot|woff|woff2|otf)(\?.*)?$', r'\.(css|scss|less)(\?.*)?$', r'\.pdf(\?.*)?$', r'/images?/', r'/fonts?/', r'/static/', r'/assets/', r'/css/', r'\.min\.(js|css)(\?.*)?$', r'/demandware\.static/', r'\.gif(\?.*)?$', ] # High priority patterns (test these first) priority_patterns = [ r'/api/(?!.*/(gtm|noticeError))', r'/v\d+/(?!.*wp)', r'developers\.', r'/apispecs/', r'\.json$', r'/graphql', r'/rest/(?!.*wp)', r'/jwks\.json', ] # Regular API patterns regular_patterns = [ r'/ajax/', r'/services/', r'/internal/', r'/private/', r'/admin/api/', r'/configuration/', r'/manifest\.json$', ] # First, collect high priority endpoints priority_urls = [] regular_urls = [] for url in urls: # Skip known false positive patterns if any(re.search(pattern, url, re.IGNORECASE) for pattern in skip_patterns): continue # Additional filter: Skip if it looks like a static asset with parameters if re.search(r'\.(jpg|png|gif|svg|ttf|woff|css)\?sw=|sh=|v=', url, re.IGNORECASE): continue # Check for high priority patterns is_priority = any(re.search(pattern, url, re.IGNORECASE) for pattern in priority_patterns) is_regular = any(re.search(pattern, url, re.IGNORECASE) for pattern in regular_patterns) # Special handling for .well-known URLs if '.well-known/' in url: # Quick test to see if this returns HTML (indicating 404/error page) try: test_response = requests.get(url, timeout=5, verify=False) if (test_response.status_code == 200 and not (test_response.text.lower().startswith('<!doctype html>') or '<html' in test_response.text.lower()[:100])): is_priority = True # Only include if it returns actual content, not HTML else: continue # Skip - this is returning HTML error page or non-200 status except: continue # Skip if we can't test it # Special handling for JSON files - only include if they're actually config files elif url.endswith('.json'): # Include if it's in config or API directories (well-known already handled above) if any(keyword in url.lower() for keyword in ['config', 'api', 'manifest', 'jwks']): # But exclude if it's clearly in a static directory if not any(static_dir in url.lower() for static_dir in ['static', 'assets', 'images', 'css']): is_priority = True else: continue # Skip other JSON files if is_priority: priority_urls.append(url) elif is_regular: regular_urls.append(url) # Combine with priority first, test all relevant endpoints api_urls = priority_urls + regular_urls # Remove duplicates while preserving order seen = set() filtered_api_urls = [] for url in api_urls: if url not in seen: seen.add(url) filtered_api_urls.append(url) logger.info(f"Found {len(filtered_api_urls)} potential API endpoints (Priority: {len(priority_urls)}, Regular: {len(regular_urls)})") logger.info(f"Filtered out static assets and non-API endpoints") return filtered_api_urls def test_endpoint_bypass(self, url: str) -> Dict: """Test a single endpoint for multiple authentication bypass techniques""" results = [] try: # Test 1: Response Manipulation (original technique) response_manip = self.test_response_manipulation(url) if response_manip['vulnerable']: results.append(response_manip) # Test 2: Missing Authentication missing_auth = self.test_missing_authentication(url) if missing_auth['vulnerable']: results.append(missing_auth) # Test 3: IDOR (parameter manipulation) idor_test = self.test_idor_vulnerability(url) if idor_test['vulnerable']: results.append(idor_test) # Test 4: Weak Token Validation weak_token = self.test_weak_token_validation(url) if weak_token['vulnerable']: results.append(weak_token) # Test 5: Privilege Escalation priv_esc = self.test_privilege_escalation(url) if priv_esc['vulnerable']: results.append(priv_esc) # Test 6: HTTP Method Access Control method_bypass = self.test_http_method_bypass(url) if method_bypass['vulnerable']: results.append(method_bypass) # Test 7: Information Disclosure info_disclosure = self.test_information_disclosure(url) if info_disclosure['vulnerable']: results.append(info_disclosure) # Test 8: CORS Misconfiguration cors_test = self.test_cors_misconfiguration(url) if cors_test['vulnerable']: results.append(cors_test) # Test 9: Error Message Analysis error_analysis = self.test_error_message_disclosure(url) if error_analysis['vulnerable']: results.append(error_analysis) if results: return {'vulnerable': True, 'url': url, 'vulnerabilities': results} else: return {'vulnerable': False, 'url': url} except Exception as e: logger.debug(f"Error testing {url}: {e}") return {'vulnerable': False, 'url': url, 'error': str(e)} def test_response_manipulation(self, url: str) -> Dict: """Test 1: Response Manipulation Bypass""" try: response = self.session.get(url, timeout=self.timeout, allow_redirects=False) # Check if it's a JSON API endpoint if 'application/json' in response.headers.get('content-type', ''): return self.test_json_manipulation(url, response) # Check for redirect to SSO/login if response.status_code in [301, 302, 303, 307, 308]: location = response.headers.get('location', '') if any(keyword in location.lower() for keyword in ['login', 'auth', 'sso']): return { 'vulnerable': True, 'method': 'Response Manipulation - Redirect Bypass', 'details': f'Redirects to: {location}', 'exploitation': 'Intercept response and remove Location header' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_missing_authentication(self, url: str) -> Dict: """Test 2: Missing Authentication on Sensitive Endpoint""" try: # Skip common false positives if any(pattern in url.lower() for pattern in [ 'wp-json/wp/v2/tags', 'wp-json/wp/v2/pages', 'wp-json/wp/v2/comments', 'wp-json/wp/v2/users', 'wp-json/wp/v2/media', '/job/', '/events/' ]): return {'vulnerable': False} # Test completely unauthenticated request clean_session = requests.Session() clean_session.verify = False response = clean_session.get(url, timeout=self.timeout) if response.status_code == 200: content = response.text.lower() content_length = len(response.text) # Check if this is actually an HTML error page or redirect page if content.startswith('<!doctype html>') or '<html' in content[:100]: # This is an HTML page, not a configuration file return {'vulnerable': False} # High-value sensitive patterns critical_patterns = [ 'api_key', 'secret_key', 'access_token', 'private_key', 'password', 'connection_string', 'database_url', 'jwt', 'bearer', 'oauth' ] # API specification patterns spec_patterns = [ 'swagger', 'openapi', 'api-spec', 'endpoints', 'schemas' ] # Configuration patterns config_patterns = [ 'config', 'settings', 'environment', 'credentials' ] has_critical_data = any(pattern in content for pattern in critical_patterns) has_spec_data = any(pattern in content for pattern in spec_patterns) has_config_data = any(pattern in content for pattern in config_patterns) # Check for structured sensitive data (must be actual JSON/config, not HTML) is_json_config = (url.endswith('.json') and content_length > 100 and content.strip().startswith('{') and content.strip().endswith('}')) is_api_spec = ('spec' in url or 'swagger' in url or 'openapi' in url) # Only flag truly sensitive exposures if (content_length > 200 and (has_critical_data or (is_api_spec and has_spec_data) or (is_json_config and has_config_data))): return { 'vulnerable': True, 'method': 'Missing Authentication - Sensitive Data Exposure', 'details': f'Critical endpoint accessible without authentication (Status: {response.status_code}, {content_length} chars)', 'exploitation': 'Direct access to sensitive configuration/API data', 'poc': f'curl -X GET "{url}"', 'response_sample': response.text[:300] + '...' if len(response.text) > 300 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_idor_vulnerability(self, url: str) -> Dict: """Test 3: Insecure Direct Object Reference (IDOR)""" try: # Look for numeric IDs in URL id_patterns = [r'/(\d+)/?$', r'/(\d+)/', r'id=(\d+)', r'user=(\d+)'] for pattern in id_patterns: match = re.search(pattern, url) if match: original_id = match.group(1) # Try different IDs test_ids = [str(int(original_id) + 1), str(int(original_id) - 1), '1', '999', '0'] for test_id in test_ids: test_url = re.sub(pattern, f'/{test_id}/', url) if '/' in pattern else url.replace(f'={original_id}', f'={test_id}') response = self.session.get(test_url, timeout=self.timeout) if response.status_code == 200 and len(response.text) > 100: return { 'vulnerable': True, 'method': 'IDOR (Insecure Direct Object Reference)', 'details': f'ID {original_id} → {test_id} accessible', 'exploitation': f'Change ID parameter to access other users data' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_weak_token_validation(self, url: str) -> Dict: """Test 4: Weak Session Token or Cookie Validation""" try: # Skip static assets - they don't validate tokens by design static_extensions = r'\.(jpg|jpeg|png|gif|svg|ico|webp|ttf|eot|woff|woff2|css|js)(\?.*)?$' if re.search(static_extensions, url, re.IGNORECASE): return {'vulnerable': False} # First test normal request to establish baseline normal_response = self.session.get(url, timeout=self.timeout) # Skip if baseline response is HTML error page if normal_response.text.lower().startswith('<!doctype html>') or '<html' in normal_response.text.lower()[:100]: return {'vulnerable': False} # Test with invalid/tampered tokens invalid_tokens = [ 'Bearer invalid_token', 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.invalid', 'Bearer admin_token', 'Bearer 123456' ] for token in invalid_tokens: headers = {'Authorization': token} response = self.session.get(url, headers=headers, timeout=self.timeout) # Skip if response is HTML error page if response.text.lower().startswith('<!doctype html>') or '<html' in response.text.lower()[:100]: continue # Only flag as vulnerable if it returns 200 AND different/substantial content if response.status_code == 200 and len(response.text) > 50: # Check if the response is different from normal request (indicating it processed the token) if response.text != normal_response.text or 'error' not in response.text.lower(): # Additional check: make sure this isn't just a static file content_type = response.headers.get('content-type', '').lower() if not any(static_type in content_type for static_type in ['image/', 'font/', 'text/css', 'text/html']): return { 'vulnerable': True, 'method': 'Weak Token Validation', 'details': f'Accepts invalid token: "{token[:30]}..." (Status: {response.status_code})', 'exploitation': 'Server accepts invalid or missing tokens', 'poc': f'curl -X GET "{url}" -H "Authorization: {token}"', 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_privilege_escalation(self, url: str) -> Dict: """Test 5: Privilege Escalation via Parameter Tampering""" try: # First get baseline response baseline_response = self.session.get(url, timeout=self.timeout) baseline_content = baseline_response.text.lower() # Test with privilege escalation parameters priv_params = [ {'role': 'admin'}, {'isAdmin': 'true'}, {'user_id': '1'}, {'admin': '1'}, {'privilege': 'admin'}, {'level': '999'} ] for params in priv_params: response = self.session.get(url, params=params, timeout=self.timeout) if response.status_code == 200: content = response.text.lower() # Check if response suggests elevated privileges or is significantly different admin_indicators = ['admin', 'administrator', 'root', 'superuser', 'privilege'] has_admin_content = any(indicator in content for indicator in admin_indicators) content_changed = len(content) != len(baseline_content) or content != baseline_content if has_admin_content and content_changed: return { 'vulnerable': True, 'method': 'Privilege Escalation via Parameter Tampering', 'details': f'Parameter manipulation successful: {params} (Status: {response.status_code})', 'exploitation': 'Add privilege parameters to requests', 'poc': f'curl -X GET "{url}?" + "&".join([f"{k}={v}" for k,v in params.items()])', 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_http_method_bypass(self, url: str) -> Dict: """Test 6: Broken Access Control on HTTP Methods""" try: # Skip static assets - DELETE on images/fonts/CSS isn't a vulnerability static_extensions = r'\.(jpg|jpeg|png|gif|svg|ico|webp|ttf|eot|woff|woff2|css|js)(\?.*)?$' if re.search(static_extensions, url, re.IGNORECASE): return {'vulnerable': False} # Skip static asset directories if any(static_dir in url.lower() for static_dir in ['/images/', '/fonts/', '/css/', '/static/', '/assets/']): return {'vulnerable': False} # Get baseline with GET request get_response = self.session.get(url, timeout=self.timeout) get_status = get_response.status_code get_content_length = len(get_response.text) methods = ['POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'] for method in methods: response = self.session.request(method, url, timeout=self.timeout) # Only flag as vulnerable if method returns success AND doesn't contain error messages if response.status_code in [200, 201, 202, 204]: content = response.text.lower() content_length = len(response.text) # Check for common error indicators that suggest the request was rejected error_indicators = [ 'request rejected', 'access denied', 'method not allowed', 'forbidden', 'unauthorized', 'error', 'blocked', 'rejected' ] has_errors = any(indicator in content for indicator in error_indicators) # Additional check: make sure response isn't just a static file content_type = response.headers.get('content-type', '').lower() is_static_content = any(static_type in content_type for static_type in ['image/', 'font/', 'text/css']) # Consider it vulnerable if: # 1. No error messages in response, AND # 2. Not static content, AND # 3. Method should not be allowed (DELETE, PUT, PATCH), OR # 4. Substantially different content that suggests actual processing if (not has_errors and not is_static_content and (method in ['DELETE', 'PUT', 'PATCH'] or (abs(content_length - get_content_length) > 100 and content_length > 50))): return { 'vulnerable': True, 'method': f'HTTP Method Bypass - {method}', 'details': f'{method} method accessible (Status: {response.status_code}, Content: {content_length} chars vs GET: {get_content_length} chars)', 'exploitation': f'Use {method} method to bypass access controls', 'poc': f'curl -X {method} "{url}"', 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_information_disclosure(self, url: str) -> Dict: """Test 7: Exposed API Key or Token in Unauthenticated Requests""" try: response = self.session.get(url, timeout=self.timeout) # Look for exposed secrets secret_patterns = [ r'api[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9]{20,})', r'secret[_-]?key["\']?\s*[:=]\s*["\']?([a-zA-Z0-9]{20,})', r'access[_-]?token["\']?\s*[:=]\s*["\']?([a-zA-Z0-9]{20,})', r'bearer["\']?\s*[:=]\s*["\']?([a-zA-Z0-9]{20,})', r'jwt["\']?\s*[:=]\s*["\']?(eyJ[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]+\.[a-zA-Z0-9\-_=]*)' ] for pattern in secret_patterns: matches = re.findall(pattern, response.text, re.IGNORECASE) if matches: return { 'vulnerable': True, 'method': 'Information Disclosure - Exposed Secrets', 'details': f'Found {len(matches)} potential secrets in response', 'exploitation': 'Extract exposed API keys/tokens from response', 'poc': f'curl -X GET "{url}"', 'secrets_found': matches[:3], 'response_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_cors_misconfiguration(self, url: str) -> Dict: """Test 8: CORS Misconfiguration""" try: # Skip 404 pages and other error pages if '404?' in url or 'ItemNotFound' in url: return {'vulnerable': False} headers = {'Origin': 'https://evil.com'} response = self.session.get(url, headers=headers, timeout=self.timeout) # Only check successful responses, not error pages if response.status_code != 200: return {'vulnerable': False} cors_header = response.headers.get('Access-Control-Allow-Origin', '') # Check for overly permissive CORS if cors_header == '*' or 'evil.com' in cors_header: # Make sure this isn't just an error page content = response.text.lower() if not any(error in content for error in ['error', 'not found', 'rejected']): return { 'vulnerable': True, 'method': 'CORS Misconfiguration', 'details': f'CORS allows unauthorized origin: {cors_header}', 'exploitation': 'Cross-origin requests from malicious sites possible', 'poc': f'curl -X GET "{url}" -H "Origin: https://evil.com"', 'cors_header': cors_header } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_error_message_disclosure(self, url: str) -> Dict: """Test 9: Information Disclosure in Error Messages""" try: # Trigger errors with malformed requests error_triggers = [ {'params': {'id': 'invalid'}}, {'headers': {'Content-Type': 'application/xml'}}, {'data': 'invalid json'}, {'params': {'q': "' OR 1=1 --"}} ] for trigger in error_triggers: response = self.session.post(url, timeout=self.timeout, **trigger) # Look for information disclosure in errors disclosure_patterns = [ r'stack trace', r'sql.*error', r'database.*error', r'/var/www/', r'/home/', r'c:\\', r'exception', r'traceback' ] content = response.text.lower() for pattern in disclosure_patterns: if re.search(pattern, content): return { 'vulnerable': True, 'method': 'Information Disclosure in Error Messages', 'details': f'Error message contains sensitive information', 'exploitation': 'Craft malformed requests to extract system info', 'poc': f'curl -X POST "{url}" -d "invalid json" -H "Content-Type: application/json"', 'error_sample': response.text[:200] + '...' if len(response.text) > 200 else response.text } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def test_json_manipulation(self, url: str, original_response: requests.Response) -> Dict: """Test JSON response manipulation bypass""" try: if original_response.status_code != 200: return {'vulnerable': False} try: data = original_response.json() except: return {'vulnerable': False} # Look for any access control indicators in response bypass_found = False potential_bypasses = [] for key, value in self.flatten_dict(data).items(): if any(indicator in key.lower() for indicator in self.bypass_indicators): if value is False or str(value).lower() in ['false', '0', 'no', 'disabled', 'expired']: bypass_found = True potential_bypasses.append({ 'field': key, 'current_value': value, 'suggested_value': self.get_bypass_value(key, value) }) if bypass_found: return { 'vulnerable': True, 'method': 'JSON Response Manipulation', 'details': f'Found {len(potential_bypasses)} bypass opportunities', 'bypass_opportunities': potential_bypasses, 'exploitation': 'Intercept response and modify boolean values', 'poc': f'curl -X GET "{url}" # Then use Burp Suite to intercept and modify response', 'response_sample': json.dumps(data, indent=2)[:300] + '...' if len(json.dumps(data)) > 300 else json.dumps(data, indent=2) } # Also check for common API error patterns that might indicate auth issues if self.check_for_auth_errors(data): return { 'vulnerable': True, 'method': 'API Error Response Manipulation', 'details': 'API returns auth errors - try manipulating error responses', 'exploitation': 'Modify error responses to bypass checks' } return {'vulnerable': False} except Exception as e: return {'vulnerable': False, 'error': str(e)} def get_bypass_value(self, key: str, current_value) -> str: """Suggest appropriate bypass value based on the field name and current value""" key_lower = key.lower() if any(word in key_lower for word in ['expired', 'disabled', 'restricted']): return 'false' if current_value else 'true' elif 'level' in key_lower or 'role' in key_lower: return 'admin' if isinstance(current_value, str) else '999' else: return 'true' if current_value is False else 'false' def check_for_auth_errors(self, data: dict) -> bool: """Check if response contains authentication/authorization errors""" error_indicators = [ 'unauthorized', 'forbidden', 'access_denied', 'insufficient_privileges', 'login_required', 'authentication_required', 'invalid_token', 'permission_denied', 'not_authorized', 'access_forbidden' ] # Convert entire response to lowercase string for searching response_str = json.dumps(data).lower() return any(indicator in response_str for indicator in error_indicators) def flatten_dict(self, d: dict, parent_key: str = '', sep: str = '.') -> dict: """Flatten nested dictionary for easier searching""" items = [] for k, v in d.items(): new_key = f"{parent_key}{sep}{k}" if parent_key else k if isinstance(v, dict): items.extend(self.flatten_dict(v, new_key, sep=sep).items()) else: items.append((new_key, v)) return dict(items) def test_all_endpoints(self, urls: List[str]) -> List[Dict]: """Test all API endpoints concurrently for multiple bypass techniques""" logger.info(f"Testing {len(urls)} endpoints for authentication bypasses using 9 different techniques") results = [] with ThreadPoolExecutor(max_workers=self.threads) as executor: future_to_url = {executor.submit(self.test_endpoint_bypass, url): url for url in urls} for future in as_completed(future_to_url): result = future.result() if result['vulnerable']: # Handle multiple vulnerabilities per endpoint if 'vulnerabilities' in result: for vuln in result['vulnerabilities']: vuln_result = { 'url': result['url'], 'vulnerable': True, **vuln } self.vulnerable_endpoints.append(vuln_result) logger.info(f"🚨 VULNERABLE: {result['url']} - {vuln.get('method', 'Unknown')}") else: self.vulnerable_endpoints.append(result) logger.info(f"🚨 VULNERABLE: {result['url']} - {result.get('method', 'Unknown')}") results.append(result) return results def generate_report(self, results: List[Dict]) -> str: """Generate a detailed report of findings""" total_tested = len(results) vulnerable = len(self.vulnerable_endpoints) report = f""" === Comprehensive API Security Test Results === Domain: {self.domain} Total URLs discovered: {len(self.found_urls)} API endpoints tested: {total_tested} Vulnerable endpoints found: {vulnerable} TESTS PERFORMED: 1. Response Manipulation (Boolean/Redirect Bypass) 2. Missing Authentication on Sensitive Endpoints 3. Insecure Direct Object Reference (IDOR) 4. Weak Session Token/Cookie Validation 5. Privilege Escalation via Parameter Tampering 6. HTTP Method Access Control Bypass 7. Information Disclosure (Exposed Secrets) 8. CORS Misconfiguration 9. Error Message Information Disclosure """ if vulnerable > 0: report += "=== VULNERABLE API ENDPOINTS ===\n" for vuln in self.vulnerable_endpoints: report += f""" URL: {vuln['url']} Vulnerability: {vuln.get('method', 'Unknown')} Details: {vuln.get('details', 'No additional details')} Exploitation: {vuln.get('exploitation', 'Manual testing required')} 🔍 PROOF OF CONCEPT (POC): {vuln.get('poc', 'Manual verification required')} """ if 'response_sample' in vuln: report += f""" 📄 Response Sample: {vuln['response_sample']} """ if 'bypass_opportunities' in vuln: report += "\n🎯 Specific Bypass Opportunities:\n" for bypass in vuln['bypass_opportunities']: report += f" - Change '{bypass['field']}' from '{bypass['current_value']}' to '{bypass['suggested_value']}'\n" if 'secrets_found' in vuln: report += f"\n🔑 Secrets Found: {', '.join(vuln['secrets_found'])}\n" if 'cors_header' in vuln: report += f"\n🌐 CORS Header: {vuln['cors_header']}\n" if 'error_sample' in vuln: report += f"\n⚠️ Error Sample: {vuln['error_sample']}\n" report += "\n" + "="*60 + "\n" report += f""" === SUMMARY BY VULNERABILITY TYPE === """ # Count vulnerabilities by type vuln_types = {} for vuln in self.vulnerable_endpoints: vuln_type = vuln.get('method', 'Unknown') vuln_types[vuln_type] = vuln_types.get(vuln_type, 0) + 1 for vuln_type, count in sorted(vuln_types.items()): report += f"{vuln_type}: {count} endpoints\n" return report def run_full_scan(self) -> str: """Run the complete authentication bypass scan""" logger.info(f"Starting comprehensive API security scan for {self.domain}") # Step 1: Discover URLs all_urls = self.run_urlfinder() if not all_urls: return "No URLs discovered. Check URLfinder installation and domain validity." self.found_urls = set(all_urls) # Step 2: Filter API endpoints api_urls = self.filter_api_endpoints(all_urls) if not api_urls: return "No API endpoints found in discovered URLs." # Step 3: Test for bypasses results = self.test_all_endpoints(api_urls) # Step 4: Generate report return self.generate_report(results) def main(): parser = argparse.ArgumentParser(description='Automated Authentication Bypass Tester') parser.add_argument('domain', help='Target domain to test') parser.add_argument('-t', '--threads', type=int, default=10, help='Number of threads (default: 10)') parser.add_argument('-o', '--output', help='Output file for results') parser.add_argument('--timeout', type=int, default=10, help='Request timeout in seconds (default: 10)') args = parser.parse_args() # Validate domain if not args.domain or '.' not in args.domain: logger.error("Please provide a valid domain") return # Warning message print("⚠️ WARNING: Only use this tool on domains you own or have explicit permission to test!") print("Unauthorized testing may be illegal and could result in serious consequences.") # Run the scan tester = AuthBypassTester(args.domain, threads=args.threads, timeout=args.timeout) report = tester.run_full_scan() # Output results print(report) if args.output: with open(args.output, 'w') as f: f.write(report) logger.info(f"Results saved to {args.output}") if __name__ == "__main__": main()