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

Organized the report from enum better. It's not more readable.

This commit is contained in:
Caleb Stewart 2020-05-28 00:09:53 -04:00
parent da591f9a22
commit 8461de7182
6 changed files with 207 additions and 21 deletions

View File

@ -1,10 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import os
import textwrap import textwrap
from typing import List, Dict from typing import List, Dict
import pytablewriter
from colorama import Fore, Style from colorama import Fore, Style
from pytablewriter import MarkdownTableWriter
import pwncat import pwncat
from pwncat import util from pwncat import util
@ -14,7 +15,6 @@ from pwncat.commands.base import (
Parameter, Parameter,
StoreConstOnce, StoreConstOnce,
Group, Group,
StoreForAction,
) )
@ -144,13 +144,102 @@ class Command(CommandDefinition):
report_data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {} report_data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {}
hostname = "" hostname = ""
system_details = []
try:
# Grab hostname
hostname = pwncat.victim.enumerate.first("system.hostname").data
system_details.append(["Hostname", hostname])
except ValueError:
hostname = "[unknown-hostname]"
# Not provided by enumerate, but natively known due to our connection
system_details.append(["Primary Address", pwncat.victim.host.ip])
system_details.append(["Derived Hash", pwncat.victim.host.hash])
try:
# Grab distribution
distro = pwncat.victim.enumerate.first("system.distro").data
system_details.append(
["Distribution", f"{distro.name} ({distro.ident}) {distro.version}"]
)
except ValueError:
pass
try:
# Grab the architecture
arch = pwncat.victim.enumerate.first("system.arch").data
system_details.append(["Architecture", arch.arch])
except ValueError:
pass
try:
# Grab kernel version
kernel = pwncat.victim.enumerate.first("system.kernel.version").data
system_details.append(
[
"Kernel",
f"Linux Kernel {kernel.major}.{kernel.minor}.{kernel.patch}-{kernel.abi}",
]
)
except ValueError:
pass
try:
# Grab init system
init = pwncat.victim.enumerate.first("system.init").data
system_details.append(["Init", init.init])
except ValueError:
pass
# Build the table writer for the main section
table_writer = MarkdownTableWriter()
table_writer.headers = ["Property", "Value"]
table_writer.column_styles = [
pytablewriter.style.Style(align="right"),
pytablewriter.style.Style(align="center"),
]
table_writer.value_matrix = system_details
table_writer.margin = 1
# Note enumeration data we don't need anymore
ignore_types = [
"system.hostname",
"system.kernel.version",
"system.distro",
"system.init",
"system.arch",
]
# This is the list of known enumeration types that we want to
# happen first in this order. Other types will still be output
# but will be output in an arbitrary order following this list
ordered_types = [
# Possible kernel exploits - very important
"system.kernel.exploit",
# Enumerated user passwords - very important
"system.user.password",
# Enumerated possible user private keys - very important
"system.user.private_key",
]
# These types are very noisy. They are important for full enumeration,
# but are better suited for the end of the list. These are output last
# no matter what in this order.
noisy_types = [
# System services. There's normally a lot of these
"system.service",
# Installed packages. There's *always* a lot of these
"system.package",
]
util.progress("enumerating report_data") util.progress("enumerating report_data")
for fact in pwncat.victim.enumerate.iter( for fact in pwncat.victim.enumerate.iter(
typ, filter=lambda f: provider is None or f.source == provider typ, filter=lambda f: provider is None or f.source == provider
): ):
util.progress(f"enumerating report_data: {fact.data}") util.progress(f"enumerating report_data: {fact.data}")
if fact.type == "system.hostname": if fact.type in ignore_types:
hostname = str(fact.data) continue
if fact.type not in report_data: if fact.type not in report_data:
report_data[fact.type] = {} report_data[fact.type] = {}
if fact.source not in report_data[fact.type]: if fact.source not in report_data[fact.type]:
@ -162,27 +251,58 @@ class Command(CommandDefinition):
try: try:
with open(report_path, "w") as filp: with open(report_path, "w") as filp:
filp.write(f"# {hostname} - {pwncat.victim.host.ip}\n\n") filp.write(f"# {hostname} - {pwncat.victim.host.ip}\n\n")
# Write the system info table
table_writer.dump(filp, close_after_write=False)
filp.write("\n")
# output ordered types first
for typ in ordered_types:
if typ not in report_data:
continue
self.render_section(filp, typ, report_data[typ])
# output everything that's not a ordered or noisy type
for typ, sources in report_data.items(): for typ, sources in report_data.items():
filp.write(f"## {typ.upper()} Facts\n\n") if typ in ordered_types or typ in noisy_types:
sections = [] continue
for source, facts in sources.items(): self.render_section(filp, typ, sources)
for fact in facts:
if getattr(fact.data, "description", None) is not None:
sections.append(fact)
continue
filp.write(f"- {util.strip_ansi_escape(str(fact.data))}\n")
filp.write("\n") # Output the noisy types
for typ in noisy_types:
if typ not in report_data:
continue
self.render_section(filp, typ, report_data[typ])
for section in sections:
filp.write(
f"### {util.strip_ansi_escape(str(section.data))}\n\n"
)
filp.write(f"```\n{section.data.description}\n```\n\n")
util.success(f"enumeration report written to {report_path}") util.success(f"enumeration report written to {report_path}")
except OSError: except OSError:
self.parser.error(f"{report_path}: failed to open output file") self.parser.error(f"{report_path}: failed to open output file")
def render_section(self, filp, typ: str, sources: Dict[str, List[pwncat.db.Fact]]):
"""
Render the given facts all of the given type to the report pointed to by the open file
filp.
:param filp: the open file containing the report
:param typ: the type all of these facts provide
:param sources: a dictionary of sources->fact list
"""
filp.write(f"## {typ.upper()} Facts\n\n")
sections = []
for source, facts in sources.items():
for fact in facts:
if getattr(fact.data, "description", None) is not None:
sections.append(fact)
continue
filp.write(f"- {util.strip_ansi_escape(str(fact.data))}\n")
filp.write("\n")
for section in sections:
filp.write(f"### {util.strip_ansi_escape(str(section.data))}\n\n")
filp.write(f"```\n{section.data.description}\n```\n\n")
def show_facts(self, typ: str, provider: str, long: bool): def show_facts(self, typ: str, provider: str, long: bool):
""" Display known facts matching the criteria """ """ Display known facts matching the criteria """

