├── version.txt ├── requirements.txt ├── .gitattributes ├── images └── ExternalAttacker-MCP-Banner.png ├── .github └── FUNDING.yml ├── templates ├── base.html ├── result.html └── index.html ├── README.md ├── ExternalAttacker-App.py └── ExternalAttacker-MCP.py /version.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==3.0.2 2 | werkzeug==3.0.1 3 | aiohttp==3.9.3 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /images/ExternalAttacker-MCP-Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MorDavid/ExternalAttacker-MCP/HEAD/images/ExternalAttacker-MCP-Banner.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mordavid 4 | patreon: mordavid 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: mordavid 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Security Tools Runner 7 | 8 | 9 | 10 | 11 | 16 | 17 |
18 | {% with messages = get_flashed_messages(with_categories=true) %} 19 | {% if messages %} 20 | {% for category, message in messages %} 21 | 25 | {% endfor %} 26 | {% endif %} 27 | {% endwith %} 28 | 29 | {% block content %}{% endblock %} 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /templates/result.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |
Results for {{ tool }}
9 | Run Another Tool 10 |
11 |
12 |
13 |
Command Details:
14 | {{ tool }} {{ args }} 15 |
16 | 17 | {% if result.error %} 18 |
19 |
Error:
20 |
{{ result.error }}
21 |
22 | {% else %} 23 | {% if result.stdout %} 24 |
25 |
Output:
26 |
{{ result.stdout }}
27 |
28 | {% endif %} 29 | 30 | {% if result.stderr %} 31 |
32 |
Errors/Warnings:
33 |
{{ result.stderr }}
34 |
35 | {% endif %} 36 | 37 |
38 |
Exit Code: {{ result.returncode }}
39 |
40 | {% endif %} 41 |
42 |
43 |
44 |
45 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExternalAttacker MCP Server 2 | 3 | ![ExternalAttacker-MCP](/images/ExternalAttacker-MCP-Banner.png) 4 | 5 | ## Model Context Protocol (MCP) Server for External Attack Surface Management 6 | 7 | ExternalAttacker is a powerful integration that brings automated scanning capabilities with natural language interface for comprehensive external attack surface management and reconnaissance. 8 | 9 | > 🔍 **Automated Attack Surface Management with AI!** 10 | > Scan domains, analyze infrastructure, and discover vulnerabilities using natural language. 11 | 12 | ## 🔍 What is ExternalAttacker? 13 | 14 | ExternalAttacker combines the power of: 15 | 16 | * **Automated Scanning**: Comprehensive toolset for external reconnaissance 17 | * **Model Context Protocol (MCP)**: An open protocol for creating custom AI tools 18 | * **Natural Language Processing**: Convert plain English queries into scanning commands 19 | 20 | ## 📱 Community 21 | 22 | Join our Telegram channel for updates, tips, and discussion: 23 | - **Telegram**: [https://t.me/root_sec](https://t.me/root_sec) 24 | 25 | ## ✨ Features 26 | 27 | * **Natural Language Interface**: Run scans using plain English 28 | * **Comprehensive Scanning Categories**: 29 | * 🌐 Subdomain Discovery (subfinder) 30 | * 🔢 Port Scanning (naabu) 31 | * 🌍 HTTP Analysis (httpx) 32 | * 🛡️ CDN Detection (cdncheck) 33 | * 🔐 TLS Analysis (tlsx) 34 | * 📁 Directory Fuzzing (ffuf, gobuster) 35 | * 📝 DNS Enumeration (dnsx) 36 | 37 | ## 📋 Prerequisites 38 | 39 | * Python 3.8 or higher 40 | * Go (for installing tools) 41 | * MCP Client 42 | 43 | ## 🔧 Installation 44 | 45 | 1. Clone this repository: 46 | ```bash 47 | git clone https://github.com/mordavid/ExternalAttacker-MCP.git 48 | cd ExternalAttacker 49 | ``` 50 | 51 | 2. Install Python dependencies: 52 | ```bash 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | 3. Install required Go tools: 57 | ```bash 58 | go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest 59 | go install -v github.com/projectdiscovery/naabu/v2/cmd/naabu@latest 60 | go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest 61 | go install -v github.com/projectdiscovery/cdncheck/cmd/cdncheck@latest 62 | go install -v github.com/projectdiscovery/tlsx/cmd/tlsx@latest 63 | go install -v github.com/ffuf/ffuf@latest 64 | go install github.com/OJ/gobuster/v3@latest 65 | go install -v github.com/projectdiscovery/dnsx/cmd/dnsx@latest 66 | ``` 67 | 68 | 4. Run ExternalAttacker-App.py 69 | ```bash 70 | python ExternalAttacker-App.py 71 | # Access http://localhost:6991 72 | ``` 73 | 74 | 5. Configure the MCP Server 75 | ```bash 76 | "mcpServers": { 77 | "ExternalAttacker-MCP": { 78 | "command": "python", 79 | "args": [ 80 | "\\ExternalAttacker-MCP.py" 81 | ] 82 | } 83 | } 84 | ``` 85 | 86 | ## 🚀 Usage 87 | 88 | Example queries you can ask through the MCP: 89 | 90 | * "Scan example.com for subdomains" 91 | * "Check open ports on 192.168.1.1" 92 | * "Analyze HTTP services on test.com" 93 | * "Check if domain.com uses a CDN" 94 | * "Analyze SSL configuration of site.com" 95 | * "Fuzz endpoints on target.com" 96 | 97 | ## 📜 License 98 | 99 | MIT License 100 | 101 | ## 🙏 Acknowledgments 102 | 103 | * The ProjectDiscovery team for their excellent security tools 104 | * The MCP community for advancing AI-powered tooling 105 | 106 | --- 107 | 108 | _Note: This is a security tool. Please use responsibly and only on systems you have permission to test._ -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |
Run Security Tool
9 |
10 |
11 |
12 |
13 | 14 | 20 |
21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 47 |
48 |
49 | 50 |
51 |
52 |
API Usage
53 |
54 |
55 |
curl -X POST http://localhost:6991/api/run \
 56 |     -H "Content-Type: application/json" \
 57 |     -d '{"tool": "subfinder", "args": "-d example.com"}'
