Skip to content

Commit

Permalink
Merge pull request #119 from calebstewart/feature-reflective-dotnet
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
calebstewart authored Jun 12, 2021
2 parents 274c4b6 + 44aff46 commit f74510a
Show file tree
Hide file tree
Showing 17 changed files with 582 additions and 72 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -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 }}
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
5 changes: 0 additions & 5 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ well. Pull requests are always welcome!

installation.rst
usage.rst
windows.rst
configuration.rst
modules.rst
enum.rst
Expand Down
42 changes: 41 additions & 1 deletion docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
-----------------------

Expand Down
132 changes: 132 additions & 0 deletions docs/source/windows.rst
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions pwncat/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 6 additions & 6 deletions pwncat/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
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):
Expand Down Expand Up @@ -92,6 +87,11 @@ def run(self, manager: "pwncat.manager.Manager", args):
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:
Expand Down
11 changes: 8 additions & 3 deletions pwncat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -109,7 +114,7 @@ def __init__(self):
"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,
},
Expand Down
1 change: 1 addition & 0 deletions pwncat/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __init__(self, types, source):
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.
Expand Down
2 changes: 2 additions & 0 deletions pwncat/facts/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(
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
Expand All @@ -78,6 +79,7 @@ def __init__(
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:
Expand Down
Loading

0 comments on commit f74510a

Please sign in to comment.