View File

@ -114,7 +114,9 @@ class Enumerate:
continue continue
yield fact yield fact
# Add the dummy value # Add the dummy value. We do this after so that
# if the generator was closed, this data will get
# re-enumerated for the missing entries.
if not getattr(enumerator, "always_run", False): if not getattr(enumerator, "always_run", False):
self.add_fact(enumerator.provides, None, dummy_name) self.add_fact(enumerator.provides, None, dummy_name)
@ -126,6 +128,25 @@ class Enumerate:
""" """
yield from self.iter() yield from self.iter()
def first(self, typ: str) -> pwncat.db.Fact:
"""
Find and return the first fact matching this type. Raises a ValueError
if no fact of the given type exists/could be enumerated.
:param typ: the fact type
:return: the fact
:raises: ValueError
"""
try:
iter = self.iter(typ)
fact = next(iter)
iter.close()
except StopIteration:
raise ValueError(f"{typ}: no facts located")
return fact
def add_fact(self, typ: str, data: Any, source: str) -> pwncat.db.Fact: def add_fact(self, typ: str, data: Any, source: str) -> pwncat.db.Fact:
""" """
Register a fact with the fact database. This does not have to come from Register a fact with the fact database. This does not have to come from

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
import dataclasses
from typing import Generator, List
from colorama import Fore
from pwncat.enumerate import FactData
from pwncat import util
import pwncat
name = "pwncat.enumerate.system"
provides = "system.arch"
per_user = False
@dataclasses.dataclass
class ArchData(FactData):
"""
Represents a W.X.Y-Z kernel version where W is the major version,
X is the minor version, Y is the patch, and Z is the ABI.
This explanation came from here:
https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called
"""
arch: str
def __str__(self):
return f"Running on a {Fore.CYAN}{self.arch}{Fore.RESET} processor"
def enumerate() -> Generator[FactData, None, None]:
"""
Enumerate kernel/OS version information
:return:
"""
try:
result = pwncat.victim.env(["uname", "-m"]).decode("utf-8").strip()
except FileNotFoundError:
return
yield ArchData(result)

View File

@ -9,7 +9,7 @@ from pwncat import util
import pwncat import pwncat
name = "pwncat.enumerate.system" name = "pwncat.enumerate.system"
provides = "system.version.distro" provides = "system.distro"
per_user = False per_user = False

View File

@ -8,7 +8,7 @@ import pwncat
from pwncat.enumerate import FactData from pwncat.enumerate import FactData
name = "pwncat.enumerate.capabilities" name = "pwncat.enumerate.capabilities"
provides = "system.packages" provides = "system.package"
per_user = True per_user = True
always_run = False always_run = False

View File

@ -3,6 +3,8 @@ from io import RawIOBase
import socket import socket
import time import time
import pwncat
class RemoteBinaryPipe(RawIOBase): class RemoteBinaryPipe(RawIOBase):
""" Encapsulate a piped interaction with a remote process. The remote PTY """ Encapsulate a piped interaction with a remote process. The remote PTY