├── .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 | [](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 |
--------------------------------------------------------------------------------