1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 19:04:15 +01:00

More module modifications for the move

This commit is contained in:
Caleb Stewart 2020-11-15 14:08:43 -05:00
parent c1068ad567
commit 1a2030e599
10 changed files with 257 additions and 188 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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
View 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()

View File

@ -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 """

View File

@ -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)

View File

@ -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

View File

@ -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]):
"""

View File

@ -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,
):
"""

View File

@ -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.