1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 20:34:15 +01:00

Updated windows platform for new C2 comms

This commit is contained in:
Caleb Stewart 2021-06-09 20:57:25 -04:00
parent 00c6e13c39
commit 04587bffb1
4 changed files with 162 additions and 146 deletions

View File

@ -2,14 +2,15 @@
from typing import Any, Dict, List from typing import Any, Dict, List
import pwncat
import rich.markup import rich.markup
import pwncat
from pwncat import util from pwncat import util
from pwncat.db import Fact from pwncat.db import Fact
from pwncat.modules import ModuleFailed from pwncat.modules import ModuleFailed
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.platform import PlatformError from pwncat.platform import PlatformError
from pwncat.platform.windows import PowershellError, Windows from pwncat.platform.windows import Windows, PowershellError
from pwncat.modules.enumerate import Schedule, EnumerateModule
class LSAProtectionData(Fact): class LSAProtectionData(Fact):
@ -18,17 +19,19 @@ class LSAProtectionData(Fact):
self.active: bool = active self.active: bool = active
def title(self, session): def title(self, session):
out = "LSA Protection is " out = "LSA Protection is "
out += "[bold red]active[/bold red]" if self.active else "[bold green]inactive[/bold green]" out += (
"[bold red]active[/bold red]"
if self.active
else "[bold green]inactive[/bold green]"
)
return out return out
def description(self, session): def description(self, session):
return None return None
class Module(EnumerateModule): class Module(EnumerateModule):
"""Enumerate the current Windows Defender settings on the target""" """Enumerate the current Windows Defender settings on the target"""
@ -37,11 +40,9 @@ class Module(EnumerateModule):
def enumerate(self, session): def enumerate(self, session):
registry_value = "RunAsPPL" registry_value = "RunAsPPL"
registry_key = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\LSA" registry_key = "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\LSA"
try: try:
result = session.platform.powershell( result = session.platform.powershell(
f"Get-ItemPropertyValue {registry_key} -Name {registry_value}" f"Get-ItemPropertyValue {registry_key} -Name {registry_value}"
@ -55,7 +56,7 @@ class Module(EnumerateModule):
status = bool(result[0]) status = bool(result[0])
except PowershellError as exc: except PowershellError as exc:
if "does not exist" in exc.errors[0]["Message"]: if "does not exist" in exc.message:
status = bool(0) # default status = bool(0) # default
else: else:
raise ModuleFailed( raise ModuleFailed(

View File

@ -2,14 +2,15 @@
from typing import Any, Dict, List from typing import Any, Dict, List
import pwncat
import rich.markup import rich.markup
import pwncat
from pwncat import util from pwncat import util
from pwncat.db import Fact from pwncat.db import Fact
from pwncat.modules import ModuleFailed from pwncat.modules import ModuleFailed
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.platform import PlatformError from pwncat.platform import PlatformError
from pwncat.platform.windows import PowershellError, Windows from pwncat.platform.windows import Windows, PowershellError
from pwncat.modules.enumerate import Schedule, EnumerateModule
class UACData(Fact): class UACData(Fact):
@ -117,7 +118,7 @@ class Module(EnumerateModule):
registry_values[registry_value] = registry_type(result[0]) registry_values[registry_value] = registry_type(result[0])
except PowershellError as exc: except PowershellError as exc:
if "does not exist" in exc.errors[0]["Message"]: if "does not exist" in exc.message:
registry_values[registry_value] = registry_type(0) registry_values[registry_value] = registry_type(0)
else: else:
raise ModuleFailed( raise ModuleFailed(

View File

@ -2,14 +2,15 @@
from typing import Any, Dict, List from typing import Any, Dict, List
import pwncat
import rich.markup import rich.markup
import pwncat
from pwncat import util from pwncat import util
from pwncat.db import Fact from pwncat.db import Fact
from pwncat.modules import ModuleFailed from pwncat.modules import ModuleFailed
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.platform import PlatformError from pwncat.platform import PlatformError
from pwncat.platform.windows import PowershellError, Windows from pwncat.platform.windows import Windows, PowershellError
from pwncat.modules.enumerate import Schedule, EnumerateModule
class AlwaysInstallElevatedData(Fact): class AlwaysInstallElevatedData(Fact):
@ -19,9 +20,12 @@ class AlwaysInstallElevatedData(Fact):
self.enabled: bool = enabled self.enabled: bool = enabled
self.context: str = context self.context: str = context
def title(self, session): def title(self, session):
out = "AlwaysInstallElevated is " + "[bold green]enabled[/bold green]" if self.enabled else "[red]disabled[/red]" out = (
"AlwaysInstallElevated is " + "[bold green]enabled[/bold green]"
if self.enabled
else "[red]disabled[/red]"
)
out += f" for this {self.context}" out += f" for this {self.context}"
return out return out
@ -34,14 +38,12 @@ class Module(EnumerateModule):
def enumerate(self, session): def enumerate(self, session):
registry_value = "AlwaysInstallElevated" registry_value = "AlwaysInstallElevated"
registry_keys = [ registry_keys = [
"HKCU:\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer\\", "HKCU:\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer\\",
"HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer\\" "HKLM:\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer\\",
] ]
for registry_key in registry_keys: for registry_key in registry_keys:
try: try:
result = session.platform.powershell( result = session.platform.powershell(
@ -56,14 +58,14 @@ class Module(EnumerateModule):
status = bool(result[0]) status = bool(result[0])
except PowershellError as exc: except PowershellError as exc:
if "does not exist" in exc.errors[0]["Message"]: if "does not exist" in exc.message:
status = bool(0) # default status = bool(0) # default
else: else:
raise ModuleFailed( raise ModuleFailed(
f"could not retrieve registry value {registry_value}: {exc}" f"could not retrieve registry value {registry_value}: {exc}"
) from exc ) from exc
if registry_key.startswith('HKCU'): if registry_key.startswith("HKCU"):
yield AlwaysInstallElevatedData(self.name, status, "current user") yield AlwaysInstallElevatedData(self.name, status, "current user")
else: else:
yield AlwaysInstallElevatedData(self.name, status, "local machine") yield AlwaysInstallElevatedData(self.name, status, "local machine")

View File

@ -49,16 +49,25 @@ import pwncat.subprocess
from pwncat.platform import Path, Platform, PlatformError from pwncat.platform import Path, Platform, PlatformError
INTERACTIVE_END_MARKER = b"INTERACTIVE_COMPLETE\r\n" INTERACTIVE_END_MARKER = b"INTERACTIVE_COMPLETE\r\n"
PWNCAT_WINDOWS_C2_VERSION = "v0.1.1" PWNCAT_WINDOWS_C2_VERSION = "v0.2.0"
PWNCAT_WINDOWS_C2_RELEASE_URL = "https://github.com/calebstewart/pwncat-windows-c2/releases/download/{version}/pwncat-windows-{version}.tar.gz" PWNCAT_WINDOWS_C2_RELEASE_URL = "https://github.com/calebstewart/pwncat-windows-c2/releases/download/{version}/pwncat-windows-{version}.tar.gz"
class PowershellError(Exception): class PowershellError(Exception):
"""Executing a powershell script caused an error""" """Executing a powershell script caused an error"""
def __init__(self, errors): def __init__(self, msg):
self.errors = json.loads(errors) super().__init__(msg)
super().__init__(self.errors[0]["Message"])
self.message = msg
class ProtocolError(Exception):
def __init__(self, code: int, message: str):
self.code = code
self.message = message
super().__init__(self.message)
@dataclass @dataclass
@ -116,8 +125,7 @@ class WindowsFile(RawIOBase):
if not self.is_open: if not self.is_open:
return return
self.platform.run_method("File", "close") self.platform.run_method("File", "close", self.handle)
self.platform.channel.sendline(str(self.handle).encode("utf-8"))
self.is_open = False self.is_open = False
return return
@ -140,25 +148,24 @@ class WindowsFile(RawIOBase):
if self.eof: if self.eof:
return 0 return 0
self.platform.run_method("File", "read") try:
self.platform.channel.sendline(str(self.handle).encode("utf-8")) result = self.platform.run_method("File", "read", self.handle, len(b))
self.platform.channel.sendline(str(len(b)).encode("utf-8")) except ProtocolError as exc:
count = int(self.platform.channel.recvuntil(b"\n").strip())
if count == 0: # ERROR_BROKEN_PIPE
if exc.code == 0x6D:
self.eof = True self.eof = True
return 0 return 0
n = 0 raise IOError(exc.message) from exc
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 data = base64.b64decode(result["data"])
b[: len(data)] = data
if len(data) == 0:
self.eof = True
return len(data)
def write(self, data: bytes): def write(self, data: bytes):
"""Write data to this file""" """Write data to this file"""
@ -170,16 +177,18 @@ class WindowsFile(RawIOBase):
while nwritten < len(data): while nwritten < len(data):
chunk = data[nwritten:] chunk = data[nwritten:]
payload = BytesIO() try:
with gzip.GzipFile(fileobj=payload, mode="wb") as gz: result = self.platform.run_method(
gz.write(chunk) "File", "write", self.handle, base64.b64encode(data)
self.platform.run_method("File", "write")
self.platform.channel.sendline(str(self.handle).encode("utf-8"))
self.platform.channel.sendline(base64.b64encode(payload.getbuffer()))
nwritten += int(
self.platform.channel.recvuntil(b"\n").strip().decode("utf-8")
) )
except ProtocolError as exc:
# ERROR_BROKEN_PIPE
if exc.code == 0x6D:
self.eof = True
break
raise IOError(exc.message) from exc
nwritten += result["count"]
return nwritten return nwritten
@ -200,19 +209,18 @@ class PopenWindows(pwncat.subprocess.Popen):
encoding, encoding,
errors, errors,
bufsize, bufsize,
handle, result,
stdio,
): ):
super().__init__() super().__init__()
self.platform = platform self.platform = platform
self.handle = handle self.handle = result["handle"]
self.stdio = stdio self.stdio = [result["stdin"], result["stdout"], result["stderr"]]
self.returncode = None self.returncode = None
self.stdin = WindowsFile(platform, "w", stdio[0]) self.stdin = WindowsFile(platform, "w", result["stdin"])
self.stdout = WindowsFile(platform, "r", stdio[1]) self.stdout = WindowsFile(platform, "r", result["stdout"])
self.stderr = WindowsFile(platform, "r", stdio[2]) self.stderr = WindowsFile(platform, "r", result["stderr"])
if stdout != subprocess.PIPE: if stdout != subprocess.PIPE:
self.stdout.close() self.stdout.close()
@ -266,9 +274,7 @@ class PopenWindows(pwncat.subprocess.Popen):
if self.returncode is not None: if self.returncode is not None:
return return
self.platform.run_method("Process", "kill") self.platform.run_method("Process", "kill", self.handle, 0)
self.platform.channel.sendline(str(self.handle).encode("utf-8"))
self.platform.channel.sendline(b"0")
self.returncode = -1 self.returncode = -1
def poll(self): def poll(self):
@ -277,15 +283,13 @@ class PopenWindows(pwncat.subprocess.Popen):
if self.returncode is not None: if self.returncode is not None:
return self.returncode return self.returncode
self.platform.run_method("Process", "poll") try:
self.platform.channel.sendline(str(self.handle).encode("utf-8")) result = self.platform.run_method("Process", "poll", self.handle)
result = self.platform.channel.recvuntil(b"\n").strip().decode("utf-8") except ProtocolError as exc:
raise RuntimeError(exc.message)
if result == "E": if result["stopped"]:
raise RuntimeError(f"process {self.handle}: failed to get exit status") self.returncode = result["code"] or 0
if result != "R":
self.returncode = int(result)
return self.returncode return self.returncode
def wait(self, timeout: float = None): def wait(self, timeout: float = None):
@ -430,19 +434,54 @@ class Windows(Platform):
self.run_method("StageTwo", "exit") self.run_method("StageTwo", "exit")
def run_method(self, typ: str, method: str): def parse_response(self, data: bytes):
"""Run a method reflectively from the loaded StageTwo assembly. This """ Parse a line of data from the C2 """
can technically run any .Net method, but doesn't implement a way to
abstractly pass arguments. Instead, all the StageTwo methods take with gzip.GzipFile(
arguments through stdin. fileobj=BytesIO(base64.b64decode(data.decode("utf-8").strip())),
mode="rb",
) as gz:
result = json.loads(gz.read().decode("utf-8"))
return result
def run_method(self, typ: str, method: str, *args, wait: bool = True):
"""
Execute a method within the pwncat-windows-c2. You must specify the type
and method arguments. Arguments are passed via json encoding so any valid
JSON types should be passed correctly onto the C2. Named arguments are not
supported. Results are returned as a dictionary. In the case of an error,
a ProtocolError is raised with the error code and message.
:param typ: The type name where the method you'd like to execute resides :param typ: The type name where the method you'd like to execute resides
:type typ: str :type typ: str
:param method: The name of the method you'd like to execute :param method: The name of the method you'd like to execute
:type method: str :type method: str
:param \*args: the positional arguments for the method you are calling
:type \*args: correct type for given method
""" """
self.channel.send(f"{typ}\n{method}\n".encode("utf-8")) command = [typ, method, *args]
payload = BytesIO()
# compress command arguments
with gzip.GzipFile(fileobj=payload, mode="wb") as gz:
gz.write(json.dumps(command).encode("utf-8"))
# Send the command
thing = base64.b64encode(payload.getbuffer())
self.channel.sendline(thing)
if wait:
# Receive the response
result = self.parse_response(self.channel.recvline())
# Raise an appropriate error if needed
if result["error"] != 0:
raise ProtocolError(result["error"], result.get("message", ""))
return result["result"]
def setup_prompt(self): def setup_prompt(self):
"""Set a prompt method for powershell to ensure our prompt looks pretty :)""" """Set a prompt method for powershell to ensure our prompt looks pretty :)"""
@ -679,24 +718,13 @@ function prompt {
elif not isinstance(args, str): elif not isinstance(args, str):
raise ValueError("expected command string or list of arguments") raise ValueError("expected command string or list of arguments")
self.run_method("Process", "start") try:
self.channel.sendline(args.encode("utf-8")) result = self.run_method("Process", "start", args)
except ProtocolError as exc:
hProcess = self.channel.recvuntil(b"\n").strip().decode("utf-8") if "pipe" in exc.message:
if hProcess == "E:IN": raise OSError(exc.message)
raise RuntimeError("failed to open stdin pipe") else:
if hProcess == "E:OUT": raise FileNotFoundError(exc.message)
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( return PopenWindows(
self, self,
@ -708,8 +736,7 @@ function prompt {
encoding, encoding,
errors, errors,
bufsize, bufsize,
hProcess, result,
stdio,
) )
def get_host_hash(self): def get_host_hash(self):
@ -762,15 +789,28 @@ function prompt {
# Reset the tracker # Reset the tracker
if value: if value:
self.run_method("PowerShell", "start") try:
self.run_method("PowerShell", "start", wait=False)
except ProtocolError as exc:
raise PlatformError(exc.message)
# Wait for the powershell runspace to be up and running
output = self.channel.recvline() output = self.channel.recvline()
if not output.strip().startswith(b"INTERACTIVE_START"): if not output.strip().startswith(b"INTERACTIVE_START"):
self.interactive_tracker = len(INTERACTIVE_END_MARKER) self.interactive_tracker = len(INTERACTIVE_END_MARKER)
raise PlatformError(f"no interactive start message: {output}") result = self.parse_response(output)
raise PlatformError(result["message"])
self._interactive = True self._interactive = True
self.interactive_tracker = 0 self.interactive_tracker = 0
return return
if not value: if not value:
# Receive the method response
data = self.parse_response(self.channel.recvline())
if data["error"] != 0:
self.session.log(data["message"])
self._interactive = False self._interactive = False
self.refresh_uid() self.refresh_uid()
@ -837,17 +877,12 @@ function prompt {
if "b" not in mode: if "b" not in mode:
buffering = -1 buffering = -1
self.run_method("File", "open")
self.channel.sendline(str(path).encode("utf-8"))
self.channel.sendline(mode.encode("utf-8"))
result = self.channel.recvuntil(b"\n").strip()
try: try:
handle = int(result) result = self.run_method("File", "open", path, mode)
except ValueError: except ProtocolError as exc:
raise FileNotFoundError(f"{str(path)}: {result}") raise FileNotFoundError(f"{path}: {exc.message}")
stream = WindowsFile(self, mode, handle, name=path) stream = WindowsFile(self, mode, result["handle"], name=path)
if "b" not in mode: if "b" not in mode:
stream = TextIOWrapper( stream = TextIOWrapper(
@ -1291,37 +1326,14 @@ function prompt {
:type depth: int :type depth: int
""" """
if isinstance(script, str): if not isinstance(script, str):
script = BytesIO(script.encode("utf-8")) script = script.read()
if isinstance(script, bytes):
payload = BytesIO() script = script.decode("utf-8")
with gzip.GzipFile(fileobj=payload, mode="wb") as gz:
shutil.copyfileobj(script, gz)
self.run_method("PowerShell", "run")
self.channel.sendline(base64.b64encode(payload.getbuffer()))
self.channel.sendline(str(depth).encode("utf-8"))
results = []
result = self.channel.recvline().strip()
if result.startswith(b"E:S2:EXCEPTION:"):
raise PlatformError(result.split(b"E:S2:EXCEPTION:")[1].decode("utf-8"))
# Wait for the command to complete
while result != b"DONE":
result = self.channel.recvline().strip()
try: try:
# Receive results result = self.run_method("PowerShell", "run", script, depth)
result = self.channel.recvline().strip() except ProtocolError as exc:
if result.startswith(b"E:PWSH:"): raise PowershellError(exc.message)
raise PowershellError(result.split(b"E:PWSH:")[1].decode("utf-8"))
while result != b"END":
results.append(json.loads(result))
result = self.channel.recvline().strip()
except json.JSONDecodeError as exc:
raise PlatformError(result)
return results return [json.loads(x) for x in result["output"]]