mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 10:54:14 +01:00
More module modifications for the move
This commit is contained in:
parent
c1068ad567
commit
1a2030e599
@ -468,11 +468,13 @@ def create(protocol: Optional[str] = None, **kwargs):
|
||||
|
||||
|
||||
# Import default channel types and register them
|
||||
from pwncat.channel.socket import Socket
|
||||
from pwncat.channel.bind import Bind
|
||||
from pwncat.channel.connect import Connect
|
||||
from pwncat.channel.ssh import Ssh
|
||||
from pwncat.channel.reconnect import Reconnect
|
||||
|
||||
register("socket", Socket)
|
||||
register("bind", Bind)
|
||||
register("connect", Connect)
|
||||
register("ssh", Ssh)
|
||||
|
@ -4,10 +4,11 @@ from typing import Optional
|
||||
|
||||
from rich.progress import BarColumn, Progress
|
||||
|
||||
from pwncat.channel.socket import Socket
|
||||
from pwncat.channel import Channel, ChannelError
|
||||
|
||||
|
||||
class Bind(Channel):
|
||||
class Bind(Socket):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel will listen for incoming
|
||||
@ -15,8 +16,7 @@ class Bind(Channel):
|
||||
connection is a shell from the victim.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, **kwargs):
|
||||
super().__init__(host, port, **kwargs)
|
||||
def __init__(self, port: int, host: str = None, **kwargs):
|
||||
|
||||
if not host or host == "":
|
||||
host = "0.0.0.0"
|
||||
@ -44,61 +44,4 @@ class Bind(Channel):
|
||||
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)
|
||||
super().__init__(client=client, host=host, port=port, **kwargs)
|
||||
|
@ -7,10 +7,11 @@ from typing import Optional
|
||||
|
||||
from rich.progress import BarColumn, Progress
|
||||
|
||||
from pwncat.channel.socket import Socket
|
||||
from pwncat.channel import Channel, ChannelError, ChannelClosed
|
||||
|
||||
|
||||
class Connect(Channel):
|
||||
class Connect(Socket):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel will listen for incoming
|
||||
@ -19,8 +20,6 @@ class Connect(Channel):
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int, **kwargs):
|
||||
super().__init__(host, port, **kwargs)
|
||||
|
||||
if not host:
|
||||
raise ChannelError("no host address provided")
|
||||
|
||||
@ -42,86 +41,4 @@ class Connect(Channel):
|
||||
f"[blue]{host}[/blue]:[cyan]{port}[/cyan] [green]established[/green]"
|
||||
)
|
||||
|
||||
self.client = client
|
||||
self.address = (host, port)
|
||||
|
||||
# Ensure we are non-blocking
|
||||
self.client.setblocking(False)
|
||||
fcntl.fcntl(self.client, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
try:
|
||||
written = 0
|
||||
while written < len(data):
|
||||
try:
|
||||
written += self.client.send(data[written:])
|
||||
except BlockingIOError:
|
||||
pass
|
||||
except BrokenPipeError as exc:
|
||||
raise ChannelClosed(self) from exc
|
||||
|
||||
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[len(data) :]
|
||||
count -= len(data)
|
||||
else:
|
||||
data = b""
|
||||
|
||||
try:
|
||||
return data + self.client.recv(count)
|
||||
except socket.error as exc:
|
||||
if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK:
|
||||
return data
|
||||
|
||||
raise ChannelClosed(self) from exc
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
if self.peek_buffer:
|
||||
data = self.peek_buffer[:count]
|
||||
count -= len(data)
|
||||
else:
|
||||
data = b""
|
||||
|
||||
try:
|
||||
return data + self.client.recv(count)
|
||||
except socket.error as exc:
|
||||
if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK:
|
||||
return data
|
||||
|
||||
raise ChannelClosed(self) from exc
|
||||
|
||||
def fileno(self):
|
||||
return self.client.fileno()
|
||||
super().__init__(client=client, host=host, port=port, **kwargs)
|
||||
|
115
pwncat/channel/socket.py
Normal file
115
pwncat/channel/socket.py
Normal file
@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from rich.progress import BarColumn, Progress
|
||||
|
||||
from pwncat.channel import Channel, ChannelError, ChannelClosed
|
||||
|
||||
|
||||
class Socket(Channel):
|
||||
"""
|
||||
Implements a channel which rides over a shell attached
|
||||
directly to a socket. This channel takes an existing
|
||||
socket as an argument, and allows pwncat to reuse
|
||||
an existing connection.
|
||||
"""
|
||||
|
||||
def __init__(self, client: socket.socket, **kwargs):
|
||||
|
||||
# Report host and port number to base channel
|
||||
host, port = client.getpeername()
|
||||
|
||||
if "host" not in kwargs:
|
||||
kwargs["host"] = host
|
||||
if "port" not in kwargs:
|
||||
kwargs["port"] = port
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.client = client
|
||||
self.address = (host, port)
|
||||
|
||||
# Ensure we are non-blocking
|
||||
self.client.setblocking(False)
|
||||
fcntl.fcntl(self.client, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
|
||||
def send(self, data: bytes):
|
||||
""" Send data to the remote shell. This is a blocking call
|
||||
that only returns after all data is sent. """
|
||||
|
||||
try:
|
||||
written = 0
|
||||
while written < len(data):
|
||||
try:
|
||||
written += self.client.send(data[written:])
|
||||
except BlockingIOError:
|
||||
pass
|
||||
except BrokenPipeError as exc:
|
||||
raise ChannelClosed(self) from exc
|
||||
|
||||
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[len(data) :]
|
||||
count -= len(data)
|
||||
else:
|
||||
data = b""
|
||||
|
||||
try:
|
||||
return data + self.client.recv(count)
|
||||
except socket.error as exc:
|
||||
if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK:
|
||||
return data
|
||||
|
||||
raise ChannelClosed(self) from exc
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
if self.peek_buffer:
|
||||
data = self.peek_buffer[:count]
|
||||
count -= len(data)
|
||||
else:
|
||||
data = b""
|
||||
|
||||
try:
|
||||
return data + self.client.recv(count)
|
||||
except socket.error as exc:
|
||||
if exc.args[0] == errno.EAGAIN or exc.args[0] == errno.EWOULDBLOCK:
|
||||
return data
|
||||
|
||||
raise ChannelClosed(self) from exc
|
||||
|
||||
def fileno(self):
|
||||
return self.client.fileno()
|
@ -233,6 +233,11 @@ class Manager:
|
||||
self.parser = CommandParser(self)
|
||||
self.interactive_running = False
|
||||
|
||||
# This is needed because pwntools captures the terminal...
|
||||
# there's no way officially to undo it, so this is a nasty
|
||||
# hack. You can't use pwntools output after creating a manager.
|
||||
self._patch_pwntools()
|
||||
|
||||
# Load standard modules
|
||||
self.load_modules(*pwncat.modules.__path__)
|
||||
|
||||
@ -348,6 +353,28 @@ class Manager:
|
||||
raise ValueError("invalid target")
|
||||
self._target = value
|
||||
|
||||
def _patch_pwntools(self):
|
||||
""" This method patches stdout and stdin and sys.exchook
|
||||
back to their original contents temporarily in order to
|
||||
interact properly with pwntools. You must complete all
|
||||
pwntools progress items before calling this. It attempts to
|
||||
remove all the hooks placed into stdio by pwntools. """
|
||||
|
||||
pwnlib = None
|
||||
|
||||
# We only run this if pwnlib is loaded
|
||||
if "pwnlib" in sys.modules:
|
||||
pwnlib = sys.modules["pwnlib"]
|
||||
|
||||
if pwnlib is None or not pwnlib.term.term_mode:
|
||||
return
|
||||
|
||||
sys.stdout = sys.stdout._fd
|
||||
sys.stdin = sys.stdin._fd
|
||||
# I don't know how to get the old hook back...
|
||||
sys.excepthook = lambda _, __, ___: None
|
||||
pwnlib.term.term_mode = False
|
||||
|
||||
def interactive(self):
|
||||
""" Start interactive prompt """
|
||||
|
||||
|
@ -17,9 +17,9 @@ class FileCapabilityData:
|
||||
""" List of strings representing the capabilities (e.g. "cap_net_raw+ep") """
|
||||
|
||||
def __str__(self):
|
||||
line = f"[cyan]{self.path}[/cyan] -> [["
|
||||
line = f"[cyan]{self.path}[/cyan] -> ["
|
||||
line += ",".join(f"[blue]{c}[/blue]" for c in self.caps)
|
||||
line += "]]"
|
||||
line += "]"
|
||||
return line
|
||||
|
||||
|
||||
@ -29,17 +29,21 @@ class Module(EnumerateModule):
|
||||
PROVIDES = ["file.caps"]
|
||||
PLATFORM = [Linux]
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
|
||||
# Spawn a find command to locate the setuid binaries
|
||||
with pwncat.victim.subprocess(
|
||||
["getcap", "-r", "/"], stderr="/dev/null", mode="r", no_job=True,
|
||||
) as stream:
|
||||
proc = session.platform.Popen(
|
||||
["getcap", "-r", "/"],
|
||||
stderr=pwncat.subprocess.DEVNULL,
|
||||
stdout=pwncat.subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Process the standard output from the command
|
||||
with proc.stdout as stream:
|
||||
for path in stream:
|
||||
# Parse out owner ID and path
|
||||
path, caps = [
|
||||
x.strip() for x in path.strip().decode("utf-8").split(" = ")
|
||||
]
|
||||
# Parse out path and capability list
|
||||
path, caps = [x.strip() for x in path.strip().split(" = ")]
|
||||
caps = caps.split(",")
|
||||
|
||||
yield "file.caps", FileCapabilityData(path, caps)
|
||||
|
@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import dataclasses
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat import util
|
||||
from pwncat.modules import Status
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@ -15,7 +17,7 @@ class Binary:
|
||||
|
||||
path: str
|
||||
""" The path to the binary """
|
||||
uid: int
|
||||
owner: "pwncat.db.User"
|
||||
""" The owner of the binary """
|
||||
|
||||
def __str__(self):
|
||||
@ -23,8 +25,8 @@ class Binary:
|
||||
return f"[cyan]{self.path}[/cyan] owned by [{color}]{self.owner.name}[/{color}]"
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return pwncat.victim.find_user_by_id(self.uid)
|
||||
def uid(self):
|
||||
return self.owner.id
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
@ -34,18 +36,26 @@ class Module(EnumerateModule):
|
||||
PLATFORM = [Linux]
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session: "pwncat.manager.Session"):
|
||||
|
||||
# Spawn a find command to locate the setuid binaries
|
||||
with pwncat.victim.subprocess(
|
||||
proc = session.platform.Popen(
|
||||
["find", "/", "-perm", "-4000", "-printf", "%U %p\\n"],
|
||||
stderr="/dev/null",
|
||||
mode="r",
|
||||
no_job=True,
|
||||
) as stream:
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
facts = []
|
||||
with proc.stdout as stream:
|
||||
for path in stream:
|
||||
# Parse out owner ID and path
|
||||
path = path.strip().decode("utf-8").split(" ")
|
||||
path = path.strip().split(" ")
|
||||
uid, path = int(path[0]), " ".join(path[1:])
|
||||
|
||||
yield "file.suid", Binary(path, uid)
|
||||
facts.append(Binary(path, uid))
|
||||
yield Status(path)
|
||||
|
||||
for fact in facts:
|
||||
fact.owner = session.platform.find_user(id=fact.owner)
|
||||
yield "file.suid", fact
|
||||
|
@ -407,6 +407,7 @@ class Platform:
|
||||
self.logger = logging.getLogger(str(channel))
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.name = "unknown"
|
||||
self._current_user = None
|
||||
|
||||
# output log to a file
|
||||
if log is not None:
|
||||
@ -492,10 +493,20 @@ class Platform:
|
||||
|
||||
return user
|
||||
|
||||
def update_user(self):
|
||||
""" Force an update of the current user the next time it is requested. """
|
||||
|
||||
self._current_user = None
|
||||
|
||||
def current_user(self):
|
||||
""" Retrieve a user object for the current user """
|
||||
|
||||
return self.find_user(name=self.whoami())
|
||||
if self._current_user is not None:
|
||||
return self._current_user
|
||||
|
||||
self._current_user = self.find_user(name=self.whoami())
|
||||
|
||||
return self._current_user
|
||||
|
||||
def iter_groups(self) -> Generator["pwncat.db.Group", None, None]:
|
||||
""" Iterate over all groups on the remote system """
|
||||
@ -830,20 +841,6 @@ class Platform:
|
||||
is also raised.
|
||||
"""
|
||||
|
||||
@property
|
||||
def interactive(self) -> bool:
|
||||
"""
|
||||
Indicates whether the remote victim shell is currently in a state suitable for
|
||||
user-interactivity. Setting this property to True will ensure that a suitable
|
||||
shell prompt is set, echoing is one, etc.
|
||||
"""
|
||||
|
||||
@interactive.setter
|
||||
def interactive(self, value: bool):
|
||||
"""
|
||||
Enable or disable interactivity for this victim.
|
||||
"""
|
||||
|
||||
|
||||
def register(name: str, platform: Type[Platform]):
|
||||
"""
|
||||
|
@ -2,6 +2,7 @@
|
||||
from typing import Generator, List, Union, BinaryIO, Optional
|
||||
from subprocess import CalledProcessError, TimeoutExpired
|
||||
from io import TextIOWrapper, BufferedIOBase, UnsupportedOperation
|
||||
import subprocess
|
||||
import pathlib
|
||||
import pkg_resources
|
||||
import hashlib
|
||||
@ -76,6 +77,19 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
self.stdin, encoding=encoding, errors=errors, write_through=True
|
||||
)
|
||||
|
||||
def detach(self):
|
||||
|
||||
# Indicate the process is complete
|
||||
self.returncode = 0
|
||||
|
||||
# Close file descriptors to prevent further interaction
|
||||
if self.stdout is not None:
|
||||
self.stdout.close()
|
||||
if self.stdin is not None:
|
||||
self.stdin.close()
|
||||
if self.stdout_raw is not None:
|
||||
self.stdout_raw.close()
|
||||
|
||||
def poll(self):
|
||||
|
||||
if self.returncode is not None:
|
||||
@ -956,11 +970,54 @@ class Linux(Platform):
|
||||
PermissionError: the provided password was incorrect
|
||||
"""
|
||||
|
||||
# We need a pty to call `su`
|
||||
self.get_pty()
|
||||
|
||||
if password is None and self.current_user().id != 0:
|
||||
password = self.find_user(name=user).password
|
||||
|
||||
if self.current_user().id != 0 and password is None:
|
||||
raise PermissionError("no password provided")
|
||||
|
||||
# Run `su`
|
||||
proc = self.Popen(
|
||||
["su", user], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
# Assume we don't need a password if we are root
|
||||
if self.current_user().id != 0:
|
||||
|
||||
# Send the password
|
||||
proc.stdin.write(password + "\n")
|
||||
proc.stdin.flush()
|
||||
|
||||
# Retrieve the response (this may take some time if wrong)
|
||||
result = proc.stdout.readline().lower()
|
||||
|
||||
if result == "password: \n":
|
||||
result = proc.stdout.readline().lower()
|
||||
|
||||
# Check for keywords indicating failure
|
||||
if "fail" in result or "incorrect" in result:
|
||||
|
||||
try:
|
||||
# The call failed, wait for the result
|
||||
proc.wait(timeout=5)
|
||||
except TimeoutError:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
# Raise an error. The password was incorrect
|
||||
raise PermissionError("incorrect password")
|
||||
|
||||
proc.detach()
|
||||
|
||||
def sudo(
|
||||
self,
|
||||
command: Union[str, List[str]],
|
||||
user: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
**popen_kwargs,
|
||||
):
|
||||
"""
|
||||
|
@ -5,16 +5,13 @@ from subprocess import (
|
||||
SubprocessError,
|
||||
TimeoutExpired,
|
||||
CalledProcessError,
|
||||
DEVNULL,
|
||||
PIPE,
|
||||
)
|
||||
import io
|
||||
|
||||
import pwncat
|
||||
|
||||
DEVNULL = 0
|
||||
""" Redirect to/from /dev/null or equivalent """
|
||||
PIPE = 1
|
||||
""" Retrieve data via a Pipe """
|
||||
|
||||
|
||||
class Popen:
|
||||
""" Base class for Popen objects defining the interface.
|
||||
|
Loading…
Reference in New Issue
Block a user