├── .gitignore ├── requirements.txt ├── LICENSE ├── README.md ├── mcp_server.py └── kali_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=3.0.0 2 | requests>=2.31.0 3 | mcp>=1.0.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yousof Nahya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Kali Server 2 | 3 | **Kali MCP Server** is a lightweight API bridge that connects MCP Clients (e.g: Claude Desktop, [5ire](https://github.com/nanbingxyz/5ire)) to the API server which allows excuting commands on a Linux terminal. 4 | 5 | This allows the MCP to run terminal commands like `nmap`, `nxc` or any other tool, interact with web applications using tools like `curl`, `wget`, `gobuster`. 6 | And perform **AI-assisted penetration testing**, solving **CTF web challenge** in real time, helping in **solving machines from HTB or THM**. 7 | 8 | ## My Medium Article on This Tool 9 | 10 | [![How MCP is Revolutionizing Offensive Security](https://miro.medium.com/v2/resize:fit:828/format:webp/1*g4h-mIpPEHpq_H63W7Emsg.png)](https://yousofnahya.medium.com/how-mcp-is-revolutionizing-offensive-security-93b2442a5096) 11 | 12 | 👉 [**How MCP is Revolutionizing Offensive Security**](https://yousofnahya.medium.com/how-mcp-is-revolutionizing-offensive-security-93b2442a5096) 13 | 14 | --- 15 | 16 | ## 🔍 Use Case 17 | 18 | The goal is to enable AI-driven offensive security testing by: 19 | 20 | - Letting the MCP interact with AI endpoints like OpenAI, Claude, DeepSeek, or any other models. 21 | - Exposing an API to execute commands on a Kali machine. 22 | - Using AI to suggest and run terminal commands to solve CTF challenges or automate recon/exploitation tasks. 23 | - Allowing MCP apps to send custom requests (e.g., `curl`, `nmap`, `ffuf`, etc.) and receive structured outputs. 24 | 25 | Here are some example for my testing (I used google's AI `gemini 2.0 flash`) 26 | 27 | ### Example solving my web CTF challenge in RamadanCTF 28 | https://github.com/user-attachments/assets/dc93b71d-9a4a-4ad5-8079-2c26c04e5397 29 | 30 | ### Trying to solve machine "code" from HTB 31 | https://github.com/user-attachments/assets/3ec06ff8-0bdf-4ad5-be71-2ec490b7ee27 32 | 33 | 34 | --- 35 | 36 | ## 🚀 Features 37 | 38 | - 🧠 **AI Endpoint Integration**: Connect your kali to any MCP of your liking such as claude desktop or 5ier. 39 | - 🖥️ **Command Execution API**: Exposes a controlled API to execute terminal commands on your Kali Linux machine. 40 | - 🕸️ **Web Challenge Support**: AI can interact with websites and APIs, capture flags via `curl` and any other tool AI the needs. 41 | - 🔐 **Designed for Offensive Security Professionals**: Ideal for red teamers, bug bounty hunters, or CTF players automating common tasks. 42 | 43 | --- 44 | 45 | ## 🛠️ Installation and Running 46 | 47 | ### On your Kali Machine 48 | ```bash 49 | git clone https://github.com/Wh0am123/MCP-Kali-Server.git 50 | cd MCP-Kali-Server 51 | pip install -r requirements.txt 52 | python3 kali_server.py 53 | ``` 54 | 55 | **Command Line Options:** 56 | - `--ip
`: Specify the IP address to bind the server to (default: `127.0.0.1` for localhost only) 57 | - Use `127.0.0.1` for local connections only (secure, recommended) 58 | - Use `0.0.0.0` to allow connections from any network interface (very dangerous; use with caution) 59 | - Use a specific IP address to bind to a particular network interface 60 | - `--port `: Specify the port number (default: `5000`) 61 | - `--debug`: Enable debug mode for verbose logging 62 | 63 | **Examples:** 64 | ```bash 65 | # Run on localhost only (secure, default) 66 | python3 kali_server.py 67 | 68 | # Run on all interfaces (less secure, useful for remote access) 69 | python3 kali_server.py --ip 0.0.0.0 70 | 71 | # Run on a specific IP and custom port 72 | python3 kali_server.py --ip 192.168.1.100 --port 8080 73 | 74 | # Run with debug mode 75 | python3 kali_server.py --debug 76 | ``` 77 | 78 | ### On your MCP client machine (can be local or remote) 79 | 80 | ```bash 81 | git clone https://github.com/Wh0am123/MCP-Kali-Server.git 82 | cd MCP-Kali-Server 83 | pip install -r requirements.txt 84 | ``` 85 | 86 | If you're running the client and server on the same machine: 87 | 88 | ```bash 89 | ./mcp_server.py --server http://127.0.0.1:5000 90 | ``` 91 | 92 | If separate machines, create an ssh tunnel to your Kali MCP server, then launch the client: 93 | 94 | ```bash 95 | ssh -L 5000:localhost:5000 user@KALI_IP 96 | ./mcp_server.py --server http://127.0.0.1:5000 97 | ``` 98 | 99 | NOTE: If you're openly hosting the Kali MCP server on your network (`kali_server --IP...`), you don't need the SSH tunnel ⚠️(this is highly discouraged)⚠️. 100 | 101 | ```bash 102 | ./mcp_server.py --server http://LINUX_IP:5000 103 | ``` 104 | 105 | #### Configuration for claude desktop: 106 | edit (C:\Users\USERNAME\AppData\Roaming\Claude\claude_desktop_config.json) 107 | 108 | ```json 109 | { 110 | "mcpServers": { 111 | "kali_mcp": { 112 | "command": "python3", 113 | "args": [ 114 | "/absolute/path/to/mcp_server.py", 115 | "--server", 116 | "http://LINUX_IP:5000/" 117 | ] 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | #### Configuration for [5ire](https://github.com/nanbingxyz/5ire) Desktop Application: 124 | - Simply add an MCP with the command `python3 /absolute/path/to/mcp_server.py http://LINUX_IP:5000` and it will automatically generate the needed configuration files. 125 | 126 | ## 🔮 Other Possibilities 127 | 128 | There are more possibilites than described since the AI model can now execute commands on the terminal. Here are some example: 129 | 130 | - Memory forensics using Volatility 131 | - Automating memory analysis tasks such as process enumeration, DLL injection checks, and registry extraction from memory dumps. 132 | 133 | - Disk forensics with SleuthKit 134 | - Automating analysis from disk images, timeline generation, file carving, and hash comparisons. 135 | 136 | 137 | ## ⚠️ Disclaimer: 138 | This project is intended solely for educational and ethical testing purposes. Any misuse of the information or tools provided — including unauthorized access, exploitation, or malicious activity — is strictly prohibited. 139 | The author assumes no responsibility for misuse. 140 | -------------------------------------------------------------------------------- /mcp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script connect the MCP AI agent to Kali Linux terminal and API Server. 4 | 5 | # some of the code here was inspired from https://github.com/whit3rabbit0/project_astro , be sure to check them out 6 | 7 | import sys 8 | import os 9 | import argparse 10 | import logging 11 | from typing import Dict, Any, Optional 12 | import requests 13 | 14 | from mcp.server.fastmcp import FastMCP 15 | 16 | # Configure logging 17 | logging.basicConfig( 18 | level=logging.INFO, 19 | format="%(asctime)s [%(levelname)s] %(message)s", 20 | handlers=[ 21 | logging.StreamHandler(sys.stdout) 22 | ] 23 | ) 24 | logger = logging.getLogger(__name__) 25 | 26 | # Default configuration 27 | DEFAULT_KALI_SERVER = "http://localhost:5000" # change to your linux IP 28 | DEFAULT_REQUEST_TIMEOUT = 300 # 5 minutes default timeout for API requests 29 | 30 | class KaliToolsClient: 31 | """Client for communicating with the Kali Linux Tools API Server""" 32 | 33 | def __init__(self, server_url: str, timeout: int = DEFAULT_REQUEST_TIMEOUT): 34 | """ 35 | Initialize the Kali Tools Client 36 | 37 | Args: 38 | server_url: URL of the Kali Tools API Server 39 | timeout: Request timeout in seconds 40 | """ 41 | self.server_url = server_url.rstrip("/") 42 | self.timeout = timeout 43 | logger.info(f"Initialized Kali Tools Client connecting to {server_url}") 44 | 45 | def safe_get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 46 | """ 47 | Perform a GET request with optional query parameters. 48 | 49 | Args: 50 | endpoint: API endpoint path (without leading slash) 51 | params: Optional query parameters 52 | 53 | Returns: 54 | Response data as dictionary 55 | """ 56 | if params is None: 57 | params = {} 58 | 59 | url = f"{self.server_url}/{endpoint}" 60 | 61 | try: 62 | logger.debug(f"GET {url} with params: {params}") 63 | response = requests.get(url, params=params, timeout=self.timeout) 64 | response.raise_for_status() 65 | return response.json() 66 | except requests.exceptions.RequestException as e: 67 | logger.error(f"Request failed: {str(e)}") 68 | return {"error": f"Request failed: {str(e)}", "success": False} 69 | except Exception as e: 70 | logger.error(f"Unexpected error: {str(e)}") 71 | return {"error": f"Unexpected error: {str(e)}", "success": False} 72 | 73 | def safe_post(self, endpoint: str, json_data: Dict[str, Any]) -> Dict[str, Any]: 74 | """ 75 | Perform a POST request with JSON data. 76 | 77 | Args: 78 | endpoint: API endpoint path (without leading slash) 79 | json_data: JSON data to send 80 | 81 | Returns: 82 | Response data as dictionary 83 | """ 84 | url = f"{self.server_url}/{endpoint}" 85 | 86 | try: 87 | logger.debug(f"POST {url} with data: {json_data}") 88 | response = requests.post(url, json=json_data, timeout=self.timeout) 89 | response.raise_for_status() 90 | return response.json() 91 | except requests.exceptions.RequestException as e: 92 | logger.error(f"Request failed: {str(e)}") 93 | return {"error": f"Request failed: {str(e)}", "success": False} 94 | except Exception as e: 95 | logger.error(f"Unexpected error: {str(e)}") 96 | return {"error": f"Unexpected error: {str(e)}", "success": False} 97 | 98 | def execute_command(self, command: str) -> Dict[str, Any]: 99 | """ 100 | Execute a generic command on the Kali server 101 | 102 | Args: 103 | command: Command to execute 104 | 105 | Returns: 106 | Command execution results 107 | """ 108 | return self.safe_post("api/command", {"command": command}) 109 | 110 | def check_health(self) -> Dict[str, Any]: 111 | """ 112 | Check the health of the Kali Tools API Server 113 | 114 | Returns: 115 | Health status information 116 | """ 117 | return self.safe_get("health") 118 | 119 | def setup_mcp_server(kali_client: KaliToolsClient) -> FastMCP: 120 | """ 121 | Set up the MCP server with all tool functions 122 | 123 | Args: 124 | kali_client: Initialized KaliToolsClient 125 | 126 | Returns: 127 | Configured FastMCP instance 128 | """ 129 | mcp = FastMCP("kali-mcp") 130 | 131 | @mcp.tool() 132 | def nmap_scan(target: str, scan_type: str = "-sV", ports: str = "", additional_args: str = "") -> Dict[str, Any]: 133 | """ 134 | Execute an Nmap scan against a target. 135 | 136 | Args: 137 | target: The IP address or hostname to scan 138 | scan_type: Scan type (e.g., -sV for version detection) 139 | ports: Comma-separated list of ports or port ranges 140 | additional_args: Additional Nmap arguments 141 | 142 | Returns: 143 | Scan results 144 | """ 145 | data = { 146 | "target": target, 147 | "scan_type": scan_type, 148 | "ports": ports, 149 | "additional_args": additional_args 150 | } 151 | return kali_client.safe_post("api/tools/nmap", data) 152 | 153 | @mcp.tool() 154 | def gobuster_scan(url: str, mode: str = "dir", wordlist: str = "/usr/share/wordlists/dirb/common.txt", additional_args: str = "") -> Dict[str, Any]: 155 | """ 156 | Execute Gobuster to find directories, DNS subdomains, or virtual hosts. 157 | 158 | Args: 159 | url: The target URL 160 | mode: Scan mode (dir, dns, fuzz, vhost) 161 | wordlist: Path to wordlist file 162 | additional_args: Additional Gobuster arguments 163 | 164 | Returns: 165 | Scan results 166 | """ 167 | data = { 168 | "url": url, 169 | "mode": mode, 170 | "wordlist": wordlist, 171 | "additional_args": additional_args 172 | } 173 | return kali_client.safe_post("api/tools/gobuster", data) 174 | 175 | @mcp.tool() 176 | def dirb_scan(url: str, wordlist: str = "/usr/share/wordlists/dirb/common.txt", additional_args: str = "") -> Dict[str, Any]: 177 | """ 178 | Execute Dirb web content scanner. 179 | 180 | Args: 181 | url: The target URL 182 | wordlist: Path to wordlist file 183 | additional_args: Additional Dirb arguments 184 | 185 | Returns: 186 | Scan results 187 | """ 188 | data = { 189 | "url": url, 190 | "wordlist": wordlist, 191 | "additional_args": additional_args 192 | } 193 | return kali_client.safe_post("api/tools/dirb", data) 194 | 195 | @mcp.tool() 196 | def nikto_scan(target: str, additional_args: str = "") -> Dict[str, Any]: 197 | """ 198 | Execute Nikto web server scanner. 199 | 200 | Args: 201 | target: The target URL or IP 202 | additional_args: Additional Nikto arguments 203 | 204 | Returns: 205 | Scan results 206 | """ 207 | data = { 208 | "target": target, 209 | "additional_args": additional_args 210 | } 211 | return kali_client.safe_post("api/tools/nikto", data) 212 | 213 | @mcp.tool() 214 | def sqlmap_scan(url: str, data: str = "", additional_args: str = "") -> Dict[str, Any]: 215 | """ 216 | Execute SQLmap SQL injection scanner. 217 | 218 | Args: 219 | url: The target URL 220 | data: POST data string 221 | additional_args: Additional SQLmap arguments 222 | 223 | Returns: 224 | Scan results 225 | """ 226 | post_data = { 227 | "url": url, 228 | "data": data, 229 | "additional_args": additional_args 230 | } 231 | return kali_client.safe_post("api/tools/sqlmap", post_data) 232 | 233 | @mcp.tool() 234 | def metasploit_run(module: str, options: Dict[str, Any] = {}) -> Dict[str, Any]: 235 | """ 236 | Execute a Metasploit module. 237 | 238 | Args: 239 | module: The Metasploit module path 240 | options: Dictionary of module options 241 | 242 | Returns: 243 | Module execution results 244 | """ 245 | data = { 246 | "module": module, 247 | "options": options 248 | } 249 | return kali_client.safe_post("api/tools/metasploit", data) 250 | 251 | @mcp.tool() 252 | def hydra_attack( 253 | target: str, 254 | service: str, 255 | username: str = "", 256 | username_file: str = "", 257 | password: str = "", 258 | password_file: str = "", 259 | additional_args: str = "" 260 | ) -> Dict[str, Any]: 261 | """ 262 | Execute Hydra password cracking tool. 263 | 264 | Args: 265 | target: Target IP or hostname 266 | service: Service to attack (ssh, ftp, http-post-form, etc.) 267 | username: Single username to try 268 | username_file: Path to username file 269 | password: Single password to try 270 | password_file: Path to password file 271 | additional_args: Additional Hydra arguments 272 | 273 | Returns: 274 | Attack results 275 | """ 276 | data = { 277 | "target": target, 278 | "service": service, 279 | "username": username, 280 | "username_file": username_file, 281 | "password": password, 282 | "password_file": password_file, 283 | "additional_args": additional_args 284 | } 285 | return kali_client.safe_post("api/tools/hydra", data) 286 | 287 | @mcp.tool() 288 | def john_crack( 289 | hash_file: str, 290 | wordlist: str = "/usr/share/wordlists/rockyou.txt", 291 | format_type: str = "", 292 | additional_args: str = "" 293 | ) -> Dict[str, Any]: 294 | """ 295 | Execute John the Ripper password cracker. 296 | 297 | Args: 298 | hash_file: Path to file containing hashes 299 | wordlist: Path to wordlist file 300 | format_type: Hash format type 301 | additional_args: Additional John arguments 302 | 303 | Returns: 304 | Cracking results 305 | """ 306 | data = { 307 | "hash_file": hash_file, 308 | "wordlist": wordlist, 309 | "format": format_type, 310 | "additional_args": additional_args 311 | } 312 | return kali_client.safe_post("api/tools/john", data) 313 | 314 | @mcp.tool() 315 | def wpscan_analyze(url: str, additional_args: str = "") -> Dict[str, Any]: 316 | """ 317 | Execute WPScan WordPress vulnerability scanner. 318 | 319 | Args: 320 | url: The target WordPress URL 321 | additional_args: Additional WPScan arguments 322 | 323 | Returns: 324 | Scan results 325 | """ 326 | data = { 327 | "url": url, 328 | "additional_args": additional_args 329 | } 330 | return kali_client.safe_post("api/tools/wpscan", data) 331 | 332 | @mcp.tool() 333 | def enum4linux_scan(target: str, additional_args: str = "-a") -> Dict[str, Any]: 334 | """ 335 | Execute Enum4linux Windows/Samba enumeration tool. 336 | 337 | Args: 338 | target: The target IP or hostname 339 | additional_args: Additional enum4linux arguments 340 | 341 | Returns: 342 | Enumeration results 343 | """ 344 | data = { 345 | "target": target, 346 | "additional_args": additional_args 347 | } 348 | return kali_client.safe_post("api/tools/enum4linux", data) 349 | 350 | @mcp.tool() 351 | def server_health() -> Dict[str, Any]: 352 | """ 353 | Check the health status of the Kali API server. 354 | 355 | Returns: 356 | Server health information 357 | """ 358 | return kali_client.check_health() 359 | 360 | @mcp.tool() 361 | def execute_command(command: str) -> Dict[str, Any]: 362 | """ 363 | Execute an arbitrary command on the Kali server. 364 | 365 | Args: 366 | command: The command to execute 367 | 368 | Returns: 369 | Command execution results 370 | """ 371 | return kali_client.execute_command(command) 372 | 373 | return mcp 374 | 375 | def parse_args(): 376 | """Parse command line arguments.""" 377 | parser = argparse.ArgumentParser(description="Run the Kali MCP Client") 378 | parser.add_argument("--server", type=str, default=DEFAULT_KALI_SERVER, 379 | help=f"Kali API server URL (default: {DEFAULT_KALI_SERVER})") 380 | parser.add_argument("--timeout", type=int, default=DEFAULT_REQUEST_TIMEOUT, 381 | help=f"Request timeout in seconds (default: {DEFAULT_REQUEST_TIMEOUT})") 382 | parser.add_argument("--debug", action="store_true", help="Enable debug logging") 383 | return parser.parse_args() 384 | 385 | def main(): 386 | """Main entry point for the MCP server.""" 387 | args = parse_args() 388 | 389 | # Configure logging based on debug flag 390 | if args.debug: 391 | logger.setLevel(logging.DEBUG) 392 | logger.debug("Debug logging enabled") 393 | 394 | # Initialize the Kali Tools client 395 | kali_client = KaliToolsClient(args.server, args.timeout) 396 | 397 | # Check server health and log the result 398 | health = kali_client.check_health() 399 | if "error" in health: 400 | logger.warning(f"Unable to connect to Kali API server at {args.server}: {health['error']}") 401 | logger.warning("MCP server will start, but tool execution may fail") 402 | else: 403 | logger.info(f"Successfully connected to Kali API server at {args.server}") 404 | logger.info(f"Server health status: {health['status']}") 405 | if not health.get("all_essential_tools_available", False): 406 | logger.warning("Not all essential tools are available on the Kali server") 407 | missing_tools = [tool for tool, available in health.get("tools_status", {}).items() if not available] 408 | if missing_tools: 409 | logger.warning(f"Missing tools: {', '.join(missing_tools)}") 410 | 411 | # Set up and run the MCP server 412 | mcp = setup_mcp_server(kali_client) 413 | logger.info("Starting Kali MCP server") 414 | mcp.run() 415 | 416 | if __name__ == "__main__": 417 | main() 418 | -------------------------------------------------------------------------------- /kali_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script connect the MCP AI agent to Kali Linux terminal and API Server. 4 | 5 | # some of the code here was inspired from https://github.com/whit3rabbit0/project_astro , be sure to check them out 6 | 7 | import argparse 8 | import json 9 | import logging 10 | import os 11 | import subprocess 12 | import sys 13 | import traceback 14 | import threading 15 | from typing import Dict, Any 16 | from flask import Flask, request, jsonify 17 | 18 | # Configure logging 19 | logging.basicConfig( 20 | level=logging.INFO, 21 | format="%(asctime)s [%(levelname)s] %(message)s", 22 | handlers=[ 23 | logging.StreamHandler(sys.stdout) 24 | ] 25 | ) 26 | logger = logging.getLogger(__name__) 27 | 28 | # Configuration 29 | API_PORT = int(os.environ.get("API_PORT", 5000)) 30 | DEBUG_MODE = os.environ.get("DEBUG_MODE", "0").lower() in ("1", "true", "yes", "y") 31 | COMMAND_TIMEOUT = 180 # 5 minutes default timeout 32 | 33 | app = Flask(__name__) 34 | 35 | class CommandExecutor: 36 | """Class to handle command execution with better timeout management""" 37 | 38 | def __init__(self, command: str, timeout: int = COMMAND_TIMEOUT): 39 | self.command = command 40 | self.timeout = timeout 41 | self.process = None 42 | self.stdout_data = "" 43 | self.stderr_data = "" 44 | self.stdout_thread = None 45 | self.stderr_thread = None 46 | self.return_code = None 47 | self.timed_out = False 48 | 49 | def _read_stdout(self): 50 | """Thread function to continuously read stdout""" 51 | for line in iter(self.process.stdout.readline, ''): 52 | self.stdout_data += line 53 | 54 | def _read_stderr(self): 55 | """Thread function to continuously read stderr""" 56 | for line in iter(self.process.stderr.readline, ''): 57 | self.stderr_data += line 58 | 59 | def execute(self) -> Dict[str, Any]: 60 | """Execute the command and handle timeout gracefully""" 61 | logger.info(f"Executing command: {self.command}") 62 | 63 | try: 64 | self.process = subprocess.Popen( 65 | self.command, 66 | shell=True, 67 | stdout=subprocess.PIPE, 68 | stderr=subprocess.PIPE, 69 | text=True, 70 | bufsize=1 # Line buffered 71 | ) 72 | 73 | # Start threads to read output continuously 74 | self.stdout_thread = threading.Thread(target=self._read_stdout) 75 | self.stderr_thread = threading.Thread(target=self._read_stderr) 76 | self.stdout_thread.daemon = True 77 | self.stderr_thread.daemon = True 78 | self.stdout_thread.start() 79 | self.stderr_thread.start() 80 | 81 | # Wait for the process to complete or timeout 82 | try: 83 | self.return_code = self.process.wait(timeout=self.timeout) 84 | # Process completed, join the threads 85 | self.stdout_thread.join() 86 | self.stderr_thread.join() 87 | except subprocess.TimeoutExpired: 88 | # Process timed out but we might have partial results 89 | self.timed_out = True 90 | logger.warning(f"Command timed out after {self.timeout} seconds. Terminating process.") 91 | 92 | # Try to terminate gracefully first 93 | self.process.terminate() 94 | try: 95 | self.process.wait(timeout=5) # Give it 5 seconds to terminate 96 | except subprocess.TimeoutExpired: 97 | # Force kill if it doesn't terminate 98 | logger.warning("Process not responding to termination. Killing.") 99 | self.process.kill() 100 | 101 | # Update final output 102 | self.return_code = -1 103 | 104 | # Always consider it a success if we have output, even with timeout 105 | success = True if self.timed_out and (self.stdout_data or self.stderr_data) else (self.return_code == 0) 106 | 107 | return { 108 | "stdout": self.stdout_data, 109 | "stderr": self.stderr_data, 110 | "return_code": self.return_code, 111 | "success": success, 112 | "timed_out": self.timed_out, 113 | "partial_results": self.timed_out and (self.stdout_data or self.stderr_data) 114 | } 115 | 116 | except Exception as e: 117 | logger.error(f"Error executing command: {str(e)}") 118 | logger.error(traceback.format_exc()) 119 | return { 120 | "stdout": self.stdout_data, 121 | "stderr": f"Error executing command: {str(e)}\n{self.stderr_data}", 122 | "return_code": -1, 123 | "success": False, 124 | "timed_out": False, 125 | "partial_results": bool(self.stdout_data or self.stderr_data) 126 | } 127 | 128 | 129 | def execute_command(command: str) -> Dict[str, Any]: 130 | """ 131 | Execute a shell command and return the result 132 | 133 | Args: 134 | command: The command to execute 135 | 136 | Returns: 137 | A dictionary containing the stdout, stderr, and return code 138 | """ 139 | executor = CommandExecutor(command) 140 | return executor.execute() 141 | 142 | 143 | @app.route("/api/command", methods=["POST"]) 144 | def generic_command(): 145 | """Execute any command provided in the request.""" 146 | try: 147 | params = request.json 148 | command = params.get("command", "") 149 | 150 | if not command: 151 | logger.warning("Command endpoint called without command parameter") 152 | return jsonify({ 153 | "error": "Command parameter is required" 154 | }), 400 155 | 156 | result = execute_command(command) 157 | return jsonify(result) 158 | except Exception as e: 159 | logger.error(f"Error in command endpoint: {str(e)}") 160 | logger.error(traceback.format_exc()) 161 | return jsonify({ 162 | "error": f"Server error: {str(e)}" 163 | }), 500 164 | 165 | 166 | @app.route("/api/tools/nmap", methods=["POST"]) 167 | def nmap(): 168 | """Execute nmap scan with the provided parameters.""" 169 | try: 170 | params = request.json 171 | target = params.get("target", "") 172 | scan_type = params.get("scan_type", "-sCV") 173 | ports = params.get("ports", "") 174 | additional_args = params.get("additional_args", "-T4 -Pn") 175 | 176 | if not target: 177 | logger.warning("Nmap called without target parameter") 178 | return jsonify({ 179 | "error": "Target parameter is required" 180 | }), 400 181 | 182 | command = f"nmap {scan_type}" 183 | 184 | if ports: 185 | command += f" -p {ports}" 186 | 187 | if additional_args: 188 | # Basic validation for additional args - more sophisticated validation would be better 189 | command += f" {additional_args}" 190 | 191 | command += f" {target}" 192 | 193 | result = execute_command(command) 194 | return jsonify(result) 195 | except Exception as e: 196 | logger.error(f"Error in nmap endpoint: {str(e)}") 197 | logger.error(traceback.format_exc()) 198 | return jsonify({ 199 | "error": f"Server error: {str(e)}" 200 | }), 500 201 | 202 | @app.route("/api/tools/gobuster", methods=["POST"]) 203 | def gobuster(): 204 | """Execute gobuster with the provided parameters.""" 205 | try: 206 | params = request.json 207 | url = params.get("url", "") 208 | mode = params.get("mode", "dir") 209 | wordlist = params.get("wordlist", "/usr/share/wordlists/dirb/common.txt") 210 | additional_args = params.get("additional_args", "") 211 | 212 | if not url: 213 | logger.warning("Gobuster called without URL parameter") 214 | return jsonify({ 215 | "error": "URL parameter is required" 216 | }), 400 217 | 218 | # Validate mode 219 | if mode not in ["dir", "dns", "fuzz", "vhost"]: 220 | logger.warning(f"Invalid gobuster mode: {mode}") 221 | return jsonify({ 222 | "error": f"Invalid mode: {mode}. Must be one of: dir, dns, fuzz, vhost" 223 | }), 400 224 | 225 | command = f"gobuster {mode} -u {url} -w {wordlist}" 226 | 227 | if additional_args: 228 | command += f" {additional_args}" 229 | 230 | result = execute_command(command) 231 | return jsonify(result) 232 | except Exception as e: 233 | logger.error(f"Error in gobuster endpoint: {str(e)}") 234 | logger.error(traceback.format_exc()) 235 | return jsonify({ 236 | "error": f"Server error: {str(e)}" 237 | }), 500 238 | 239 | @app.route("/api/tools/dirb", methods=["POST"]) 240 | def dirb(): 241 | """Execute dirb with the provided parameters.""" 242 | try: 243 | params = request.json 244 | url = params.get("url", "") 245 | wordlist = params.get("wordlist", "/usr/share/wordlists/dirb/common.txt") 246 | additional_args = params.get("additional_args", "") 247 | 248 | if not url: 249 | logger.warning("Dirb called without URL parameter") 250 | return jsonify({ 251 | "error": "URL parameter is required" 252 | }), 400 253 | 254 | command = f"dirb {url} {wordlist}" 255 | 256 | if additional_args: 257 | command += f" {additional_args}" 258 | 259 | result = execute_command(command) 260 | return jsonify(result) 261 | except Exception as e: 262 | logger.error(f"Error in dirb endpoint: {str(e)}") 263 | logger.error(traceback.format_exc()) 264 | return jsonify({ 265 | "error": f"Server error: {str(e)}" 266 | }), 500 267 | 268 | @app.route("/api/tools/nikto", methods=["POST"]) 269 | def nikto(): 270 | """Execute nikto with the provided parameters.""" 271 | try: 272 | params = request.json 273 | target = params.get("target", "") 274 | additional_args = params.get("additional_args", "") 275 | 276 | if not target: 277 | logger.warning("Nikto called without target parameter") 278 | return jsonify({ 279 | "error": "Target parameter is required" 280 | }), 400 281 | 282 | command = f"nikto -h {target}" 283 | 284 | if additional_args: 285 | command += f" {additional_args}" 286 | 287 | result = execute_command(command) 288 | return jsonify(result) 289 | except Exception as e: 290 | logger.error(f"Error in nikto endpoint: {str(e)}") 291 | logger.error(traceback.format_exc()) 292 | return jsonify({ 293 | "error": f"Server error: {str(e)}" 294 | }), 500 295 | 296 | @app.route("/api/tools/sqlmap", methods=["POST"]) 297 | def sqlmap(): 298 | """Execute sqlmap with the provided parameters.""" 299 | try: 300 | params = request.json 301 | url = params.get("url", "") 302 | data = params.get("data", "") 303 | additional_args = params.get("additional_args", "") 304 | 305 | if not url: 306 | logger.warning("SQLMap called without URL parameter") 307 | return jsonify({ 308 | "error": "URL parameter is required" 309 | }), 400 310 | 311 | command = f"sqlmap -u {url} --batch" 312 | 313 | if data: 314 | command += f" --data=\"{data}\"" 315 | 316 | if additional_args: 317 | command += f" {additional_args}" 318 | 319 | result = execute_command(command) 320 | return jsonify(result) 321 | except Exception as e: 322 | logger.error(f"Error in sqlmap endpoint: {str(e)}") 323 | logger.error(traceback.format_exc()) 324 | return jsonify({ 325 | "error": f"Server error: {str(e)}" 326 | }), 500 327 | 328 | @app.route("/api/tools/metasploit", methods=["POST"]) 329 | def metasploit(): 330 | """Execute metasploit module with the provided parameters.""" 331 | try: 332 | params = request.json 333 | module = params.get("module", "") 334 | options = params.get("options", {}) 335 | 336 | if not module: 337 | logger.warning("Metasploit called without module parameter") 338 | return jsonify({ 339 | "error": "Module parameter is required" 340 | }), 400 341 | 342 | # Format options for Metasploit 343 | options_str = "" 344 | for key, value in options.items(): 345 | options_str += f" {key}={value}" 346 | 347 | # Create an MSF resource script 348 | resource_content = f"use {module}\n" 349 | for key, value in options.items(): 350 | resource_content += f"set {key} {value}\n" 351 | resource_content += "exploit\n" 352 | 353 | # Save resource script to a temporary file 354 | resource_file = "/tmp/mcp_msf_resource.rc" 355 | with open(resource_file, "w") as f: 356 | f.write(resource_content) 357 | 358 | command = f"msfconsole -q -r {resource_file}" 359 | result = execute_command(command) 360 | 361 | # Clean up the temporary file 362 | try: 363 | os.remove(resource_file) 364 | except Exception as e: 365 | logger.warning(f"Error removing temporary resource file: {str(e)}") 366 | 367 | return jsonify(result) 368 | except Exception as e: 369 | logger.error(f"Error in metasploit endpoint: {str(e)}") 370 | logger.error(traceback.format_exc()) 371 | return jsonify({ 372 | "error": f"Server error: {str(e)}" 373 | }), 500 374 | 375 | @app.route("/api/tools/hydra", methods=["POST"]) 376 | def hydra(): 377 | """Execute hydra with the provided parameters.""" 378 | try: 379 | params = request.json 380 | target = params.get("target", "") 381 | service = params.get("service", "") 382 | username = params.get("username", "") 383 | username_file = params.get("username_file", "") 384 | password = params.get("password", "") 385 | password_file = params.get("password_file", "") 386 | additional_args = params.get("additional_args", "") 387 | 388 | if not target or not service: 389 | logger.warning("Hydra called without target or service parameter") 390 | return jsonify({ 391 | "error": "Target and service parameters are required" 392 | }), 400 393 | 394 | if not (username or username_file) or not (password or password_file): 395 | logger.warning("Hydra called without username/password parameters") 396 | return jsonify({ 397 | "error": "Username/username_file and password/password_file are required" 398 | }), 400 399 | 400 | command = f"hydra -t 4" 401 | 402 | if username: 403 | command += f" -l {username}" 404 | elif username_file: 405 | command += f" -L {username_file}" 406 | 407 | if password: 408 | command += f" -p {password}" 409 | elif password_file: 410 | command += f" -P {password_file}" 411 | 412 | if additional_args: 413 | command += f" {additional_args}" 414 | 415 | command += f" {target} {service}" 416 | 417 | result = execute_command(command) 418 | return jsonify(result) 419 | except Exception as e: 420 | logger.error(f"Error in hydra endpoint: {str(e)}") 421 | logger.error(traceback.format_exc()) 422 | return jsonify({ 423 | "error": f"Server error: {str(e)}" 424 | }), 500 425 | 426 | @app.route("/api/tools/john", methods=["POST"]) 427 | def john(): 428 | """Execute john with the provided parameters.""" 429 | try: 430 | params = request.json 431 | hash_file = params.get("hash_file", "") 432 | wordlist = params.get("wordlist", "/usr/share/wordlists/rockyou.txt") 433 | format_type = params.get("format", "") 434 | additional_args = params.get("additional_args", "") 435 | 436 | if not hash_file: 437 | logger.warning("John called without hash_file parameter") 438 | return jsonify({ 439 | "error": "Hash file parameter is required" 440 | }), 400 441 | 442 | command = f"john" 443 | 444 | if format_type: 445 | command += f" --format={format_type}" 446 | 447 | if wordlist: 448 | command += f" --wordlist={wordlist}" 449 | 450 | if additional_args: 451 | command += f" {additional_args}" 452 | 453 | command += f" {hash_file}" 454 | 455 | result = execute_command(command) 456 | return jsonify(result) 457 | except Exception as e: 458 | logger.error(f"Error in john endpoint: {str(e)}") 459 | logger.error(traceback.format_exc()) 460 | return jsonify({ 461 | "error": f"Server error: {str(e)}" 462 | }), 500 463 | 464 | @app.route("/api/tools/wpscan", methods=["POST"]) 465 | def wpscan(): 466 | """Execute wpscan with the provided parameters.""" 467 | try: 468 | params = request.json 469 | url = params.get("url", "") 470 | additional_args = params.get("additional_args", "") 471 | 472 | if not url: 473 | logger.warning("WPScan called without URL parameter") 474 | return jsonify({ 475 | "error": "URL parameter is required" 476 | }), 400 477 | 478 | command = f"wpscan --url {url}" 479 | 480 | if additional_args: 481 | command += f" {additional_args}" 482 | 483 | result = execute_command(command) 484 | return jsonify(result) 485 | except Exception as e: 486 | logger.error(f"Error in wpscan endpoint: {str(e)}") 487 | logger.error(traceback.format_exc()) 488 | return jsonify({ 489 | "error": f"Server error: {str(e)}" 490 | }), 500 491 | 492 | @app.route("/api/tools/enum4linux", methods=["POST"]) 493 | def enum4linux(): 494 | """Execute enum4linux with the provided parameters.""" 495 | try: 496 | params = request.json 497 | target = params.get("target", "") 498 | additional_args = params.get("additional_args", "-a") 499 | 500 | if not target: 501 | logger.warning("Enum4linux called without target parameter") 502 | return jsonify({ 503 | "error": "Target parameter is required" 504 | }), 400 505 | 506 | command = f"enum4linux {additional_args} {target}" 507 | 508 | result = execute_command(command) 509 | return jsonify(result) 510 | except Exception as e: 511 | logger.error(f"Error in enum4linux endpoint: {str(e)}") 512 | logger.error(traceback.format_exc()) 513 | return jsonify({ 514 | "error": f"Server error: {str(e)}" 515 | }), 500 516 | 517 | 518 | # Health check endpoint 519 | @app.route("/health", methods=["GET"]) 520 | def health_check(): 521 | """Health check endpoint.""" 522 | # Check if essential tools are installed 523 | essential_tools = ["nmap", "gobuster", "dirb", "nikto"] 524 | tools_status = {} 525 | 526 | for tool in essential_tools: 527 | try: 528 | result = execute_command(f"which {tool}") 529 | tools_status[tool] = result["success"] 530 | except: 531 | tools_status[tool] = False 532 | 533 | all_essential_tools_available = all(tools_status.values()) 534 | 535 | return jsonify({ 536 | "status": "healthy", 537 | "message": "Kali Linux Tools API Server is running", 538 | "tools_status": tools_status, 539 | "all_essential_tools_available": all_essential_tools_available 540 | }) 541 | 542 | @app.route("/mcp/capabilities", methods=["GET"]) 543 | def get_capabilities(): 544 | # Return tool capabilities similar to our existing MCP server 545 | pass 546 | 547 | @app.route("/mcp/tools/kali_tools/", methods=["POST"]) 548 | def execute_tool(tool_name): 549 | # Direct tool execution without going through the API server 550 | pass 551 | 552 | def parse_args(): 553 | """Parse command line arguments.""" 554 | parser = argparse.ArgumentParser(description="Run the Kali Linux API Server") 555 | parser.add_argument("--debug", action="store_true", help="Enable debug mode") 556 | parser.add_argument("--port", type=int, default=API_PORT, help=f"Port for the API server (default: {API_PORT})") 557 | parser.add_argument("--ip", type=str, default="127.0.0.1", help="IP address to bind the server to (default: 127.0.0.1 for localhost only)") 558 | return parser.parse_args() 559 | 560 | if __name__ == "__main__": 561 | args = parse_args() 562 | 563 | # Set configuration from command line arguments 564 | if args.debug: 565 | DEBUG_MODE = True 566 | os.environ["DEBUG_MODE"] = "1" 567 | logger.setLevel(logging.DEBUG) 568 | 569 | if args.port != API_PORT: 570 | API_PORT = args.port 571 | 572 | logger.info(f"Starting Kali Linux Tools API Server on {args.ip}:{API_PORT}") 573 | app.run(host=args.ip, port=API_PORT, debug=DEBUG_MODE) 574 | --------------------------------------------------------------------------------