mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Initial modifications to make configuration refactoring work
This commit is contained in:
parent
fa18ae68fd
commit
a825d00da2
@ -1,14 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Optional
|
||||
|
||||
import pwncat.db
|
||||
import pwncat.modules
|
||||
import pwncat.platform
|
||||
import pwncat.commands
|
||||
import pwncat.config
|
||||
import pwncat.file
|
||||
import pwncat.remote
|
||||
import pwncat.tamper
|
||||
import pwncat.util
|
||||
from .config import Config
|
||||
|
||||
victim: Optional["pwncat.remote.Victim"] = None
|
||||
|
||||
config: Config = Config()
|
||||
|
163
pwncat/channel/__init__.py
Normal file
163
pwncat/channel/__init__.py
Normal file
@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
from typing import Optional, Type
|
||||
|
||||
CHANNEL_TYPES = {}
|
||||
|
||||
|
||||
class ChannelError(Exception):
|
||||
""" Raised when a channel fails to connect """
|
||||
|
||||
|
||||
class ChannelTimeout(Exception):
|
||||
""" Raised when a read times out.
|
||||
|
||||
:param data: the data read before the timeout occurred
|
||||
:type data: bytes
|
||||
"""
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
super().__init__("channel recieve timed out")
|
||||
self.data: bytes = data
|
||||
|
||||
|
||||
class Channel:
|
||||
"""
|
||||
Abstract interation with a remote victim. This class acts similarly to a
|
||||
socket object. In the common cases, it simply wraps a socket object.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, user: str, password: str, **kwargs):
|
||||
self.host: str = host
|
||||
self.port: int = port
|
||||
self.user: str = user
|
||||
self.password: str = password
|
||||
|
||||
self.peek_buffer: bytes = b""
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
def sendline(self, data: bytes, end: bytes = b"\n"):
|
||||
""" Send data followed by an ending character. If no ending
|
||||
character is specified, a new line is used. """
|
||||
|
||||
return self.send(data + end)
|
||||
|
||||
def recv(self, count: Optional[int] = None) -> bytes:
|
||||
""" Receive data from the remote shell
|
||||
|
||||
If your channel class does not implement ``peak``, a default
|
||||
implementation is provided. In this case, you can use the
|
||||
``_pop_peek`` to get available peek buffer data prior to
|
||||
reading data like a normal ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: the data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
def recvuntil(self, needle: bytes, timeout: Optional[float] = None) -> bytes:
|
||||
""" Receive data until the specified string of bytes is bytes
|
||||
is found. The needle is not stripped from the data.
|
||||
|
||||
:param needle: the bytes to wait for
|
||||
:type needle: bytes
|
||||
:param timeout: a timeout in seconds (default: 30s)
|
||||
:type timeout: Optional[float]
|
||||
:return: the bytes that were read
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
if timeout is None:
|
||||
timeout = 30
|
||||
|
||||
data = b""
|
||||
time_end = time.time() + timeout
|
||||
|
||||
# We read one byte at a time so we don't overshoot the goal
|
||||
while not data.endswith(needle):
|
||||
|
||||
# Check if we have timed out
|
||||
if time.time() >= time_end:
|
||||
raise ChannelTimeout(data)
|
||||
|
||||
next_byte = self.recv(1)
|
||||
|
||||
if next_byte is not None:
|
||||
data += next_byte
|
||||
|
||||
return data
|
||||
|
||||
def recvline(self, timeout: Optional[float] = None) -> bytes:
|
||||
""" Recieve data until a newline is received. The newline
|
||||
is not stripped. """
|
||||
|
||||
return self.recvuntil(b"\n", timeout=timeout)
|
||||
|
||||
def peek(self, count: Optional[int] = None):
|
||||
""" Receive data from the remote shell and leave
|
||||
the data in the recv buffer.
|
||||
|
||||
There is a default implementation for this method which will
|
||||
utilize ``recv`` to get data, and buffer it. If the default
|
||||
``peek`` implementation is used, ``recv`` should read from
|
||||
``self.peek_buffer`` prior to calling the underlying ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
# Grab any already buffered data
|
||||
if self.peek_buffer:
|
||||
data = self.peek_buffer
|
||||
else:
|
||||
data = b""
|
||||
|
||||
# Check for more data within our count
|
||||
if len(data) < count:
|
||||
self.peek_buffer = b""
|
||||
data += self.recv(count - len(data))
|
||||
self.peek_buffer = data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def register(name: str, channel_class):
|
||||
"""
|
||||
Register a new channel class with ``pwncat``.
|
||||
|
||||
:param name: the name which this channel will be referenced by.
|
||||
:type name: str
|
||||
:param channel_class: A class object implementing the channel
|
||||
interface.
|
||||
"""
|
||||
|
||||
CHANNEL_TYPES[name] = channel_class
|
||||
|
||||
|
||||
def find(name: str) -> Type[Channel]:
|
||||
"""
|
||||
Retrieve the channel class for the specified name.
|
||||
|
||||
:param name: the name of the channel you'd like
|
||||
:type name: str
|
||||
:return: the channel class
|
||||
:rtype: Channel Class Object
|
||||
"""
|
||||
|
||||
return CHANNEL_TYPES[name]
|
||||
|
||||
|
||||
# Import default channel types and register them
|
||||
from pwncat.channel.bind import Bind
|
||||
from pwncat.channel.connect import Connect
|
||||
from pwncat.channel.ssh import Ssh
|
||||
|
||||
register("bind", Bind)
|
||||
register("connect", Connect)
|
||||
register("ssh", Ssh)
|
104
pwncat/channel/bind.py
Normal file
104
pwncat/channel/bind.py
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
from rich.progress import BarColumn, Progress
|
||||
|
||||
from pwncat.channel import Channel, ChannelError
|
||||
|
||||
|
||||
class Bind(Channel):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel will listen for incoming
|
||||
connections on the specified port, and assume the resulting
|
||||
connection is a shell from the victim.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, user: str, password: str, **kwargs):
|
||||
super().__init__(host, port, user, password)
|
||||
|
||||
if not host or host == "":
|
||||
host = "0.0.0.0"
|
||||
|
||||
if port is None:
|
||||
raise ChannelError(f"no port specified")
|
||||
|
||||
with Progress(
|
||||
f"bound to [blue]{host}[/blue]:[cyan]{port}[/cyan]",
|
||||
BarColumn(bar_width=None),
|
||||
transient=True,
|
||||
) as progress:
|
||||
task_id = progress.add_task("listening", total=1, start=False)
|
||||
# Create the socket server
|
||||
server = socket.create_server((host, port), reuse_port=True)
|
||||
|
||||
try:
|
||||
# Wait for a connection
|
||||
(client, address) = server.accept()
|
||||
except KeyboardInterrupt:
|
||||
raise ChannelError("listener aborted")
|
||||
|
||||
progress.update(task_id, visible=False)
|
||||
progress.log(
|
||||
f"[green]received[/green] connection from [blue]{address[0]}[/blue]:[cyan]{address[1]}[/cyan]"
|
||||
)
|
||||
|
||||
self.client = client
|
||||
self.address = address
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
self.client.sendall(data)
|
||||
|
||||
return len(data)
|
||||
|
||||
def recv(self, count: Optional[int] = None) -> bytes:
|
||||
""" Receive data from the remote shell
|
||||
|
||||
If your channel class does not implement ``peak``, a default
|
||||
implementation is provided. In this case, you can use the
|
||||
``_pop_peek`` to get available peek buffer data prior to
|
||||
reading data like a normal ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: the data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
return self.client.recv(count)
|
||||
|
||||
def recvuntil(self, needle: bytes) -> bytes:
|
||||
""" Receive data until the specified string of bytes is bytes
|
||||
is found. The needle is not stripped from the data. """
|
||||
|
||||
data = b""
|
||||
|
||||
# We read one byte at a time so we don't overshoot the goal
|
||||
while not data.endswith(needle):
|
||||
next_byte = self.recv(1)
|
||||
|
||||
if next_byte is not None:
|
||||
data += next_byte
|
||||
|
||||
return data
|
||||
|
||||
def peek(self, count: Optional[int] = None):
|
||||
""" Receive data from the remote shell and leave
|
||||
the data in the recv buffer.
|
||||
|
||||
There is a default implementation for this method which will
|
||||
utilize ``recv`` to get data, and buffer it. If the default
|
||||
``peek`` implementation is used, ``recv`` should read from
|
||||
``self.peek_buffer`` prior to calling the underlying ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
return self.client.recv(count, socket.MSG_PEEK)
|
99
pwncat/channel/connect.py
Normal file
99
pwncat/channel/connect.py
Normal file
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
from rich.progress import BarColumn, Progress
|
||||
|
||||
from pwncat.channel import Channel, ChannelError
|
||||
|
||||
|
||||
class Connect(Channel):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel will listen for incoming
|
||||
connections on the specified port, and assume the resulting
|
||||
connection is a shell from the victim.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, user: str, password: str, **kwargs):
|
||||
super().__init__(host, port, user, password)
|
||||
|
||||
if not host:
|
||||
raise ChannelError("no host address provided")
|
||||
|
||||
if port is None:
|
||||
raise ChannelError("no port provided")
|
||||
|
||||
with Progress(
|
||||
f"connecting to [blue]{host}[/blue]:[cyan]{port}[/cyan]",
|
||||
BarColumn(bar_width=None),
|
||||
transient=True,
|
||||
) as progress:
|
||||
task_id = progress.add_task("connecting", total=1, start=False)
|
||||
# Connect to the remote host
|
||||
client = socket.create_connection((host, port))
|
||||
|
||||
progress.update(task_id, visible=False)
|
||||
progress.log(
|
||||
f"connection to "
|
||||
f"[blue]{host}[/blue]:[cyan]{port}[/cyan] [green]established[/green]"
|
||||
)
|
||||
|
||||
self.client = client
|
||||
self.address = (host, port)
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
self.client.sendall(data)
|
||||
|
||||
return len(data)
|
||||
|
||||
def recv(self, count: Optional[int] = None) -> bytes:
|
||||
""" Receive data from the remote shell
|
||||
|
||||
If your channel class does not implement ``peak``, a default
|
||||
implementation is provided. In this case, you can use the
|
||||
``_pop_peek`` to get available peek buffer data prior to
|
||||
reading data like a normal ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: the data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
return self.client.recv(count)
|
||||
|
||||
def recvuntil(self, needle: bytes) -> bytes:
|
||||
""" Receive data until the specified string of bytes is bytes
|
||||
is found. The needle is not stripped from the data. """
|
||||
|
||||
data = b""
|
||||
|
||||
# We read one byte at a time so we don't overshoot the goal
|
||||
while not data.endswith(needle):
|
||||
next_byte = self.recv(1)
|
||||
|
||||
if next_byte is not None:
|
||||
data += next_byte
|
||||
|
||||
return data
|
||||
|
||||
def peek(self, count: Optional[int] = None):
|
||||
""" Receive data from the remote shell and leave
|
||||
the data in the recv buffer.
|
||||
|
||||
There is a default implementation for this method which will
|
||||
utilize ``recv`` to get data, and buffer it. If the default
|
||||
``peek`` implementation is used, ``recv`` should read from
|
||||
``self.peek_buffer`` prior to calling the underlying ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
return self.client.recv(count, socket.MSG_PEEK)
|
104
pwncat/channel/reconnect.py
Normal file
104
pwncat/channel/reconnect.py
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Optional
|
||||
|
||||
from rich.progress import BarColumn, Progress
|
||||
|
||||
from pwncat.channel import Channel, ChannelError
|
||||
import pwncat.modules
|
||||
|
||||
|
||||
class Reconnect(Channel):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel will listen for incoming
|
||||
connections on the specified port, and assume the resulting
|
||||
connection is a shell from the victim.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, user: str, password: str, **kwargs):
|
||||
super().__init__(host, port, user, password)
|
||||
|
||||
if not host or host == "":
|
||||
host = "0.0.0.0"
|
||||
|
||||
if port is None:
|
||||
raise ChannelError(f"no port specified")
|
||||
|
||||
with Progress(
|
||||
f"bound to [blue]{host}[/blue]:[cyan]{port}[/cyan]",
|
||||
BarColumn(bar_width=None),
|
||||
transient=True,
|
||||
) as progress:
|
||||
task_id = progress.add_task("listening", total=1, start=False)
|
||||
# Create the socket server
|
||||
server = socket.create_server((host, port), reuse_port=True)
|
||||
|
||||
try:
|
||||
# Wait for a connection
|
||||
(client, address) = server.accept()
|
||||
except KeyboardInterrupt:
|
||||
raise ChannelError("listener aborted")
|
||||
|
||||
progress.update(task_id, visible=False)
|
||||
progress.log(
|
||||
f"[green]received[/green] connection from [blue]{address[0]}[/blue]:[cyan]{address[1]}[/cyan]"
|
||||
)
|
||||
|
||||
self.client = client
|
||||
self.address = address
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
self.client.sendall(data)
|
||||
|
||||
return len(data)
|
||||
|
||||
def recv(self, count: Optional[int] = None) -> bytes:
|
||||
""" Receive data from the remote shell
|
||||
|
||||
If your channel class does not implement ``peak``, a default
|
||||
implementation is provided. In this case, you can use the
|
||||
``_pop_peek`` to get available peek buffer data prior to
|
||||
reading data like a normal ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: the data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
return self.client.recv(count)
|
||||
|
||||
def recvuntil(self, needle: bytes) -> bytes:
|
||||
""" Receive data until the specified string of bytes is bytes
|
||||
is found. The needle is not stripped from the data. """
|
||||
|
||||
data = b""
|
||||
|
||||
# We read one byte at a time so we don't overshoot the goal
|
||||
while not data.endswith(needle):
|
||||
next_byte = self.recv(1)
|
||||
|
||||
if next_byte is not None:
|
||||
data += next_byte
|
||||
|
||||
return data
|
||||
|
||||
def peek(self, count: Optional[int] = None):
|
||||
""" Receive data from the remote shell and leave
|
||||
the data in the recv buffer.
|
||||
|
||||
There is a default implementation for this method which will
|
||||
utilize ``recv`` to get data, and buffer it. If the default
|
||||
``peek`` implementation is used, ``recv`` should read from
|
||||
``self.peek_buffer`` prior to calling the underlying ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
return self.client.recv(count, socket.MSG_PEEK)
|
136
pwncat/channel/ssh.py
Normal file
136
pwncat/channel/ssh.py
Normal file
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
import paramiko
|
||||
from prompt_toolkit import prompt
|
||||
|
||||
from pwncat.channel import Channel, ChannelError
|
||||
|
||||
|
||||
class Ssh(Channel):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel will listen for incoming
|
||||
connections on the specified port, and assume the resulting
|
||||
connection is a shell from the victim.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
user: str,
|
||||
password: str,
|
||||
identity: str = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(host, port, user, password)
|
||||
|
||||
if port is None:
|
||||
port = 22
|
||||
|
||||
if not user or user is None:
|
||||
raise ChannelError("you must specify a user")
|
||||
|
||||
if password is None and identity is None:
|
||||
password = prompt("Password: ", is_password=True)
|
||||
|
||||
try:
|
||||
# Connect to the remote host's ssh server
|
||||
sock = socket.create_connection((host, port))
|
||||
except Exception as exc:
|
||||
raise ChannelError(str(exc))
|
||||
|
||||
# Create a paramiko SSH transport layer around the socket
|
||||
t = paramiko.Transport(sock)
|
||||
try:
|
||||
t.start_client()
|
||||
except paramiko.SSHException:
|
||||
sock.close()
|
||||
raise ChannelError("ssh negotiation failed")
|
||||
|
||||
if identity is not None:
|
||||
try:
|
||||
# Load the private key for the user
|
||||
key = paramiko.RSAKey.from_private_key_file(identity)
|
||||
except:
|
||||
password = prompt("RSA Private Key Passphrase: ", is_password=True)
|
||||
try:
|
||||
key = paramiko.RSAKey.from_private_key_file(identity, password)
|
||||
except:
|
||||
raise ChannelError("invalid private key or passphrase")
|
||||
|
||||
# Attempt authentication
|
||||
try:
|
||||
t.auth_publickey(user, key)
|
||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||
raise ChannelError(str(exc))
|
||||
else:
|
||||
try:
|
||||
t.auth_password(user, password)
|
||||
except paramiko.ssh_exception.AuthenticationException as exc:
|
||||
raise ChannelError(str(exc))
|
||||
|
||||
if not t.is_authenticated():
|
||||
t.close()
|
||||
sock.close()
|
||||
raise ChannelError("authentication failed")
|
||||
|
||||
# Open an interactive session
|
||||
chan = t.open_session()
|
||||
chan.get_pty()
|
||||
chan.invoke_shell()
|
||||
|
||||
self.client = chan
|
||||
self.address = (host, port)
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
self.client.sendall(data)
|
||||
|
||||
return len(data)
|
||||
|
||||
def recv(self, count: Optional[int] = None) -> bytes:
|
||||
""" Receive data from the remote shell
|
||||
|
||||
If your channel class does not implement ``peak``, a default
|
||||
implementation is provided. In this case, you can use the
|
||||
``_pop_peek`` to get available peek buffer data prior to
|
||||
reading data like a normal ``recv``.
|
||||
|
||||
:param count: maximum number of bytes to receive (default: unlimited)
|
||||
:type count: int
|
||||
:return: the data that was received
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
if self.peek_buffer:
|
||||
data = self.peek_buffer[:count]
|
||||
self.peek_buffer = self.peek_buffer[count:]
|
||||
|
||||
if len(data) >= count:
|
||||
return data
|
||||
else:
|
||||
data = b""
|
||||
|
||||
data += self.client.recv(count - len(data))
|
||||
|
||||
return data
|
||||
|
||||
def recvuntil(self, needle: bytes) -> bytes:
|
||||
""" Receive data until the specified string of bytes is bytes
|
||||
is found. The needle is not stripped from the data. """
|
||||
|
||||
data = b""
|
||||
|
||||
# We read one byte at a time so we don't overshoot the goal
|
||||
while not data.endswith(needle):
|
||||
next_byte = self.recv(1)
|
||||
|
||||
if next_byte is not None:
|
||||
data += next_byte
|
||||
|
||||
return data
|
@ -15,7 +15,7 @@ from pwncat.modules import BaseModule
|
||||
|
||||
|
||||
def key_type(value: str) -> bytes:
|
||||
""" Converts a key name to a ansi keycode. The value can either be a single
|
||||
""" Converts a key name to a ansi keycode. The value can either be a single
|
||||
printable character or a named key from prompt_toolkit Keys """
|
||||
if len(value) == 1:
|
||||
return value.encode("utf-8")
|
||||
|
@ -1,18 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
from enum import Flag, auto
|
||||
from typing import List, Dict, Optional, Tuple, Generator
|
||||
import enum
|
||||
import dataclasses
|
||||
|
||||
|
||||
class Platform(Flag):
|
||||
class Platform(enum.Flag):
|
||||
|
||||
UNKNOWN = auto()
|
||||
WINDOWS = auto()
|
||||
BSD = auto()
|
||||
LINUX = auto()
|
||||
UNKNOWN = enum.auto()
|
||||
WINDOWS = enum.auto()
|
||||
BSD = enum.auto()
|
||||
LINUX = enum.auto()
|
||||
# This deserves some explanation.
|
||||
# This indicates that component of pwncat does not need an
|
||||
# actively connected host to be utilized. When used as a
|
||||
# module platform, it indicates that the module itself
|
||||
# only deals with the database or internal pwncat features.
|
||||
# and is allowed to run prior to a victim being connected.
|
||||
NO_HOST = auto()
|
||||
NO_HOST = enum.auto()
|
||||
ANY = WINDOWS | BSD | LINUX
|
||||
|
||||
|
||||
class CalledProcessError(Exception):
|
||||
""" Raised when a process exits with a non-zero return code.
|
||||
This class largely mirrors ``subprocess.CalledProcessError`` class.
|
||||
"""
|
||||
|
||||
def __init__(self, returncode: int, args: List[str], stdout: bytes):
|
||||
super().__init__(f"Process Exited with Code {returncode}")
|
||||
|
||||
self.returncode = returncode
|
||||
self.cmd = args
|
||||
self.stdout = stdout
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
return self.stdout
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CompletedProcess:
|
||||
""" Represents the results of a process run on the remote system.
|
||||
This largely mirrors the ``subprocess.CompletedProcess`` class.
|
||||
"""
|
||||
|
||||
args: List[str]
|
||||
returncode: int
|
||||
stdout: bytes
|
||||
|
||||
def check_returncode(self):
|
||||
""" If ``returncode`` is none-zero, raise a CalledProcessError """
|
||||
if self.returncode != 0:
|
||||
raise CalledProcessError(self.returncode, self.args, self.stdout)
|
||||
|
||||
|
||||
class StreamType(enum.Enum):
|
||||
""" The type of stream supplied for the stdout, stderr or stdin arguments.
|
||||
"""
|
||||
|
||||
PIPE = enum.auto()
|
||||
DEVNULL = enum.auto()
|
||||
|
||||
|
||||
class Pipe:
|
||||
""" File-like object connecting to a pipe on the victim host """
|
||||
|
||||
def read(self, count: int = None) -> bytes:
|
||||
""" Read data """
|
||||
|
||||
def write(self, data: bytes) -> int:
|
||||
""" Write data """
|
||||
|
||||
def close(self):
|
||||
""" Close the pipe """
|
||||
|
||||
def isatty(self) -> bool:
|
||||
""" Check if this stream is a tty """
|
||||
return False
|
||||
|
||||
def readable(self) -> bool:
|
||||
""" Check if the stream is readable """
|
||||
|
||||
def writeable(self) -> bool:
|
||||
""" Check if the stream is writable """
|
||||
|
||||
def seekable(self) -> bool:
|
||||
""" Remote streams are not seekable """
|
||||
return False
|
||||
|
||||
|
||||
class Popen:
|
||||
""" Wraps running a process on the remote host. """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: List[str],
|
||||
env: Dict[str, str] = None,
|
||||
stdout: str = None,
|
||||
stderr: str = None,
|
||||
stdin: str = None,
|
||||
shell: bool = False,
|
||||
cwd: str = None,
|
||||
):
|
||||
|
||||
return
|
||||
|
||||
def poll() -> Optional[int]:
|
||||
""" Check if the process has completed """
|
||||
|
||||
def communicate(self, input: bytes = None, timeout: float = None):
|
||||
""" Send data to the remote process and collect the output. """
|
||||
|
||||
def terminate(self):
|
||||
""" Kill the remote process. This is sometimes not possible. """
|
||||
|
||||
def kill(self):
|
||||
""" Kill the remote process. """
|
||||
|
||||
|
||||
class _Platform:
|
||||
""" Abstracts interactions with a target of a specific platform.
|
||||
This includes running commands, changing directories, locating
|
||||
binaries, etc.
|
||||
|
||||
:param channel: an open a channel with the specified platform
|
||||
:type channel: pwncat.channel.Channel
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, channel: "pwncat.channel.Channel"):
|
||||
self.channel = channel
|
||||
|
||||
def run(
|
||||
self,
|
||||
args: List[str],
|
||||
env: Dict[str, str] = None,
|
||||
stdout: str = None,
|
||||
stderr: str = None,
|
||||
stdin: str = None,
|
||||
shell: bool = False,
|
||||
cwd: str = None,
|
||||
) -> Tuple[bytes, bytes, bytes]:
|
||||
""" Run the given command on the remote host. A tuple of three bytearrays
|
||||
is returned. These bytes are delimeters for the sections of output. The
|
||||
first delimeter is output before the command runs. The second is output
|
||||
after the command finishes, and the last is output after the return code
|
||||
is printed. """
|
||||
|
||||
def chdir(self, path):
|
||||
""" Change the current working directory on the victim.
|
||||
This tracks directory changes on a stack allowing you to
|
||||
using ``pwncat.victim.popd()`` to return. """
|
||||
|
||||
def listdir(self, path=None) -> Generator[str, None, None]:
|
||||
""" List the contents of a directory. If ``path`` is None,
|
||||
then the contents of the current directory is listed. The
|
||||
list is not guaranteed to be sorted in any way.
|
||||
|
||||
:param path: the directory to list
|
||||
:type path: str or Path-like
|
||||
:raise FileNotFoundError: When the requested directory is not a directory,
|
||||
does not exist, or you do not have execute permissions.
|
||||
"""
|
||||
|
165
pwncat/remote/subprocess.py
Normal file
165
pwncat/remote/subprocess.py
Normal file
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import List, IO, Optional
|
||||
from subprocess import (
|
||||
CompletedProcess,
|
||||
SubprocessError,
|
||||
TimeoutExpired,
|
||||
CalledProcessError,
|
||||
)
|
||||
import io
|
||||
|
||||
import pwncat
|
||||
|
||||
DEVNULL = 0
|
||||
""" Redirect to/from /dev/null or equivalent """
|
||||
PIPE = 1
|
||||
""" Retrieve data via a Pipe """
|
||||
|
||||
|
||||
class PopenBase:
|
||||
""" Base class for Popen objects defining the interface.
|
||||
Individual platforms will subclass this object to implement
|
||||
the correct logic. This is an abstract class. """
|
||||
|
||||
stdin: IO
|
||||
"""
|
||||
If the stdin argument was PIPE, this attribute is a writeable
|
||||
stream object as returned by open(). If the encoding or errors
|
||||
arguments were specified or the universal_newlines argument was
|
||||
True, the stream is a text stream, otherwise it is a byte
|
||||
stream. If the stdin argument was not PIPE, this attribute is
|
||||
None.
|
||||
"""
|
||||
stdout: IO
|
||||
"""
|
||||
If the stdout argument was PIPE, this attribute is a readable
|
||||
stream object as returned by open(). Reading from the stream
|
||||
provides output from the child process. If the encoding or
|
||||
errors arguments were specified or the universal_newlines
|
||||
argument was True, the stream is a text stream, otherwise it
|
||||
is a byte stream. If the stdout argument was not PIPE, this
|
||||
attribute is None.
|
||||
"""
|
||||
stderr: IO
|
||||
"""
|
||||
If the stderr argument was PIPE, this attribute is a readable
|
||||
stream object as returned by open(). Reading from the stream
|
||||
provides error output from the child process. If the encoding
|
||||
or errors arguments were specified or the universal_newlines
|
||||
argument was True, the stream is a text stream, otherwise it
|
||||
is a byte stream. If the stderr argument was not PIPE, this
|
||||
attribute is None.
|
||||
"""
|
||||
args: List[str]
|
||||
"""
|
||||
The args argument as it was passed to Popen – a sequence of
|
||||
program arguments or else a single string.
|
||||
"""
|
||||
pid: int
|
||||
""" The process ID of the child process. """
|
||||
returncode: int
|
||||
"""
|
||||
The child return code, set by poll() and wait() (and indirectly by
|
||||
communicate()). A None value indicates that the process hasn’t
|
||||
terminated yet.
|
||||
"""
|
||||
|
||||
def poll(self) -> Optional[int]:
|
||||
""" Check if the child process has terminated. Set and return
|
||||
``returncode`` attribute. Otherwise, returns None. """
|
||||
|
||||
def wait(self, timeout: float = None) -> int:
|
||||
""" Wait for child process to terminate. Set and return
|
||||
``returncode`` attribute.
|
||||
|
||||
If the process does not terminate after ``timeout`` seconds,
|
||||
raise a ``TimeoutExpired`` exception. It is safe to catch
|
||||
this exception and retry the wait.
|
||||
"""
|
||||
|
||||
def communicate(self, input: bytes = None, timeout: float = None):
|
||||
""" Interact with process: Send data to stdin. Read data from stdout
|
||||
and stderr, until end-of-file is readched. Wait for the process to
|
||||
terminate and set the ``returncode`` attribute. The optional ``input``
|
||||
argument should be data to be sent to the child process, or None, if
|
||||
no data should be sent to the child. If streams were opened in text mode,
|
||||
``input`` must be a string. Otherwise, it must be ``bytes``. """
|
||||
|
||||
def send_signal(self, signal: int):
|
||||
""" Sends the signal ``signal`` to the child.
|
||||
|
||||
Does nothing if the process completed.
|
||||
"""
|
||||
|
||||
def terminate(self):
|
||||
""" Stop the child. """
|
||||
|
||||
def kill(self):
|
||||
""" Kills the child """
|
||||
|
||||
|
||||
def Popen(*args, **kwargs) -> PopenBase:
|
||||
""" Wrapper to create a new popen object. Deligates to
|
||||
the current victim's platform ``popen`` method. """
|
||||
|
||||
return pwncat.victim.popen(*args, **kwargs)
|
||||
|
||||
|
||||
def run(
|
||||
args,
|
||||
stdin=None,
|
||||
input=None,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
capture_output=False,
|
||||
shell=False,
|
||||
cwd=None,
|
||||
timeout=None,
|
||||
check=False,
|
||||
encoding=None,
|
||||
errors=None,
|
||||
text=None,
|
||||
env=None,
|
||||
universal_newlines=None,
|
||||
**other_popen_kwargs
|
||||
):
|
||||
""" Run the command described by `args`. Wait for command to complete
|
||||
and then return a CompletedProcess instance.
|
||||
|
||||
The arguments are the same as the `Popen` constructor with ``capture_output``,
|
||||
``timeout``, and ``check`` added.
|
||||
"""
|
||||
|
||||
# Ensure we capture standard output and standard error
|
||||
if capture_output:
|
||||
stdout = PIPE
|
||||
stderr = PIPE
|
||||
|
||||
# Execute the process
|
||||
proc = Popen(
|
||||
args=args,
|
||||
stdin=stdin,
|
||||
input=input,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
encoding=encoding,
|
||||
errors=errors,
|
||||
text=text,
|
||||
env=env,
|
||||
universal_newlines=universal_newlines,
|
||||
**other_popen_kwargs
|
||||
)
|
||||
|
||||
# Send input/receive output
|
||||
stdout_data, stderr_data = proc.communicate(input, timeout)
|
||||
|
||||
# Build the completed process object
|
||||
completed_proc = CompletedProcess(args, proc.returncode, stdout_data, stderr_data)
|
||||
|
||||
# Check the result
|
||||
if check:
|
||||
completed_proc.check_returncode()
|
||||
|
||||
return completed_proc
|
17
setup.py
17
setup.py
@ -52,6 +52,23 @@ dependency_links = [
|
||||
"https://github.com/JohnHammond/base64io-python/tarball/master#egg=base64io",
|
||||
]
|
||||
|
||||
# Read the requirements
|
||||
with open("requirements.txt") as filp:
|
||||
dependencies = [
|
||||
line.strip() for line in filp.readlines() if not line.startswith("#")
|
||||
]
|
||||
|
||||
# Build dependency links for entries that need them
|
||||
# This works for "git+https://github.com/user/package" refs
|
||||
dependency_links = [dep for dep in dependencies if dep.startswith("git+")]
|
||||
for i, dep in enumerate(dependency_links):
|
||||
link = dep.split("git+")[1]
|
||||
name = dep.split("/")[-1]
|
||||
dependency_links[i] = f"{link}/tarball/master#egg={name}"
|
||||
|
||||
# Strip out git+ links from dependencies
|
||||
dependencies = [dep for dep in dependencies if not dep.startswith("git+")]
|
||||
|
||||
# Setup
|
||||
setup(
|
||||
name="pwncat",
|
||||
|
Loading…
Reference in New Issue
Block a user