58 |
59 |
60 |
61 |
62 | 63 | 106 | {% endblock %} -------------------------------------------------------------------------------- /ExternalAttacker-App.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, render_template, flash, redirect, url_for 2 | import subprocess 3 | import json 4 | import os 5 | import re 6 | from functools import wraps 7 | import secrets 8 | import requests 9 | import yaml 10 | from packaging import version 11 | import shutil 12 | import sys 13 | 14 | app = Flask(__name__) 15 | app.secret_key = os.environ.get('FLASK_SECRET_KEY', secrets.token_hex(32)) 16 | 17 | ALLOWED_TOOLS = [ 18 | "subfinder", 19 | "naabu", 20 | "httpx", 21 | "nuclei", 22 | "cdncheck", 23 | "tlsx", 24 | "ffuf", 25 | "gobuster", 26 | "dnsx" 27 | ] 28 | 29 | DANGEROUS_CHARS = r'[&|;`$(){}\\<>]' 30 | 31 | def validate_input(tool, args): 32 | if not tool or not args: 33 | return False 34 | if len(args) > 1000: # Reasonable limit 35 | return False 36 | if re.search(DANGEROUS_CHARS, args): 37 | return False 38 | return True 39 | 40 | def run_command(tool, args): 41 | try: 42 | """ 43 | if not validate_input(tool, args): 44 | return { 45 | 'stdout': '', 46 | 'stderr': 'Invalid input: contains dangerous characters', 47 | 'returncode': 1 48 | } 49 | """ 50 | startupinfo = None 51 | if os.name == 'nt': 52 | startupinfo = subprocess.STARTUPINFO() 53 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 54 | 55 | cmd = [tool] + args.split() 56 | process = subprocess.Popen( 57 | cmd, 58 | shell=False, # Security: Prevent shell injection 59 | stdout=subprocess.PIPE, 60 | stderr=subprocess.PIPE, 61 | startupinfo=startupinfo, 62 | encoding='utf-8', 63 | errors='replace' 64 | ) 65 | stdout, stderr = process.communicate(timeout=300) # 5 minute timeout 66 | 67 | return { 68 | 'stdout': stdout, 69 | 'stderr': stderr, 70 | 'returncode': process.returncode 71 | } 72 | except subprocess.TimeoutExpired: 73 | return {'error': 'Command timed out'} 74 | except Exception as e: 75 | return {'error': 'Internal error'} 76 | 77 | def require_api_key(f): 78 | @wraps(f) 79 | def decorated_function(*args, **kwargs): 80 | api_key = request.headers.get('X-API-Key') 81 | if not api_key or api_key != os.environ.get('API_KEY'): 82 | return jsonify({'error': 'Unauthorized'}), 401 83 | return f(*args, **kwargs) 84 | return decorated_function 85 | 86 | @app.after_request 87 | def add_security_headers(response): 88 | response.headers['X-Frame-Options'] = 'SAMEORIGIN' 89 | response.headers['X-Content-Type-Options'] = 'nosniff' 90 | response.headers['X-XSS-Protection'] = '1; mode=block' 91 | response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' 92 | return response 93 | 94 | @app.route('/') 95 | def index(): 96 | return render_template('index.html', tools=ALLOWED_TOOLS) 97 | 98 | @app.route('/run', methods=['POST']) 99 | # @require_api_key # Temporarily disabled for testing 100 | def web_run_tool(): 101 | tool = request.form.get('tool', '').lower() 102 | args = request.form.get('args', '') 103 | 104 | if not tool or not args: 105 | flash('Tool and arguments are required', 'danger') 106 | return redirect(url_for('index')) 107 | 108 | if tool not in ALLOWED_TOOLS: 109 | flash(f'Tool not allowed. Allowed tools: {", ".join(ALLOWED_TOOLS)}', 'danger') 110 | return redirect(url_for('index')) 111 | 112 | result = run_command(tool, args) 113 | return render_template('result.html', 114 | tool=tool, 115 | args=args, 116 | target='', 117 | result=result) 118 | 119 | @app.route('/api/run', methods=['POST']) 120 | # @require_api_key # Temporarily disabled for testing 121 | def api_run_tool(): 122 | try: 123 | data = request.get_json() 124 | if not data: 125 | return jsonify({'error': 'Invalid JSON'}), 400 126 | 127 | tool = data.get('tool', '').lower() 128 | args = data.get('args', '') 129 | 130 | if not tool: 131 | return jsonify({'error': 'Tool name is required'}), 400 132 | 133 | if tool not in ALLOWED_TOOLS: 134 | return jsonify({'error': f'Tool not allowed. Allowed tools: {", ".join(ALLOWED_TOOLS)}'}), 400 135 | 136 | return jsonify(run_command(tool, args)) 137 | except Exception as e: 138 | return jsonify({'error': 'Internal server error'}), 500 139 | 140 | @app.errorhandler(Exception) 141 | def handle_error(error): 142 | return jsonify({'error': 'Internal server error'}), 500 143 | 144 | def check_for_updates(): 145 | try: 146 | # Get current version from version.txt 147 | with open('version.txt', 'r', encoding='utf-8-sig') as f: 148 | current_version = f.read().strip() 149 | 150 | # Get remote version 151 | response = requests.get('https://mordavid.com/md_versions.yaml') 152 | if response.status_code != 200: 153 | print(f"Failed to check for updates: {response.status_code}") 154 | return 155 | 156 | remote_versions = yaml.safe_load(response.text) 157 | remote_version = None 158 | download_url = None 159 | 160 | for sw in remote_versions['softwares']: 161 | if sw['name'] == 'ExternalAttacker-MCP': 162 | remote_version = sw['version'] 163 | download_url = sw['download'] 164 | break 165 | 166 | if not remote_version or not download_url: 167 | print("Could not find remote version info") 168 | return 169 | 170 | # Compare versions 171 | if version.parse(remote_version) > version.parse(current_version): 172 | print(f"New version available: {remote_version}") 173 | 174 | # Update MCP file 175 | response = requests.get(download_url) 176 | if response.status_code == 200: 177 | # Backup current MCP file 178 | shutil.copy2('ExternalAttacker-MCP.py', 'ExternalAttacker-MCP.py.bak') 179 | 180 | # Write new MCP version 181 | with open('ExternalAttacker-MCP.py', 'wb') as f: 182 | f.write(response.content) 183 | print("Successfully updated MCP to new version") 184 | else: 185 | print(f"Failed to download MCP update: {response.status_code}") 186 | return 187 | 188 | # Update App file 189 | app_url = download_url.replace('ExternalAttacker-MCP.py', 'ExternalAttacker-App.py') 190 | response = requests.get(app_url) 191 | if response.status_code == 200: 192 | # Backup current App file 193 | shutil.copy2('ExternalAttacker-App.py', 'ExternalAttacker-App.py.bak') 194 | 195 | # Write new App version 196 | with open('ExternalAttacker-App.py', 'wb') as f: 197 | f.write(response.content) 198 | print("Successfully updated App to new version") 199 | else: 200 | print(f"Failed to download App update: {response.status_code}") 201 | return 202 | 203 | # Update version.txt 204 | with open('version.txt', 'w', encoding='utf-8-sig') as f: 205 | f.write(remote_version) 206 | print(f"Updated version.txt to {remote_version}") 207 | 208 | print("All components updated successfully") 209 | # Restart the application to apply updates 210 | os.execv(sys.executable, [sys.executable] + sys.argv) 211 | except Exception as e: 212 | print(f"Update check failed: {str(e)}") 213 | 214 | if __name__ == '__main__': 215 | print("Checking for updates...") 216 | check_for_updates() 217 | debug = os.environ.get('FLASK_ENV') == 'development' 218 | host = '127.0.0.1' if debug else '0.0.0.0' 219 | app.run(debug=debug, host=host, port=6991) -------------------------------------------------------------------------------- /ExternalAttacker-MCP.py: -------------------------------------------------------------------------------- 1 | from fastmcp import FastMCP 2 | import subprocess 3 | import requests 4 | import os 5 | mcp = FastMCP("docs") 6 | 7 | def run_command(command: list): 8 | """Run command via the Flask app's /api/run endpoint""" 9 | try: 10 | tool = command[0] 11 | args = " ".join(command[1:]) 12 | 13 | response = requests.post( 14 | "http://localhost:6991/api/run", 15 | json={"tool": tool, "args": args}, 16 | timeout=300 17 | ) 18 | 19 | # Handle HTTP errors 20 | if response.status_code != 200: 21 | return { 22 | 'stdout': '', 23 | 'stderr': f'HTTP Error: {response.status_code}', 24 | 'returncode': 1 25 | } 26 | 27 | # Parse JSON response 28 | result = response.json() 29 | 30 | # Ensure response has the expected structure 31 | if 'error' in result and 'stdout' not in result: 32 | return { 33 | 'stdout': '', 34 | 'stderr': result['error'], 35 | 'returncode': 1 36 | } 37 | 38 | return result 39 | except requests.exceptions.RequestException as e: 40 | return { 41 | 'stdout': '', 42 | 'stderr': f'Request error: {str(e)}', 43 | 'returncode': 1 44 | } 45 | except Exception as e: 46 | return { 47 | 'stdout': '', 48 | 'stderr': f'Error: {str(e)}', 49 | 'returncode': 1 50 | } 51 | 52 | @mcp.tool() 53 | async def scan_subdomains(target: str, domain_file: bool, threads: int = 4): 54 | """ 55 | Scan target domain(s) for subdomains using subfinder binary 56 | 57 | Args: 58 | target: Domain or file containing domains to scan 59 | domain_file: Whether target is a file containing domains 60 | threads: Number of concurrent threads to use 61 | """ 62 | command = [ 63 | "subfinder", 64 | "-list" if domain_file else "-domain", target, 65 | "-json", 66 | "-all", 67 | "-silent", 68 | "-active", 69 | "-t", str(threads) 70 | ] 71 | return run_command(command) 72 | 73 | @mcp.tool() 74 | async def scan_ports(target: str, file: bool, ports: str = "80,443", top_ports: bool = False, threads: int = 20): 75 | """ 76 | Scan a target domain for open ports using naabu binary 77 | 78 | Args: 79 | target: Domain or IP to scan 80 | file: Whether to scan a file containing domains 81 | ports: Port range to scan (e.g. "80,443" or "1-1000") or number of top ports 82 | top_ports: Whether to scan top N ports or use port range 83 | threads: Number of concurrent threads (default: 20) 84 | """ 85 | command = [ 86 | "naabu", 87 | "-silent", 88 | "-nc", 89 | "-c", str(threads), 90 | "-r", "8.8.8.8", 91 | "-skip-host-discovery", 92 | "-scan-all-ips", 93 | "-json" 94 | ] 95 | 96 | if file: 97 | command.extend(["-list", str(target)]) 98 | else: 99 | command.extend(["-host", str(target)]) 100 | 101 | if top_ports: 102 | command.extend(["-top-ports", str(ports)]) 103 | else: 104 | command.extend(["-port", str(ports)]) 105 | 106 | return run_command(command) 107 | 108 | @mcp.tool() 109 | async def analyze_http_services(target: str, file: bool, threads: int = 20): 110 | """ 111 | Scan a target domain for HTTP/HTTPS services using httpx binary 112 | 113 | Args: 114 | target: Domain or file containing domains to scan 115 | file: Whether target is a file containing domains 116 | threads: Number of concurrent threads to use 117 | """ 118 | command = [ 119 | "httpx", 120 | "-silent", 121 | "-nc", 122 | "-threads", str(threads), 123 | "-json", 124 | "-title", 125 | "-status-code", 126 | "-content-length", 127 | "-server", 128 | "-tech-detect" 129 | ] 130 | 131 | if file: 132 | command.extend(["-list", str(target)]) 133 | else: 134 | command.extend(["-target", str(target)]) 135 | 136 | return run_command(command) 137 | 138 | @mcp.tool() 139 | async def detect_cdn(target: str, resolver: str = "8.8.8.8"): 140 | """ 141 | Check if a target domain is using a CDN using cdncheck binary 142 | 143 | Args: 144 | target: Domain to check for CDN usage 145 | resolver: DNS resolver to use 146 | """ 147 | command = [ 148 | "cdncheck", 149 | "-input", str(target), 150 | "-resolver", resolver, 151 | "-nc", 152 | "-duc", 153 | "-silent", 154 | "-resp", 155 | "-jsonl" 156 | ] 157 | 158 | return run_command(command) 159 | 160 | @mcp.tool() 161 | async def analyze_tls_config(target: str, file: bool, port: int = 443, resolver: str = "8.8.8.8", threads: int = 300): 162 | """ 163 | Scan a target domain for TLS/SSL configuration using tlsx binary 164 | 165 | Args: 166 | target: Domain or file containing domains to scan 167 | file: Whether target is a file containing domains 168 | port: Port to scan for TLS 169 | resolver: DNS resolver to use 170 | threads: Number of concurrent threads to use 171 | """ 172 | command = [ 173 | "tlsx", 174 | "-silent", 175 | "-resolvers", str(resolver), 176 | "-nc", 177 | "-c", str(threads), 178 | "-p", str(port), 179 | "-json", 180 | "-so", 181 | "-tls-version", 182 | "-cipher", 183 | "-wildcard-cert", 184 | "-probe-status", 185 | "-version-enum", 186 | "-cipher-enum", 187 | "-cipher-type", "all", 188 | "-serial" 189 | ] 190 | if file: 191 | command.extend(["-l", str(target)]) 192 | else: 193 | command.extend(["-u", str(target)]) 194 | 195 | return run_command(command) 196 | 197 | @mcp.tool() 198 | async def enumerate_assets(mode: str, target: str = None, wordlist: str = None, threads: int = 10, 199 | extensions: str = None, status_codes: str = None, output: str = None, 200 | resolver: str = None, append_domain: bool = True, methods: str = None, 201 | project: str = None, region: str = None, server: str = None): 202 | """ 203 | Unified asset enumeration using gobuster binary 204 | 205 | Args: 206 | mode: Gobuster mode (dir/dns/vhost/fuzz/gcs/s3/tftp) 207 | target: Target URL/domain/server (required for dir/dns/vhost/fuzz/tftp) 208 | wordlist: Path to wordlist 209 | threads: Number of concurrent threads 210 | extensions: File extensions for dir mode 211 | status_codes: Status codes for dir mode 212 | output: Output file path 213 | resolver: DNS resolver for dns mode 214 | append_domain: Append domain in vhost mode 215 | methods: HTTP methods for fuzz mode 216 | project: Project ID for gcs mode 217 | region: AWS region for s3 mode 218 | server: Server address for tftp mode 219 | """ 220 | if not wordlist: 221 | raise ValueError("wordlist is required") 222 | 223 | command = ["gobuster", mode, "-t", str(threads), "-q"] 224 | 225 | # Add mode-specific arguments 226 | if mode == "dir": 227 | if not target: 228 | raise ValueError("target is required for dir mode") 229 | command.extend(["-u", target+"/FUZZ", "-w", wordlist]) 230 | if extensions: 231 | command.extend(["-x", extensions]) 232 | if status_codes: 233 | command.extend(["-s", status_codes]) 234 | 235 | elif mode == "dns": 236 | if not target: 237 | raise ValueError("target is required for dns mode") 238 | command.extend(["-d", target, "-w", wordlist]) 239 | if resolver: 240 | command.extend(["-r", resolver]) 241 | 242 | elif mode == "vhost": 243 | if not target: 244 | raise ValueError("target is required for vhost mode") 245 | command.extend(["-u", target, "-w", wordlist]) 246 | if not append_domain: 247 | command.append("--append-domain=false") 248 | 249 | elif mode == "fuzz": 250 | if not target: 251 | raise ValueError("target is required for fuzz mode") 252 | command.extend(["-u", target, "-w", wordlist]) 253 | if methods: 254 | command.extend(["-m", methods]) 255 | 256 | elif mode == "gcs": 257 | command.extend(["-w", wordlist]) 258 | if project: 259 | command.extend(["--project", project]) 260 | 261 | elif mode == "s3": 262 | command.extend(["-w", wordlist]) 263 | if region: 264 | command.extend(["--region", region]) 265 | 266 | elif mode == "tftp": 267 | if not server: 268 | raise ValueError("server is required for tftp mode") 269 | command.extend(["-w", wordlist, "--server", server]) 270 | 271 | if output: 272 | command.extend(["-o", output]) 273 | 274 | return run_command(command) 275 | 276 | @mcp.tool() 277 | async def fuzz_endpoints(target: str, threads: int = 40, 278 | wordlist: str = "https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/directory-list-2.3-medium.txt"): 279 | """ 280 | Fuzz a target domain for hidden endpoints using ffuf binary 281 | 282 | Args: 283 | target: Target domain to fuzz 284 | threads: Number of concurrent threads to use 285 | wordlist: Path to wordlist to use 286 | """ 287 | if "://" in wordlist: 288 | r = requests.get(wordlist) 289 | path = os.path.join(os.getcwd(), wordlist.split("/")[-1]) 290 | with open(path, "w") as f: 291 | f.write(r.text) 292 | wordlist = path 293 | else: 294 | if not os.path.exists(wordlist): 295 | raise FileNotFoundError(f"File {wordlist} not found") 296 | command = [ 297 | "ffuf", 298 | "-s", 299 | "-w", wordlist, 300 | "-u", target+"/FUZZ", 301 | "-t", str(threads) 302 | ] 303 | return run_command(command) 304 | 305 | @mcp.tool() 306 | async def resolve_dns(target: str, file: bool, threads: int = 100, resolver: str = "8.8.8.8", 307 | wordlist: str = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/subdomains-top1million-5000.txt"): 308 | """ 309 | Run DNS enumeration using dnsx binary 310 | 311 | Args: 312 | target: Domain or file containing domains to scan 313 | file: Whether target is a file containing domains 314 | threads: Number of concurrent threads to use 315 | resolver: DNS resolver to use 316 | wordlist: Path to wordlist for subdomain enumeration 317 | """ 318 | if "://" in wordlist: 319 | r = requests.get(wordlist) 320 | path = os.path.join(os.getcwd(), wordlist.split("/")[-1]) 321 | with open(path, "w") as f: 322 | f.write(r.text) 323 | wordlist = path 324 | else: 325 | if not os.path.exists(wordlist): 326 | raise FileNotFoundError(f"File {wordlist} not found") 327 | command = [ 328 | "dnsx", 329 | "-silent", 330 | "-t", str(threads), 331 | "-json", 332 | "-r", str(resolver), 333 | "-all" 334 | ] 335 | if file: 336 | command.extend(["-l", str(target)]) 337 | else: 338 | command.extend(["-d", str(target)]) 339 | 340 | 341 | if __name__ == "__main__": 342 | mcp.run(transport="stdio") --------------------------------------------------------------------------------