From cc8a1fade099bfb6af79244ccf0d3b89efd5188a Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sat, 6 Dec 2025 00:45:04 -0700 Subject: [PATCH] Additional Reverse Shell code cleanup --- .../Roles/ReverseTunnel/tunnel_Powershell.py | 112 +----------------- Data/Agent/agent-requirements.txt | 1 - .../src/Devices/ReverseTunnel/Powershell.jsx | 51 ++------ .../Agent_Reverse_Tunneling.md | 17 +-- 4 files changed, 26 insertions(+), 155 deletions(-) diff --git a/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py b/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py index 528ed0fc..97cbd4d0 100644 --- a/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py +++ b/Data/Agent/Roles/ReverseTunnel/tunnel_Powershell.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import os import sys import subprocess from typing import Any, Dict, Optional @@ -30,7 +29,6 @@ class PowershellChannel: self._reader_task = None self._writer_task = None self._stdin_queue: asyncio.Queue = asyncio.Queue() - self._pty = None self._proc: Optional[asyncio.subprocess.Process] = None self._exit_code: Optional[int] = None self._frame_cls = getattr(role, "_frame_cls", None) @@ -70,13 +68,6 @@ class PowershellChannel: # Keep the process alive and read commands from stdin; -Command - tells PS to consume stdin. return [shell, "-NoLogo", "-NoProfile", "-NoExit", "-Command", "-"] - def _initial_size(self) -> tuple: - cols = int(self.metadata.get("cols") or self.metadata.get("columns") or 120) if isinstance(self.metadata, dict) else 120 - rows = int(self.metadata.get("rows") or 32) if isinstance(self.metadata, dict) else 32 - cols = max(20, min(cols, 300)) - rows = max(10, min(rows, 200)) - return cols, rows - # ------------------------------------------------------------------ Lifecycle async def start(self) -> None: if sys.platform.lower().startswith("win") is False: @@ -85,25 +76,9 @@ class PowershellChannel: return argv = self._powershell_argv() - cols, rows = self._initial_size() - self.role._log(f"reverse_tunnel ps start channel={self.channel_id} argv={' '.join(argv)} cols={cols} rows={rows}") + self.role._log(f"reverse_tunnel ps start channel={self.channel_id} argv={' '.join(argv)} mode=pipes") - # Preferred: ConPTY via pywinpty. - try: - import pywinpty # type: ignore - - self._pty = pywinpty.Process( - spawn_cmd=" ".join(argv[:-1]) if argv[-1] == "-" else " ".join(argv), - dimensions=(cols, rows), - ) - self._reader_task = self.loop.create_task(self._pump_pty_stdout()) - self._writer_task = self.loop.create_task(self._pump_pty_stdin()) - self.role._log(f"reverse_tunnel ps channel started (pty) argv={' '.join(argv)} cols={cols} rows={rows}") - return - except Exception as exc: - self.role._log(f"reverse_tunnel ps channel pywinpty unavailable, falling back to pipes: {exc}", error=True) - - # Fallback: subprocess pipes (no PTY). + # Pipes (no PTY). try: self._proc = await asyncio.create_subprocess_exec( *argv, @@ -119,7 +94,7 @@ class PowershellChannel: self._reader_task = self.loop.create_task(self._pump_proc_stdout()) self._writer_task = self.loop.create_task(self._pump_proc_stdin()) - self.role._log(f"reverse_tunnel ps channel started (pipes) argv={' '.join(argv)} cols={cols} rows={rows}") + self.role._log(f"reverse_tunnel ps channel started (pipes) argv={' '.join(argv)}") async def on_frame(self, frame) -> None: if self._closed: @@ -139,80 +114,8 @@ class PowershellChannel: return async def _handle_control(self, payload: bytes) -> None: - try: - import json - - data = json.loads(payload.decode("utf-8")) - except Exception: - return - cols = data.get("cols") or data.get("columns") - rows = data.get("rows") - if cols is None and rows is None: - return - try: - cols_int = int(cols) if cols is not None else None - rows_int = int(rows) if rows is not None else None - except Exception: - return - await self._resize(cols_int, rows_int) - - async def _resize(self, cols: Optional[int], rows: Optional[int]) -> None: - # Resize only applies to PTY sessions; pipe mode ignores. - if self._pty is None: - return - try: - cur_cols, cur_rows = self._initial_size() - if cols is None: - cols = cur_cols - if rows is None: - rows = cur_rows - cols = max(20, min(int(cols), 300)) - rows = max(10, min(int(rows), 200)) - self._pty.set_size(cols, rows) - self.role._log(f"reverse_tunnel ps channel resized cols={cols} rows={rows}") - except Exception: - self.role._log("reverse_tunnel ps channel resize failed", error=True) - - async def _pump_pty_stdout(self) -> None: - loop = asyncio.get_event_loop() - try: - while not self._closed and self._pty: - chunk = await loop.run_in_executor(None, self._pty.read, 4096) - if chunk is None: - break - data = chunk.encode("utf-8", errors="replace") if isinstance(chunk, str) else bytes(chunk) - if not data: - break - frame = self._make_frame(MSG_DATA, payload=data) - await self._send_frame(frame) - except asyncio.CancelledError: - pass - except Exception: - self.role._log("reverse_tunnel ps pty stdout pump error", error=True) - finally: - await self.stop(reason="stdout_closed") - - async def _pump_pty_stdin(self) -> None: - loop = asyncio.get_event_loop() - try: - while not self._closed and self._pty: - try: - data = await self._stdin_queue.get() - except asyncio.CancelledError: - break - if data is None: - break - text = data.decode("utf-8", errors="replace") if isinstance(data, (bytes, bytearray)) else str(data) - try: - await loop.run_in_executor(None, self._pty.write, text) - except Exception: - break - except asyncio.CancelledError: - pass - except Exception: - self.role._log("reverse_tunnel ps pty stdin pump error", error=True) - finally: - await self.stop(reason="stdin_closed") + # No-op for pipe mode; resize is not supported here. + return # -------------------- Pipe fallback pumps -------------------- async def _pump_proc_stdout(self) -> None: @@ -258,11 +161,6 @@ class PowershellChannel: if self._closed: return self._closed = True - if self._pty is not None: - try: - self._pty.terminate() - except Exception: - pass if self._proc is not None: try: self._proc.terminate() diff --git a/Data/Agent/agent-requirements.txt b/Data/Agent/agent-requirements.txt index 8a152489..4a37c88f 100644 --- a/Data/Agent/agent-requirements.txt +++ b/Data/Agent/agent-requirements.txt @@ -27,7 +27,6 @@ pywinauto # Windows-based Macro Automation Library sounddevice numpy pywin32; platform_system == "Windows" -pywinpty; platform_system == "Windows" # ConPTY bridge for reverse tunnel PowerShell sessions # Ansible Libraries ansible-core diff --git a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx index ec75dc15..c798c7f7 100644 --- a/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx +++ b/Data/Engine/web-interface/src/Devices/ReverseTunnel/Powershell.jsx @@ -9,7 +9,6 @@ import { MenuItem, IconButton, Tooltip, - Alert, LinearProgress, } from "@mui/material"; import { @@ -102,8 +101,8 @@ export default function ReverseTunnelPowershell({ device }) { const [connectionType, setConnectionType] = useState("ps"); const [tunnel, setTunnel] = useState(null); const [sessionState, setSessionState] = useState("idle"); - const [statusMessage, setStatusMessage] = useState(""); - const [statusSeverity, setStatusSeverity] = useState("info"); + const [, setStatusMessage] = useState(""); + const [, setStatusSeverity] = useState("info"); const [output, setOutput] = useState(""); const [input, setInput] = useState(""); const [copyFlash, setCopyFlash] = useState(false); @@ -242,14 +241,8 @@ export default function ReverseTunnelPowershell({ device }) { setPolling(true); pollTimerRef.current = setTimeout(async () => { const resp = await emitAsync(socket, "ps_poll", {}); - if (resp?.error) { - if (resp.error === "ps_unsupported") { - setStatusSeverity("info"); - setStatusMessage("PowerShell channel warming up..."); - } else { - setStatusSeverity("warning"); - setStatusMessage(resp.error); - } + if (resp?.error) { + // Suppress warming/errors in UI; rely on session chips. } if (Array.isArray(resp?.output) && resp.output.length) { appendOutput(resp.output.join("")); @@ -375,11 +368,9 @@ export default function ReverseTunnelPowershell({ device }) { const dims = measureTerminal(); const openResp = await emitAsync(socket, "ps_open", dims); if (openResp?.error && openResp.error === "ps_unsupported") { - setStatusSeverity("info"); - setStatusMessage("PowerShell channel warming up..."); + // Suppress warming message; channel will settle once agent attaches. } appendOutput(""); - setStatusMessage("Attached — waiting for agent to acknowledge..."); setSessionState("waiting_agent"); pollLoop(socket, lease.tunnel_id); handleResize(); @@ -391,7 +382,7 @@ export default function ReverseTunnelPowershell({ device }) { const requestTunnel = useCallback(async () => { if (tunnel && sessionState !== "closed" && sessionState !== "idle") { setStatusSeverity("info"); - setStatusMessage("Re-attaching to existing tunnel..."); + setStatusMessage(""); connectSocket(tunnel); return; } @@ -407,8 +398,8 @@ export default function ReverseTunnelPowershell({ device }) { } resetState(); setSessionState("requesting"); - setStatusSeverity("info"); - setStatusMessage("Requesting tunnel lease..."); + setStatusSeverity("info"); + setStatusMessage(""); try { const resp = await fetch("/api/tunnel/request", { method: "POST", @@ -420,21 +411,17 @@ export default function ReverseTunnelPowershell({ device }) { const err = data?.error || `HTTP ${resp.status}`; setSessionState("error"); setStatusSeverity(err === "domain_limit" ? "warning" : "error"); - setStatusMessage( - err === "domain_limit" - ? "PowerShell session already active for this agent. Try again after it closes." - : err - ); + setStatusMessage(""); return; } setTunnel(data); - setStatusMessage("Lease issued. Waiting for agent to connect..."); + setStatusMessage(""); setSessionState("lease_issued"); connectSocket(data); } catch (e) { setSessionState("error"); setStatusSeverity("error"); - setStatusMessage(e?.message || "Failed to request tunnel"); + setStatusMessage(""); } }, [agentId, connectSocket, connectionType, resetState]); @@ -448,7 +435,7 @@ export default function ReverseTunnelPowershell({ device }) { const resp = await emitAsync(socket, "ps_send", { data: payload }); if (resp?.error) { setStatusSeverity("warning"); - setStatusMessage(resp.error); + setStatusMessage(""); } }, [appendOutput, emitAsync] @@ -583,20 +570,6 @@ export default function ReverseTunnelPowershell({ device }) { - {statusMessage ? ( - - {statusMessage} - - ) : null} -