Autarch/modules/api_fuzzer.py
DigiJ 2322f69516 v2.2.0 — Full arsenal expansion: 16 new security modules
Add WiFi Audit, API Fuzzer, Cloud Scanner, Threat Intel, Log Correlator,
Steganography, Anti-Forensics, BLE Scanner, Forensics, RFID/NFC, Malware
Sandbox, Password Toolkit, Web Scanner, Report Engine, Net Mapper, and
C2 Framework. Each module includes CLI interface, Flask routes, and web
UI template. Also includes Go DNS server source + binary, IP Capture
service, SYN Flood, Gone Fishing mail server, and hack hijack modules
from v2.0 work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 05:20:39 -08:00

743 lines
30 KiB
Python

"""AUTARCH API Fuzzer
Endpoint discovery, parameter fuzzing, auth testing, rate limit detection,
GraphQL introspection, and response analysis for REST/GraphQL APIs.
"""
DESCRIPTION = "API endpoint fuzzing & vulnerability testing"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"
import os
import re
import json
import time
import copy
import threading
from pathlib import Path
from urllib.parse import urljoin, urlparse, parse_qs
from typing import Dict, List, Optional, Any, Tuple
try:
from core.paths import get_data_dir
except ImportError:
def get_data_dir():
return str(Path(__file__).parent.parent / 'data')
try:
import requests
from requests.exceptions import RequestException
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
# ── Fuzz Payloads ────────────────────────────────────────────────────────────
SQLI_PAYLOADS = [
"' OR '1'='1", "\" OR \"1\"=\"1", "'; DROP TABLE--", "1; SELECT 1--",
"' UNION SELECT NULL--", "1' AND '1'='1", "admin'--", "' OR 1=1#",
"1 AND 1=1", "1' ORDER BY 1--", "') OR ('1'='1",
]
XSS_PAYLOADS = [
"<script>alert(1)</script>", "'\"><img src=x onerror=alert(1)>",
"javascript:alert(1)", "<svg/onload=alert(1)>", "{{7*7}}",
"${7*7}", "<%=7*7%>", "{{constructor.constructor('return 1')()}}",
]
TYPE_CONFUSION = [
None, True, False, 0, -1, 2147483647, -2147483648,
99999999999999, 0.1, -0.1, float('inf'),
"", " ", "null", "undefined", "NaN", "true", "false",
[], {}, [None], {"__proto__": {}},
"A" * 1000, "A" * 10000,
]
TRAVERSAL_PAYLOADS = [
"../../../etc/passwd", "..\\..\\..\\windows\\system32\\config\\sam",
"....//....//....//etc/passwd", "%2e%2e%2f%2e%2e%2f",
"/etc/passwd%00", "..%252f..%252f",
]
COMMON_ENDPOINTS = [
'/api', '/api/v1', '/api/v2', '/api/v3',
'/api/users', '/api/admin', '/api/login', '/api/auth',
'/api/config', '/api/settings', '/api/debug', '/api/health',
'/api/status', '/api/info', '/api/version', '/api/docs',
'/api/swagger', '/api/graphql', '/api/internal',
'/swagger.json', '/swagger-ui', '/openapi.json',
'/api/tokens', '/api/keys', '/api/secrets',
'/api/upload', '/api/download', '/api/export', '/api/import',
'/api/search', '/api/query', '/api/execute', '/api/run',
'/graphql', '/graphiql', '/playground',
'/.well-known/openid-configuration',
'/api/password/reset', '/api/register', '/api/verify',
'/api/webhook', '/api/callback', '/api/notify',
'/actuator', '/actuator/health', '/actuator/env',
'/metrics', '/prometheus', '/_debug', '/__debug__',
]
# ── API Fuzzer Engine ────────────────────────────────────────────────────────
class APIFuzzer:
"""REST & GraphQL API security testing."""
def __init__(self):
self.data_dir = os.path.join(get_data_dir(), 'api_fuzzer')
os.makedirs(self.data_dir, exist_ok=True)
self.session = requests.Session() if HAS_REQUESTS else None
self.results: List[Dict] = []
self._jobs: Dict[str, Dict] = {}
def set_auth(self, auth_type: str, value: str, header_name: str = 'Authorization'):
"""Configure authentication for requests."""
if not self.session:
return
if auth_type == 'bearer':
self.session.headers[header_name] = f'Bearer {value}'
elif auth_type == 'api_key':
self.session.headers[header_name] = value
elif auth_type == 'basic':
parts = value.split(':', 1)
if len(parts) == 2:
self.session.auth = (parts[0], parts[1])
elif auth_type == 'cookie':
self.session.cookies.set('session', value)
elif auth_type == 'custom':
self.session.headers[header_name] = value
def clear_auth(self):
"""Clear authentication."""
if self.session:
self.session.headers.pop('Authorization', None)
self.session.auth = None
self.session.cookies.clear()
# ── Endpoint Discovery ───────────────────────────────────────────────
def discover_endpoints(self, base_url: str, custom_paths: List[str] = None,
threads: int = 10) -> str:
"""Discover API endpoints. Returns job_id."""
job_id = f'discover_{int(time.time())}'
self._jobs[job_id] = {
'type': 'discover', 'status': 'running',
'found': [], 'checked': 0, 'total': 0
}
def _discover():
paths = COMMON_ENDPOINTS + (custom_paths or [])
self._jobs[job_id]['total'] = len(paths)
found = []
def check_path(path):
try:
url = urljoin(base_url.rstrip('/') + '/', path.lstrip('/'))
resp = self.session.get(url, timeout=5, allow_redirects=False)
self._jobs[job_id]['checked'] += 1
if resp.status_code < 404:
entry = {
'path': path,
'url': url,
'status': resp.status_code,
'content_type': resp.headers.get('content-type', ''),
'size': len(resp.content),
'methods': []
}
# Check allowed methods via OPTIONS
try:
opts = self.session.options(url, timeout=3)
allow = opts.headers.get('Allow', '')
if allow:
entry['methods'] = [m.strip() for m in allow.split(',')]
except Exception:
pass
found.append(entry)
except Exception:
self._jobs[job_id]['checked'] += 1
# Thread pool
active_threads = []
for path in paths:
t = threading.Thread(target=check_path, args=(path,))
t.start()
active_threads.append(t)
if len(active_threads) >= threads:
for at in active_threads:
at.join(timeout=10)
active_threads.clear()
for t in active_threads:
t.join(timeout=10)
self._jobs[job_id]['found'] = found
self._jobs[job_id]['status'] = 'complete'
threading.Thread(target=_discover, daemon=True).start()
return job_id
def parse_openapi(self, url_or_path: str) -> Dict:
"""Parse OpenAPI/Swagger spec to extract endpoints."""
try:
if url_or_path.startswith('http'):
resp = self.session.get(url_or_path, timeout=10)
spec = resp.json()
else:
with open(url_or_path) as f:
spec = json.load(f)
endpoints = []
paths = spec.get('paths', {})
for path, methods in paths.items():
for method, details in methods.items():
if method.upper() in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'):
params = []
for p in details.get('parameters', []):
params.append({
'name': p.get('name'),
'in': p.get('in'),
'required': p.get('required', False),
'type': p.get('schema', {}).get('type', 'string')
})
endpoints.append({
'path': path,
'method': method.upper(),
'summary': details.get('summary', ''),
'parameters': params,
'tags': details.get('tags', [])
})
return {
'ok': True,
'title': spec.get('info', {}).get('title', ''),
'version': spec.get('info', {}).get('version', ''),
'endpoints': endpoints,
'count': len(endpoints)
}
except Exception as e:
return {'ok': False, 'error': str(e)}
# ── Parameter Fuzzing ────────────────────────────────────────────────
def fuzz_params(self, url: str, method: str = 'GET',
params: Dict = None, payload_type: str = 'type_confusion') -> Dict:
"""Fuzz API parameters with various payloads."""
if not self.session:
return {'ok': False, 'error': 'requests not available'}
if payload_type == 'sqli':
payloads = SQLI_PAYLOADS
elif payload_type == 'xss':
payloads = XSS_PAYLOADS
elif payload_type == 'traversal':
payloads = TRAVERSAL_PAYLOADS
else:
payloads = TYPE_CONFUSION
params = params or {}
findings = []
for param_name, original_value in params.items():
for payload in payloads:
fuzzed = copy.deepcopy(params)
fuzzed[param_name] = payload
try:
if method.upper() == 'GET':
resp = self.session.get(url, params=fuzzed, timeout=10)
else:
resp = self.session.request(method.upper(), url, json=fuzzed, timeout=10)
# Analyze response for anomalies
finding = self._analyze_fuzz_response(
resp, param_name, payload, payload_type
)
if finding:
findings.append(finding)
except RequestException as e:
if 'timeout' not in str(e).lower():
findings.append({
'param': param_name,
'payload': str(payload),
'type': 'error',
'detail': str(e)
})
return {'ok': True, 'findings': findings, 'tested': len(params) * len(payloads)}
def _analyze_fuzz_response(self, resp, param: str, payload, payload_type: str) -> Optional[Dict]:
"""Analyze response for vulnerability indicators."""
body = resp.text.lower()
finding = None
# SQL error detection
sql_errors = [
'sql syntax', 'mysql_fetch', 'pg_query', 'sqlite3',
'unclosed quotation', 'unterminated string', 'syntax error',
'odbc', 'oracle error', 'microsoft ole db', 'ora-0'
]
if payload_type == 'sqli' and any(e in body for e in sql_errors):
finding = {
'param': param, 'payload': str(payload),
'type': 'sqli', 'severity': 'high',
'detail': 'SQL error in response',
'status': resp.status_code
}
# XSS reflection
if payload_type == 'xss' and str(payload).lower() in body:
finding = {
'param': param, 'payload': str(payload),
'type': 'xss_reflected', 'severity': 'high',
'detail': 'Payload reflected in response',
'status': resp.status_code
}
# Path traversal
if payload_type == 'traversal':
traversal_indicators = ['root:', '/bin/', 'windows\\system32', '[boot loader]']
if any(t in body for t in traversal_indicators):
finding = {
'param': param, 'payload': str(payload),
'type': 'path_traversal', 'severity': 'critical',
'detail': 'File content in response',
'status': resp.status_code
}
# Server error (500) might indicate injection
if resp.status_code == 500 and not finding:
finding = {
'param': param, 'payload': str(payload),
'type': 'server_error', 'severity': 'medium',
'detail': f'Server error (500) triggered',
'status': resp.status_code
}
# Stack trace / debug info disclosure
debug_indicators = [
'traceback', 'stacktrace', 'exception', 'debug',
'at line', 'file "/', 'internal server error'
]
if any(d in body for d in debug_indicators) and not finding:
finding = {
'param': param, 'payload': str(payload),
'type': 'info_disclosure', 'severity': 'medium',
'detail': 'Debug/stack trace in response',
'status': resp.status_code
}
return finding
# ── Auth Testing ─────────────────────────────────────────────────────
def test_idor(self, url_template: str, id_range: Tuple[int, int],
auth_token: str = None) -> Dict:
"""Test for IDOR by iterating IDs."""
findings = []
start_id, end_id = id_range
if auth_token:
self.session.headers['Authorization'] = f'Bearer {auth_token}'
for i in range(start_id, end_id + 1):
url = url_template.replace('{id}', str(i))
try:
resp = self.session.get(url, timeout=5)
if resp.status_code == 200:
findings.append({
'id': i, 'url': url,
'status': resp.status_code,
'size': len(resp.content),
'accessible': True
})
elif resp.status_code not in (401, 403, 404):
findings.append({
'id': i, 'url': url,
'status': resp.status_code,
'accessible': False,
'note': f'Unexpected status: {resp.status_code}'
})
except Exception:
pass
return {
'ok': True, 'findings': findings,
'accessible_count': sum(1 for f in findings if f.get('accessible')),
'tested': end_id - start_id + 1
}
def test_auth_bypass(self, url: str) -> Dict:
"""Test common auth bypass techniques."""
bypasses = []
tests = [
('No auth header', {}),
('Empty Bearer', {'Authorization': 'Bearer '}),
('Bearer null', {'Authorization': 'Bearer null'}),
('Bearer undefined', {'Authorization': 'Bearer undefined'}),
('Admin header', {'X-Admin': 'true'}),
('Internal header', {'X-Forwarded-For': '127.0.0.1'}),
('Override method', {'X-HTTP-Method-Override': 'GET'}),
('Original URL', {'X-Original-URL': '/admin'}),
]
for name, headers in tests:
try:
resp = requests.get(url, headers=headers, timeout=5)
if resp.status_code == 200:
bypasses.append({
'technique': name,
'status': resp.status_code,
'size': len(resp.content),
'success': True
})
else:
bypasses.append({
'technique': name,
'status': resp.status_code,
'success': False
})
except Exception:
pass
return {
'ok': True,
'bypasses': bypasses,
'successful': sum(1 for b in bypasses if b.get('success'))
}
# ── Rate Limiting ────────────────────────────────────────────────────
def test_rate_limit(self, url: str, requests_count: int = 50,
method: str = 'GET') -> Dict:
"""Test API rate limiting."""
results = []
start_time = time.time()
for i in range(requests_count):
try:
resp = self.session.request(method, url, timeout=10)
results.append({
'request_num': i + 1,
'status': resp.status_code,
'time': time.time() - start_time,
'rate_limit_remaining': resp.headers.get('X-RateLimit-Remaining', ''),
'retry_after': resp.headers.get('Retry-After', '')
})
if resp.status_code == 429:
break
except Exception as e:
results.append({
'request_num': i + 1,
'error': str(e),
'time': time.time() - start_time
})
rate_limited = any(r.get('status') == 429 for r in results)
elapsed = time.time() - start_time
return {
'ok': True,
'rate_limited': rate_limited,
'total_requests': len(results),
'elapsed_seconds': round(elapsed, 2),
'rps': round(len(results) / elapsed, 1) if elapsed > 0 else 0,
'limit_hit_at': next((r['request_num'] for r in results if r.get('status') == 429), None),
'results': results
}
# ── GraphQL ──────────────────────────────────────────────────────────
def graphql_introspect(self, url: str) -> Dict:
"""Run GraphQL introspection query."""
query = {
'query': '''
{
__schema {
types {
name
kind
fields {
name
type { name kind }
args { name type { name } }
}
}
queryType { name }
mutationType { name }
}
}
'''
}
try:
resp = self.session.post(url, json=query, timeout=15)
data = resp.json()
if 'errors' in data and not data.get('data'):
return {'ok': False, 'error': 'Introspection disabled or error',
'errors': data['errors']}
schema = data.get('data', {}).get('__schema', {})
types = []
for t in schema.get('types', []):
if not t['name'].startswith('__'):
types.append({
'name': t['name'],
'kind': t['kind'],
'fields': [
{'name': f['name'],
'type': f['type'].get('name', f['type'].get('kind', '')),
'args': [a['name'] for a in f.get('args', [])]}
for f in (t.get('fields') or [])
]
})
return {
'ok': True,
'query_type': schema.get('queryType', {}).get('name'),
'mutation_type': schema.get('mutationType', {}).get('name'),
'types': types,
'type_count': len(types)
}
except Exception as e:
return {'ok': False, 'error': str(e)}
def graphql_depth_test(self, url: str, max_depth: int = 10) -> Dict:
"""Test GraphQL query depth limits."""
results = []
for depth in range(1, max_depth + 1):
# Build nested query
inner = '{ __typename }'
for _ in range(depth):
inner = f'{{ __schema {{ types {inner} }} }}'
try:
resp = self.session.post(url, json={'query': inner}, timeout=10)
results.append({
'depth': depth,
'status': resp.status_code,
'has_errors': 'errors' in resp.json() if resp.headers.get('content-type', '').startswith('application/json') else None
})
if resp.status_code != 200:
break
except Exception:
results.append({'depth': depth, 'error': True})
break
max_allowed = max((r['depth'] for r in results if r.get('status') == 200), default=0)
return {
'ok': True,
'max_depth_allowed': max_allowed,
'depth_limited': max_allowed < max_depth,
'results': results
}
# ── Response Analysis ────────────────────────────────────────────────
def analyze_response(self, url: str, method: str = 'GET') -> Dict:
"""Analyze API response for security issues."""
try:
resp = self.session.request(method, url, timeout=10)
issues = []
# Check security headers
security_headers = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY|SAMEORIGIN',
'Strict-Transport-Security': None,
'Content-Security-Policy': None,
'X-XSS-Protection': None,
}
for header, expected in security_headers.items():
val = resp.headers.get(header)
if not val:
issues.append({
'type': 'missing_header',
'header': header,
'severity': 'low'
})
# Check for info disclosure
server = resp.headers.get('Server', '')
if server and any(v in server.lower() for v in ['apache/', 'nginx/', 'iis/']):
issues.append({
'type': 'server_disclosure',
'value': server,
'severity': 'info'
})
powered_by = resp.headers.get('X-Powered-By', '')
if powered_by:
issues.append({
'type': 'technology_disclosure',
'value': powered_by,
'severity': 'low'
})
# Check CORS
cors = resp.headers.get('Access-Control-Allow-Origin', '')
if cors == '*':
issues.append({
'type': 'open_cors',
'value': cors,
'severity': 'medium'
})
# Check for error/debug info in body
body = resp.text.lower()
if any(kw in body for kw in ['stack trace', 'traceback', 'debug mode']):
issues.append({
'type': 'debug_info',
'severity': 'medium',
'detail': 'Debug/stack trace information in response'
})
return {
'ok': True,
'url': url,
'status': resp.status_code,
'headers': dict(resp.headers),
'issues': issues,
'issue_count': len(issues)
}
except Exception as e:
return {'ok': False, 'error': str(e)}
# ── Job Management ───────────────────────────────────────────────────
def get_job(self, job_id: str) -> Optional[Dict]:
return self._jobs.get(job_id)
def list_jobs(self) -> List[Dict]:
return [{'id': k, **v} for k, v in self._jobs.items()]
# ── Singleton ────────────────────────────────────────────────────────────────
_instance = None
def get_api_fuzzer() -> APIFuzzer:
global _instance
if _instance is None:
_instance = APIFuzzer()
return _instance
# ── CLI Interface ────────────────────────────────────────────────────────────
def run():
"""CLI entry point for API Fuzzer module."""
if not HAS_REQUESTS:
print(" Error: requests library not installed")
return
fuzzer = get_api_fuzzer()
while True:
print(f"\n{'='*60}")
print(f" API Fuzzer")
print(f"{'='*60}")
print()
print(" 1 — Discover Endpoints")
print(" 2 — Parse OpenAPI Spec")
print(" 3 — Fuzz Parameters")
print(" 4 — Test Auth Bypass")
print(" 5 — Test IDOR")
print(" 6 — Test Rate Limiting")
print(" 7 — GraphQL Introspection")
print(" 8 — Analyze Response")
print(" 9 — Set Authentication")
print(" 0 — Back")
print()
choice = input(" > ").strip()
if choice == '0':
break
elif choice == '1':
base = input(" Base URL: ").strip()
if base:
job_id = fuzzer.discover_endpoints(base)
print(f" Discovery started (job: {job_id})")
while True:
job = fuzzer.get_job(job_id)
if job['status'] == 'complete':
print(f" Found {len(job['found'])} endpoints:")
for ep in job['found']:
print(f" [{ep['status']}] {ep['path']} "
f"({ep['content_type'][:30]})")
break
print(f" Checking... {job['checked']}/{job['total']}")
time.sleep(1)
elif choice == '2':
url = input(" OpenAPI spec URL or file: ").strip()
if url:
result = fuzzer.parse_openapi(url)
if result['ok']:
print(f" API: {result['title']} v{result['version']}")
print(f" Endpoints: {result['count']}")
for ep in result['endpoints'][:20]:
print(f" {ep['method']:<6} {ep['path']} {ep.get('summary', '')}")
else:
print(f" Error: {result['error']}")
elif choice == '3':
url = input(" Endpoint URL: ").strip()
param_str = input(" Parameters (key=val,key=val): ").strip()
ptype = input(" Payload type (sqli/xss/traversal/type_confusion): ").strip() or 'type_confusion'
if url and param_str:
params = dict(p.split('=', 1) for p in param_str.split(',') if '=' in p)
result = fuzzer.fuzz_params(url, params=params, payload_type=ptype)
if result['ok']:
print(f" Tested {result['tested']} combinations, {len(result['findings'])} findings:")
for f in result['findings']:
print(f" [{f.get('severity', '?')}] {f['type']}: {f['param']} = {f['payload'][:50]}")
elif choice == '4':
url = input(" Protected URL: ").strip()
if url:
result = fuzzer.test_auth_bypass(url)
print(f" Tested {len(result['bypasses'])} techniques, {result['successful']} successful")
for b in result['bypasses']:
status = 'BYPASSED' if b['success'] else f'blocked ({b["status"]})'
print(f" {b['technique']}: {status}")
elif choice == '6':
url = input(" URL to test: ").strip()
count = input(" Request count (default 50): ").strip()
if url:
result = fuzzer.test_rate_limit(url, int(count) if count.isdigit() else 50)
print(f" Rate limited: {result['rate_limited']}")
print(f" RPS: {result['rps']} | Total: {result['total_requests']} in {result['elapsed_seconds']}s")
if result['limit_hit_at']:
print(f" Limit hit at request #{result['limit_hit_at']}")
elif choice == '7':
url = input(" GraphQL URL: ").strip()
if url:
result = fuzzer.graphql_introspect(url)
if result['ok']:
print(f" Found {result['type_count']} types")
for t in result['types'][:10]:
print(f" {t['kind']}: {t['name']} ({len(t['fields'])} fields)")
else:
print(f" Error: {result['error']}")
elif choice == '8':
url = input(" URL: ").strip()
if url:
result = fuzzer.analyze_response(url)
if result['ok']:
print(f" Status: {result['status']} | Issues: {result['issue_count']}")
for issue in result['issues']:
print(f" [{issue['severity']}] {issue['type']}: {issue.get('value', issue.get('detail', ''))}")
elif choice == '9':
auth_type = input(" Auth type (bearer/api_key/basic/cookie): ").strip()
value = input(" Value: ").strip()
if auth_type and value:
fuzzer.set_auth(auth_type, value)
print(" Authentication configured")