Safely run a command on a Metasploit console and return the output. Relies primarily on detecting the MSF prompt for command completion. Args: console: The Metasploit console object (MsfConsole). cmd: The command to run. execution_timeout: Optional specific time
(console: MsfConsole, cmd: str, execution_timeout: Optional[int] = None)
| 216 | # else: logger.debug("No console ID obtained, skipping destruction.") |
| 217 | |
| 218 | async def run_command_safely(console: MsfConsole, cmd: str, execution_timeout: Optional[int] = None) -> str: |
| 219 | """ |
| 220 | Safely run a command on a Metasploit console and return the output. |
| 221 | Relies primarily on detecting the MSF prompt for command completion. |
| 222 | |
| 223 | Args: |
| 224 | console: The Metasploit console object (MsfConsole). |
| 225 | cmd: The command to run. |
| 226 | execution_timeout: Optional specific timeout for this command's execution phase. |
| 227 | |
| 228 | Returns: |
| 229 | The command output as a string. |
| 230 | """ |
| 231 | if not (hasattr(console, 'write') and hasattr(console, 'read')): |
| 232 | logger.error(f"Console object {type(console)} lacks required methods (write, read).") |
| 233 | raise TypeError("Unsupported console object type for command execution.") |
| 234 | |
| 235 | try: |
| 236 | logger.debug(f"Running console command: {cmd}") |
| 237 | await asyncio.to_thread(lambda: console.write(cmd + '\n')) |
| 238 | |
| 239 | output_buffer = b"" # Read as bytes to handle potential encoding issues and prompt matching |
| 240 | start_time = asyncio.get_event_loop().time() |
| 241 | |
| 242 | # Determine read timeout - use inactivity timeout as fallback |
| 243 | read_timeout = execution_timeout or (LONG_CONSOLE_READ_TIMEOUT if cmd.strip().startswith(("run", "exploit", "check")) else DEFAULT_CONSOLE_READ_TIMEOUT) |
| 244 | check_interval = 0.1 # Seconds between reads |
| 245 | last_data_time = start_time |
| 246 | |
| 247 | while True: |
| 248 | await asyncio.sleep(check_interval) |
| 249 | current_time = asyncio.get_event_loop().time() |
| 250 | |
| 251 | # Check overall timeout first |
| 252 | if (current_time - start_time) > read_timeout: |
| 253 | logger.warning(f"Overall timeout ({read_timeout}s) reached for console command '{cmd}'.") |
| 254 | break |
| 255 | |
| 256 | # Read available data |
| 257 | try: |
| 258 | chunk_result = await asyncio.to_thread(lambda: console.read()) |
| 259 | # console.read() returns {'data': '...', 'prompt': '...', 'busy': bool} |
| 260 | chunk_data = chunk_result.get('data', '').encode('utf-8', errors='replace') # Ensure bytes |
| 261 | |
| 262 | # Handle the prompt - ensure it's bytes for pattern matching |
| 263 | prompt_str = chunk_result.get('prompt', '') |
| 264 | prompt_bytes = prompt_str.encode('utf-8', errors='replace') if isinstance(prompt_str, str) else prompt_str |
| 265 | except Exception as read_err: |
| 266 | logger.warning(f"Error reading from console during command '{cmd}': {read_err}") |
| 267 | await asyncio.sleep(0.5) # Wait a bit before retrying or timing out |
| 268 | continue |
| 269 | |
| 270 | if chunk_data: |
| 271 | # logger.debug(f"Read chunk (bytes): {chunk_data[:100]}...") # Log sparingly |
| 272 | output_buffer += chunk_data |
| 273 | last_data_time = current_time # Reset inactivity timer |
| 274 | |
| 275 | # Primary Completion Check: Did we receive the prompt? |